From c92cd90a973970330b25c08bfab058b9c0c2ce4a Mon Sep 17 00:00:00 2001 From: Dinis Vieira Date: Sat, 7 Oct 2023 17:25:29 +0100 Subject: [PATCH] PM-3349 Implemented HybridWebViewHandler for Android which enables 2nd factor auth flows Ensured CustomTabbedPageHandler had it's DisconnectHandler called Some minor code upgrades of older obsolete Xamarin Forms code. --- src/App/Handlers/HybridWebViewHandler.cs | 25 +++++ src/App/MauiProgram.cs | 5 +- .../Handlers/CustomTabbedPageHandler.cs | 2 +- .../Android/Handlers/HybridWebViewHandler.cs | 96 +++++++++++++++++++ src/Core/Controls/HybridWebView.cs | 6 +- src/Core/Pages/Accounts/TwoFactorPage.xaml | 1 + src/Core/Pages/Accounts/TwoFactorPage.xaml.cs | 36 +++---- src/Core/Pages/BaseContentPage.cs | 21 ++-- src/Core/Pages/TabsPage.cs | 7 ++ 9 files changed, 159 insertions(+), 40 deletions(-) create mode 100644 src/App/Handlers/HybridWebViewHandler.cs create mode 100644 src/App/Platforms/Android/Handlers/HybridWebViewHandler.cs diff --git a/src/App/Handlers/HybridWebViewHandler.cs b/src/App/Handlers/HybridWebViewHandler.cs new file mode 100644 index 000000000..eb8acc495 --- /dev/null +++ b/src/App/Handlers/HybridWebViewHandler.cs @@ -0,0 +1,25 @@ +#if IOS || MACCATALYST +using PlatformView = WebKit.WKWebView; +#elif ANDROID +using PlatformView = Android.Webkit.WebView; +#elif (NETSTANDARD || !PLATFORM) || (NET6_0_OR_GREATER && !IOS && !ANDROID) +using PlatformView = System.Object; +#endif + +using Bit.App.Controls; +using Microsoft.Maui.Handlers; + +namespace Bit.App.Handlers +{ + public partial class HybridWebViewHandler + { + public static PropertyMapper PropertyMapper = new PropertyMapper(ViewHandler.ViewMapper) + { + [nameof(HybridWebView.Uri)] = MapUri + }; + + public HybridWebViewHandler() : base(PropertyMapper) + { + } + } +} diff --git a/src/App/MauiProgram.cs b/src/App/MauiProgram.cs index 106bb7aa0..5645bcd2d 100644 --- a/src/App/MauiProgram.cs +++ b/src/App/MauiProgram.cs @@ -1,4 +1,4 @@ -namespace Bit.App +namespace Bit.App { public class MauiProgram { @@ -28,7 +28,8 @@ namespace Bit.App Bit.App.Handlers.TimePickerHandlerMappings.Setup(); Bit.App.Handlers.ButtonHandlerMappings.Setup(); - handlers.AddHandler(typeof(TabbedPage), typeof(Bit.App.Handlers.CustomTabbedPageHandler)); + handlers.AddHandler(typeof(Bit.App.Pages.TabsPage), typeof(Bit.App.Handlers.CustomTabbedPageHandler)); + handlers.AddHandler(typeof(Bit.App.Controls.HybridWebView), typeof(Bit.App.Handlers.HybridWebViewHandler)); #else iOS.Core.Utilities.iOSCoreHelpers.ConfigureMAUIHandlers(handlers); #endif diff --git a/src/App/Platforms/Android/Handlers/CustomTabbedPageHandler.cs b/src/App/Platforms/Android/Handlers/CustomTabbedPageHandler.cs index 930922484..8d863e003 100644 --- a/src/App/Platforms/Android/Handlers/CustomTabbedPageHandler.cs +++ b/src/App/Platforms/Android/Handlers/CustomTabbedPageHandler.cs @@ -1,5 +1,4 @@ using AndroidX.AppCompat.View.Menu; -using AndroidX.Navigation.UI; using Bit.Core.Abstractions; using Bit.Core.Utilities; using Google.Android.Material.BottomNavigation; @@ -95,6 +94,7 @@ namespace Bit.App.Handlers } } + //Currently the Disconnect Handler needs to be manually called from the App: https://github.com/dotnet/maui/issues/3604 protected override void DisconnectHandler(global::Android.Views.View platformView) { if(_bottomNavigationViewGroup != null) diff --git a/src/App/Platforms/Android/Handlers/HybridWebViewHandler.cs b/src/App/Platforms/Android/Handlers/HybridWebViewHandler.cs new file mode 100644 index 000000000..390bb211c --- /dev/null +++ b/src/App/Platforms/Android/Handlers/HybridWebViewHandler.cs @@ -0,0 +1,96 @@ +using Bit.App.Controls; +using Java.Interop; +using JetBrains.Annotations; +using Microsoft.Maui.Handlers; +using AWebkit = Android.Webkit; + +namespace Bit.App.Handlers +{ + public partial class HybridWebViewHandler : ViewHandler + { + private const string JSFunction = "function invokeCSharpAction(data){jsBridge.invokeAction(data);}"; + + public HybridWebViewHandler([NotNull] IPropertyMapper mapper, CommandMapper commandMapper = null) : base(mapper, commandMapper) + { + } + + protected override AWebkit.WebView CreatePlatformView() + { + var context = MauiContext?.Context ?? throw new InvalidOperationException($"Context cannot be null here"); + var webView = new AWebkit.WebView(context); + webView.Settings.JavaScriptEnabled = true; + webView.SetWebViewClient(new JSWebViewClient(string.Format("javascript: {0}", JSFunction))); + return webView; + } + + public static void MapUri(HybridWebViewHandler handler, HybridWebView view) + { + if (view != null && view.Uri != null) + { + handler?.PlatformView?.LoadUrl(view.Uri); + } + } + + protected override void ConnectHandler(AWebkit.WebView platformView) + { + platformView?.AddJavascriptInterface(new JSBridge(this), "jsBridge"); + platformView?.LoadUrl(VirtualView?.Uri); + + base.ConnectHandler(platformView); + } + + //Currently the Disconnect Handler needs to be manually called from the App: https://github.com/dotnet/maui/issues/3604 + protected override void DisconnectHandler(AWebkit.WebView platformView) + { + platformView?.RemoveJavascriptInterface("jsBridge"); + platformView?.Dispose(); + VirtualView?.Cleanup(); + + base.DisconnectHandler(platformView); + } + + internal void InvokeActionOnVirtual(string data) + { + VirtualView?.InvokeAction(data); + } + } + + public class JSBridge : Java.Lang.Object + { + private readonly WeakReference _hybridWebViewRenderer; + + public JSBridge(HybridWebViewHandler hybridRenderer) + { + _hybridWebViewRenderer = new WeakReference(hybridRenderer); + } + + [AWebkit.JavascriptInterface] + [Export("invokeAction")] + public void InvokeAction(string data) + { + if (_hybridWebViewRenderer != null &&_hybridWebViewRenderer.TryGetTarget(out HybridWebViewHandler hybridRenderer)) + { + hybridRenderer?.InvokeActionOnVirtual(data); + } + } + } + + public class JSWebViewClient : AWebkit.WebViewClient + { + private readonly string _javascript; + + public JSWebViewClient(string javascript) + { + _javascript = javascript; + } + + public override void OnPageFinished(AWebkit.WebView view, string url) + { + base.OnPageFinished(view, url); + if (view != null) + { + view.EvaluateJavascript(_javascript, null); + } + } + } +} diff --git a/src/Core/Controls/HybridWebView.cs b/src/Core/Controls/HybridWebView.cs index 801d9b2f6..d292f7498 100644 --- a/src/Core/Controls/HybridWebView.cs +++ b/src/Core/Controls/HybridWebView.cs @@ -1,8 +1,4 @@ -using System; -using Microsoft.Maui.Controls; -using Microsoft.Maui; - -namespace Bit.App.Controls +namespace Bit.App.Controls { public class HybridWebView : View { diff --git a/src/Core/Pages/Accounts/TwoFactorPage.xaml b/src/Core/Pages/Accounts/TwoFactorPage.xaml index 77c20e13c..1c57e0e30 100644 --- a/src/Core/Pages/Accounts/TwoFactorPage.xaml +++ b/src/Core/Pages/Accounts/TwoFactorPage.xaml @@ -7,6 +7,7 @@ xmlns:controls="clr-namespace:Bit.App.Controls" xmlns:u="clr-namespace:Bit.App.Utilities" x:DataType="pages:TwoFactorPageViewModel" + Unloaded="TwoFactorPage_OnUnloaded" Title="{Binding PageTitle}"> diff --git a/src/Core/Pages/Accounts/TwoFactorPage.xaml.cs b/src/Core/Pages/Accounts/TwoFactorPage.xaml.cs index 199afd934..1dd59a971 100644 --- a/src/Core/Pages/Accounts/TwoFactorPage.xaml.cs +++ b/src/Core/Pages/Accounts/TwoFactorPage.xaml.cs @@ -1,13 +1,8 @@ -using System; -using System.Threading.Tasks; -using Bit.App.Controls; +using Bit.App.Controls; using Bit.App.Models; using Bit.App.Utilities; using Bit.Core.Abstractions; -using Bit.Core.Services; using Bit.Core.Utilities; -using Microsoft.Maui.Controls; -using Microsoft.Maui; namespace Bit.App.Pages { @@ -33,24 +28,23 @@ namespace Bit.App.Pages _vm.Page = this; _vm.AuthingWithSso = authingWithSso ?? false; _vm.StartSetPasswordAction = () => - Device.BeginInvokeOnMainThread(async () => await StartSetPasswordAsync()); + MainThread.BeginInvokeOnMainThread(async () => await StartSetPasswordAsync()); _vm.TwoFactorAuthSuccessAction = () => - Device.BeginInvokeOnMainThread(async () => await TwoFactorAuthSuccessToMainAsync()); + MainThread.BeginInvokeOnMainThread(async () => await TwoFactorAuthSuccessToMainAsync()); _vm.LockAction = () => - Device.BeginInvokeOnMainThread(TwoFactorAuthSuccessWithSSOLocked); + MainThread.BeginInvokeOnMainThread(TwoFactorAuthSuccessWithSSOLocked); _vm.UpdateTempPasswordAction = - () => Device.BeginInvokeOnMainThread(async () => await UpdateTempPasswordAsync()); + () => MainThread.BeginInvokeOnMainThread(async () => await UpdateTempPasswordAsync()); _vm.StartDeviceApprovalOptionsAction = - () => Device.BeginInvokeOnMainThread(async () => await StartDeviceApprovalOptionsAsync()); + () => MainThread.BeginInvokeOnMainThread(async () => await StartDeviceApprovalOptionsAsync()); _vm.CloseAction = async () => await Navigation.PopModalAsync(); DuoWebView = _duoWebView; - // TODO Xamarin.Forms.Device.RuntimePlatform is no longer supported. Use Microsoft.Maui.Devices.DeviceInfo.Platform instead. For more details see https://learn.microsoft.com/en-us/dotnet/maui/migration/forms-projects#device-changes - if (Device.RuntimePlatform == Device.Android) + + if (DeviceInfo.Platform == DevicePlatform.Android) { ToolbarItems.Remove(_cancelItem); } - // TODO Xamarin.Forms.Device.RuntimePlatform is no longer supported. Use Microsoft.Maui.Devices.DeviceInfo.Platform instead. For more details see https://learn.microsoft.com/en-us/dotnet/maui/migration/forms-projects#device-changes - if (Device.RuntimePlatform == Device.iOS) + if (DeviceInfo.Platform == DevicePlatform.iOS) { ToolbarItems.Add(_moreItem); } @@ -62,7 +56,7 @@ namespace Bit.App.Pages public HybridWebView DuoWebView { get; set; } - protected async override void OnAppearing() + protected override async void OnAppearing() { base.OnAppearing(); _broadcasterService.Subscribe(nameof(TwoFactorPage), (message) => @@ -73,7 +67,7 @@ namespace Bit.App.Pages if (_vm.YubikeyMethod && !string.IsNullOrWhiteSpace(token) && token.Length == 44 && !token.Contains(" ")) { - Device.BeginInvokeOnMainThread(async () => + MainThread.BeginInvokeOnMainThread(async () => { _vm.Token = token; await _vm.SubmitAsync(); @@ -107,7 +101,7 @@ namespace Bit.App.Pages return Task.FromResult(0); }); } - + protected override void OnDisappearing() { base.OnDisappearing(); @@ -117,6 +111,12 @@ namespace Bit.App.Pages _broadcasterService.Unsubscribe(nameof(TwoFactorPage)); } } + + private void TwoFactorPage_OnUnloaded(object sender, EventArgs e) + { + _duoWebView?.Handler?.DisconnectHandler(); + } + protected override bool OnBackButtonPressed() { if (_vm.YubikeyMethod) diff --git a/src/Core/Pages/BaseContentPage.cs b/src/Core/Pages/BaseContentPage.cs index b074f867b..0ad77c0a6 100644 --- a/src/Core/Pages/BaseContentPage.cs +++ b/src/Core/Pages/BaseContentPage.cs @@ -1,15 +1,10 @@ -using System; -using System.Threading.Tasks; -using Bit.App.Abstractions; +using Bit.App.Abstractions; using Bit.App.Controls; using Bit.App.Utilities; using Bit.Core.Abstractions; using Bit.Core.Utilities; using Microsoft.Maui.Controls.PlatformConfiguration; using Microsoft.Maui.Controls.PlatformConfiguration.iOSSpecific; -using Microsoft.Maui.ApplicationModel.Communication; -using Microsoft.Maui.Controls; -using Microsoft.Maui; namespace Bit.App.Pages { @@ -23,8 +18,7 @@ namespace Bit.App.Pages public BaseContentPage() { - // TODO Xamarin.Forms.Device.RuntimePlatform is no longer supported. Use Microsoft.Maui.Devices.DeviceInfo.Platform instead. For more details see https://learn.microsoft.com/en-us/dotnet/maui/migration/forms-projects#device-changes - if (Device.RuntimePlatform == Device.iOS) + if (DeviceInfo.Platform == DevicePlatform.iOS) { On().SetUseSafeArea(true); On().SetModalPresentationStyle(UIModalPresentationStyle.FullScreen); @@ -35,7 +29,7 @@ namespace Bit.App.Pages public bool IsThemeDirty { get; set; } - protected async override void OnAppearing() + protected override async void OnAppearing() { base.OnAppearing(); @@ -70,7 +64,7 @@ namespace Bit.App.Pages var indicator = new ActivityIndicator { IsRunning = true, - VerticalOptions = LayoutOptions.CenterAndExpand, + VerticalOptions = LayoutOptions.Center, HorizontalOptions = LayoutOptions.Center, Color = ThemeManager.GetResourceColor("PrimaryColor"), }; @@ -102,8 +96,7 @@ namespace Bit.App.Pages } } } - // TODO Xamarin.Forms.Device.RuntimePlatform is no longer supported. Use Microsoft.Maui.Devices.DeviceInfo.Platform instead. For more details see https://learn.microsoft.com/en-us/dotnet/maui/migration/forms-projects#device-changes - if (Device.RuntimePlatform == Device.iOS) + if (DeviceInfo.Platform == DevicePlatform.iOS) { await DoWorkAsync(); return; @@ -111,7 +104,7 @@ namespace Bit.App.Pages await Task.Run(async () => { await Task.Delay(fromModal ? ShowModalAnimationDelay : ShowPageAnimationDelay); - Device.BeginInvokeOnMainThread(async () => await DoWorkAsync()); + MainThread.BeginInvokeOnMainThread(async () => await DoWorkAsync()); }); } @@ -120,7 +113,7 @@ namespace Bit.App.Pages Task.Run(async () => { await Task.Delay(ShowModalAnimationDelay); - Device.BeginInvokeOnMainThread(() => input.Focus()); + MainThread.BeginInvokeOnMainThread(() => input.Focus()); }); } diff --git a/src/Core/Pages/TabsPage.cs b/src/Core/Pages/TabsPage.cs index 036748523..0a95a650c 100644 --- a/src/Core/Pages/TabsPage.cs +++ b/src/Core/Pages/TabsPage.cs @@ -56,6 +56,8 @@ namespace Bit.App.Pages }; Children.Add(settingsPage); + Unloaded += OnUnloaded; + if (DeviceInfo.Platform == DevicePlatform.Android) { Microsoft.Maui.Controls.PlatformConfiguration.AndroidSpecific.TabbedPage.SetToolbarPlacement(this, @@ -109,6 +111,11 @@ namespace Bit.App.Pages _broadcasterService.Unsubscribe(nameof(TabsPage)); } + private void OnUnloaded(object sender, EventArgs e) + { + Handler?.DisconnectHandler(); + } + public void ResetToVaultPage() { CurrentPage = _groupingsPage;