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.
This commit is contained in:
Dinis Vieira 2023-10-07 17:25:29 +01:00
parent 1dcd3a3daa
commit c92cd90a97
No known key found for this signature in database
GPG key ID: 9389160FF6C295F3
9 changed files with 159 additions and 40 deletions

View file

@ -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<HybridWebView, HybridWebViewHandler> PropertyMapper = new PropertyMapper<HybridWebView, HybridWebViewHandler>(ViewHandler.ViewMapper)
{
[nameof(HybridWebView.Uri)] = MapUri
};
public HybridWebViewHandler() : base(PropertyMapper)
{
}
}
}

View file

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

View file

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

View file

@ -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<HybridWebView, AWebkit.WebView>
{
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<HybridWebViewHandler> _hybridWebViewRenderer;
public JSBridge(HybridWebViewHandler hybridRenderer)
{
_hybridWebViewRenderer = new WeakReference<HybridWebViewHandler>(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);
}
}
}
}

View file

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

View file

@ -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}">
<ContentPage.BindingContext>

View file

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

View file

@ -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<iOS>().SetUseSafeArea(true);
On<iOS>().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());
});
}

View file

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