diff --git a/src/Android/Android.csproj b/src/Android/Android.csproj index 317078f61..bc82b71de 100644 --- a/src/Android/Android.csproj +++ b/src/Android/Android.csproj @@ -297,6 +297,21 @@ ..\..\packages\XLabs.IoC.SimpleInjector.2.0.5782\lib\portable-net45+netcore45+wp8+MonoAndroid1+MonoTouch1\XLabs.Ioc.SimpleInjector.dll + + ..\..\packages\ZXing.Net.Mobile.2.1.47\lib\MonoAndroid403\ZXing.Net.Mobile.Core.dll + + + ..\..\packages\ZXing.Net.Mobile.Forms.2.1.47\lib\MonoAndroid403\ZXing.Net.Mobile.Forms.dll + + + ..\..\packages\ZXing.Net.Mobile.Forms.2.1.47\lib\MonoAndroid403\ZXing.Net.Mobile.Forms.Android.dll + + + ..\..\packages\ZXing.Net.Mobile.2.1.47\lib\MonoAndroid403\zxing.portable.dll + + + ..\..\packages\ZXing.Net.Mobile.2.1.47\lib\MonoAndroid403\ZXingNetMobile.dll + diff --git a/src/Android/MainActivity.cs b/src/Android/MainActivity.cs index 50fe0b678..91817fd5d 100644 --- a/src/Android/MainActivity.cs +++ b/src/Android/MainActivity.cs @@ -199,6 +199,11 @@ namespace Bit.Android ParseYubiKey(intent.DataString); } + public override void OnRequestPermissionsResult(int requestCode, string[] permissions, Permission[] grantResults) + { + ZXing.Net.Mobile.Forms.Android.PermissionsHandler.OnRequestPermissionsResult(requestCode, permissions, grantResults); + } + public void RateApp() { try diff --git a/src/Android/MainApplication.cs b/src/Android/MainApplication.cs index 485a2bca3..048da8bd5 100644 --- a/src/Android/MainApplication.cs +++ b/src/Android/MainApplication.cs @@ -188,6 +188,7 @@ namespace Bit.Android { UserDialogs.Init(application); CachedImageRenderer.Init(); + ZXing.Net.Mobile.Forms.Android.Platform.Init(); CrossFingerprint.SetCurrentActivityResolver(() => CrossCurrentActivity.Current.Activity); //var container = new UnityContainer(); diff --git a/src/Android/Resources/Resource.Designer.cs b/src/Android/Resources/Resource.Designer.cs index 696cd701a..7891cb06b 100644 --- a/src/Android/Resources/Resource.Designer.cs +++ b/src/Android/Resources/Resource.Designer.cs @@ -193,6 +193,12 @@ namespace Bit.Android global::Plugin.Fingerprint.Resource.Layout.FingerprintDialog = global::Bit.Android.Resource.Layout.FingerprintDialog; global::Splat.Resource.String.library_name = global::Bit.Android.Resource.String.library_name; global::Xamarin.Forms.Platform.Android.Resource.Attribute.actionBarSize = global::Bit.Android.Resource.Attribute.actionBarSize; + global::ZXing.Net.Mobile.Forms.Android.Resource.Layout.zxingscanneractivitylayout = global::Bit.Android.Resource.Layout.zxingscanneractivitylayout; + global::ZXing.Net.Mobile.Forms.Android.Resource.Layout.zxingscannerfragmentlayout = global::Bit.Android.Resource.Layout.zxingscannerfragmentlayout; + global::ZXing.Net.Mobile.Forms.Android.Resource.String.library_name = global::Bit.Android.Resource.String.library_name; + global::ZXing.Mobile.Resource.Id.contentFrame = global::Bit.Android.Resource.Id.contentFrame; + global::ZXing.Mobile.Resource.Layout.zxingscanneractivitylayout = global::Bit.Android.Resource.Layout.zxingscanneractivitylayout; + global::ZXing.Mobile.Resource.Layout.zxingscannerfragmentlayout = global::Bit.Android.Resource.Layout.zxingscannerfragmentlayout; } public partial class Animation @@ -2928,6 +2934,9 @@ namespace Bit.Android // aapt resource value: 0x7f0c0027 public const int collapseActionView = 2131492903; + // aapt resource value: 0x7f0c00c6 + public const int contentFrame = 2131493062; + // aapt resource value: 0x7f0c0052 public const int contentPanel = 2131492946; @@ -3684,6 +3693,12 @@ namespace Bit.Android // aapt resource value: 0x7f030042 public const int toolbar = 2130903106; + // aapt resource value: 0x7f030043 + public const int zxingscanneractivitylayout = 2130903107; + + // aapt resource value: 0x7f030044 + public const int zxingscannerfragmentlayout = 2130903108; + static Layout() { global::Android.Runtime.ResourceIdManager.UpdateIdValues(); diff --git a/src/Android/packages.config b/src/Android/packages.config index 36bf933be..9a8c14f86 100644 --- a/src/Android/packages.config +++ b/src/Android/packages.config @@ -92,4 +92,6 @@ + + \ No newline at end of file diff --git a/src/App/App.csproj b/src/App/App.csproj index 31b1ae484..81fac9719 100644 --- a/src/App/App.csproj +++ b/src/App/App.csproj @@ -143,6 +143,7 @@ + @@ -495,6 +496,18 @@ ..\..\packages\XLabs.IoC.2.0.5782\lib\portable-net45+netcore45+wpa81+wp8+MonoAndroid1+MonoTouch1+Xamarin.iOS10\XLabs.Ioc.dll True + + ..\..\packages\ZXing.Net.Mobile.2.1.47\lib\portable-net45+netcore45+wpa81+wp8\ZXing.Net.Mobile.Core.dll + + + ..\..\packages\ZXing.Net.Mobile.Forms.2.1.47\lib\portable-net45+netcore45+wpa81+wp8\ZXing.Net.Mobile.Forms.dll + + + ..\..\packages\ZXing.Net.Mobile.2.1.47\lib\portable-net45+netcore45+wpa81+wp8\zxing.portable.dll + + + ..\..\packages\ZXing.Net.Mobile.2.1.47\lib\portable-net45+netcore45+wpa81+wp8\ZXingNetMobile.dll + diff --git a/src/App/Pages/ScanPage.cs b/src/App/Pages/ScanPage.cs new file mode 100644 index 000000000..51bff9218 --- /dev/null +++ b/src/App/Pages/ScanPage.cs @@ -0,0 +1,148 @@ +using Bit.App.Controls; +using Bit.App.Resources; +using System; +using System.Collections.Generic; +using Xamarin.Forms; +using ZXing.Net.Mobile.Forms; + +namespace Bit.App.Pages +{ + public class ScanPage : ExtendedContentPage + { + private readonly ZXingScannerView _zxing; + private readonly OverlayGrid _overlay; + + public ScanPage(Action callback) + : base(updateActivity: false) + { + _zxing = new ZXingScannerView + { + HorizontalOptions = LayoutOptions.FillAndExpand, + VerticalOptions = LayoutOptions.FillAndExpand, + AutomationId = "zxingScannerView", + Options = new ZXing.Mobile.MobileBarcodeScanningOptions + { + UseNativeScanning = true, + PossibleFormats = new List { ZXing.BarcodeFormat.QR_CODE }, + AutoRotate = false + } + }; + + _zxing.OnScanResult += (result) => + { + // Stop analysis until we navigate away so we don't keep reading barcodes + _zxing.IsAnalyzing = false; + _zxing.IsScanning = false; + + Uri uri; + if(!string.IsNullOrWhiteSpace(result.Text) && Uri.TryCreate(result.Text, UriKind.Absolute, out uri) && + !string.IsNullOrWhiteSpace(uri.Query)) + { + var queryParts = uri.Query.Substring(1).ToLowerInvariant().Split('&'); + foreach(var part in queryParts) + { + if(part.StartsWith("secret=")) + { + callback(part.Substring(7)?.ToUpperInvariant()); + return; + } + } + } + + callback(null); + }; + + _overlay = new OverlayGrid + { + AutomationId = "zxingDefaultOverlay" + }; + + _overlay.TopLabel.Text = AppResources.CameraInstructionTop; + _overlay.BottomLabel.Text = AppResources.CameraInstructionBottom; + + var grid = new Grid + { + VerticalOptions = LayoutOptions.FillAndExpand, + HorizontalOptions = LayoutOptions.FillAndExpand, + Children = { _zxing, _overlay } + }; + + if(Device.RuntimePlatform == Device.iOS) + { + ToolbarItems.Add(new DismissModalToolBarItem(this, AppResources.Close)); + } + + Title = AppResources.ScanQrTitle; + Content = grid; + } + + protected override void OnAppearing() + { + base.OnAppearing(); + _zxing.IsScanning = true; + } + + protected override void OnDisappearing() + { + _zxing.IsScanning = false; + base.OnDisappearing(); + } + + public class OverlayGrid : Grid + { + public OverlayGrid() + { + VerticalOptions = LayoutOptions.FillAndExpand; + HorizontalOptions = LayoutOptions.FillAndExpand; + + RowDefinitions.Add(new RowDefinition { Height = new GridLength(1, GridUnitType.Star) }); + RowDefinitions.Add(new RowDefinition { Height = new GridLength(2, GridUnitType.Star) }); + RowDefinitions.Add(new RowDefinition { Height = new GridLength(1, GridUnitType.Star) }); + + Children.Add(new BoxView + { + VerticalOptions = LayoutOptions.Fill, + HorizontalOptions = LayoutOptions.FillAndExpand, + BackgroundColor = Color.Black, + Opacity = 0.7, + }, 0, 0); + + Children.Add(new BoxView + { + VerticalOptions = LayoutOptions.Center, + HorizontalOptions = LayoutOptions.FillAndExpand, + BackgroundColor = Color.Transparent + }, 0, 1); + + Children.Add(new BoxView + { + VerticalOptions = LayoutOptions.Fill, + HorizontalOptions = LayoutOptions.FillAndExpand, + BackgroundColor = Color.Black, + Opacity = 0.7, + }, 0, 2); + + TopLabel = new Label + { + VerticalOptions = LayoutOptions.Center, + HorizontalOptions = LayoutOptions.Center, + TextColor = Color.White, + AutomationId = "zxingDefaultOverlay_TopTextLabel", + }; + Children.Add(TopLabel, 0, 0); + + BottomLabel = new Label + { + VerticalOptions = LayoutOptions.Center, + HorizontalOptions = LayoutOptions.Center, + TextColor = Color.White, + AutomationId = "zxingDefaultOverlay_BottomTextLabel", + }; + Children.Add(BottomLabel, 0, 2); + } + + public Label TopLabel { get; set; } + public Label BottomLabel { get; set; } + } + } +} diff --git a/src/App/Pages/Vault/VaultAddLoginPage.cs b/src/App/Pages/Vault/VaultAddLoginPage.cs index 296e1c95c..be92cf2a2 100644 --- a/src/App/Pages/Vault/VaultAddLoginPage.cs +++ b/src/App/Pages/Vault/VaultAddLoginPage.cs @@ -276,9 +276,26 @@ namespace Bit.App.Pages PasswordCell.Button.Image = "eye" + (!PasswordCell.Entry.IsPasswordFromToggled ? "_slash" : string.Empty); } - private void TotpButton_Clicked(object sender, EventArgs e) + private async void TotpButton_Clicked(object sender, EventArgs e) { - // launch camera + var scanPage = new ScanPage((key) => + { + Device.BeginInvokeOnMainThread(async () => + { + await Navigation.PopModalAsync(); + if(!string.IsNullOrWhiteSpace(key)) + { + TotpCell.Entry.Text = key; + _userDialogs.Toast(AppResources.AuthenticatorKeyAdded); + } + else + { + _userDialogs.Alert(AppResources.AuthenticatorKeyReadError); + } + }); + }); + + await Navigation.PushModalAsync(new ExtendedNavigationPage(scanPage)); } private async void GenerateCell_Tapped(object sender, EventArgs e) diff --git a/src/App/Pages/Vault/VaultEditLoginPage.cs b/src/App/Pages/Vault/VaultEditLoginPage.cs index a945fed5b..83bce7d6d 100644 --- a/src/App/Pages/Vault/VaultEditLoginPage.cs +++ b/src/App/Pages/Vault/VaultEditLoginPage.cs @@ -298,9 +298,26 @@ namespace Bit.App.Pages PasswordCell.Button.Image = "eye" + (!PasswordCell.Entry.IsPasswordFromToggled ? "_slash" : string.Empty); } - private void TotpButton_Clicked(object sender, EventArgs e) + private async void TotpButton_Clicked(object sender, EventArgs e) { - // launch camera + var scanPage = new ScanPage((key) => + { + Device.BeginInvokeOnMainThread(async () => + { + await Navigation.PopModalAsync(); + if(!string.IsNullOrWhiteSpace(key)) + { + TotpCell.Entry.Text = key; + _userDialogs.Toast(AppResources.AuthenticatorKeyAdded); + } + else + { + _userDialogs.Alert(AppResources.AuthenticatorKeyReadError); + } + }); + }); + + await Navigation.PushModalAsync(new ExtendedNavigationPage(scanPage)); } private async void GenerateCell_Tapped(object sender, EventArgs e) diff --git a/src/App/Resources/AppResources.Designer.cs b/src/App/Resources/AppResources.Designer.cs index 585281f2b..6549689ee 100644 --- a/src/App/Resources/AppResources.Designer.cs +++ b/src/App/Resources/AppResources.Designer.cs @@ -187,6 +187,24 @@ namespace Bit.App.Resources { } } + /// + /// Looks up a localized string similar to Authenticator key added.. + /// + public static string AuthenticatorKeyAdded { + get { + return ResourceManager.GetString("AuthenticatorKeyAdded", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Cannot read authenticator key.. + /// + public static string AuthenticatorKeyReadError { + get { + return ResourceManager.GetString("AuthenticatorKeyReadError", resourceCulture); + } + } + /// /// Looks up a localized string similar to Auto-fill. /// @@ -430,6 +448,24 @@ namespace Bit.App.Resources { } } + /// + /// Looks up a localized string similar to Scanning will happen automatically.. + /// + public static string CameraInstructionBottom { + get { + return ResourceManager.GetString("CameraInstructionBottom", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Point your camera at the QR code.. + /// + public static string CameraInstructionTop { + get { + return ResourceManager.GetString("CameraInstructionTop", resourceCulture); + } + } + /// /// Looks up a localized string similar to Cancel. /// @@ -1771,6 +1807,15 @@ namespace Bit.App.Resources { } } + /// + /// Looks up a localized string similar to Scan QR Code. + /// + public static string ScanQrTitle { + get { + return ResourceManager.GetString("ScanQrTitle", resourceCulture); + } + } + /// /// Looks up a localized string similar to Search. /// diff --git a/src/App/Resources/AppResources.resx b/src/App/Resources/AppResources.resx index b55e52299..c16f1b227 100644 --- a/src/App/Resources/AppResources.resx +++ b/src/App/Resources/AppResources.resx @@ -929,4 +929,19 @@ Verification Code (TOTP) Totp code label + + Authenticator key added. + + + Cannot read authenticator key. + + + Scanning will happen automatically. + + + Point your camera at the QR code. + + + Scan QR Code + \ No newline at end of file diff --git a/src/App/packages.config b/src/App/packages.config index d3d51f970..b3fc01737 100644 --- a/src/App/packages.config +++ b/src/App/packages.config @@ -24,4 +24,6 @@ + + \ No newline at end of file diff --git a/src/iOS/AppDelegate.cs b/src/iOS/AppDelegate.cs index 2553818ae..6e31ee15e 100644 --- a/src/iOS/AppDelegate.cs +++ b/src/iOS/AppDelegate.cs @@ -116,6 +116,7 @@ namespace Bit.iOS UIApplication.SharedApplication.SetStatusBarHidden(!show, false); }); + ZXing.Net.Mobile.Forms.iOS.Platform.Init(); return base.FinishedLaunching(app, options); } diff --git a/src/iOS/Info.plist b/src/iOS/Info.plist index e121e7a0b..f73fd681f 100644 --- a/src/iOS/Info.plist +++ b/src/iOS/Info.plist @@ -101,5 +101,7 @@ en NSPhotoLibraryUsageDescription This app does not require access to the photo library. + NSCameraUsageDescription + Scan QR codes diff --git a/src/iOS/iOS.csproj b/src/iOS/iOS.csproj index 4b869fbbe..c251a44e9 100644 --- a/src/iOS/iOS.csproj +++ b/src/iOS/iOS.csproj @@ -412,6 +412,21 @@ ..\..\packages\XLabs.IoC.SimpleInjector.2.0.5782\lib\portable-net45+netcore45+wp8+MonoAndroid1+MonoTouch1\XLabs.Ioc.SimpleInjector.dll + + ..\..\packages\ZXing.Net.Mobile.2.1.47\lib\Xamarin.iOS10\ZXing.Net.Mobile.Core.dll + + + ..\..\packages\ZXing.Net.Mobile.Forms.2.1.47\lib\Xamarin.iOS10\ZXing.Net.Mobile.Forms.dll + + + ..\..\packages\ZXing.Net.Mobile.Forms.2.1.47\lib\Xamarin.iOS10\ZXing.Net.Mobile.Forms.iOS.dll + + + ..\..\packages\ZXing.Net.Mobile.2.1.47\lib\Xamarin.iOS10\zxing.portable.dll + + + ..\..\packages\ZXing.Net.Mobile.2.1.47\lib\Xamarin.iOS10\ZXingNetMobile.dll + diff --git a/src/iOS/packages.config b/src/iOS/packages.config index ab5e46578..18a9957a7 100644 --- a/src/iOS/packages.config +++ b/src/iOS/packages.config @@ -78,4 +78,6 @@ + + \ No newline at end of file