diff --git a/src/Android/Android.csproj b/src/Android/Android.csproj index a893480fc..c5a1d6637 100644 --- a/src/Android/Android.csproj +++ b/src/Android/Android.csproj @@ -43,6 +43,7 @@ + @@ -87,6 +88,7 @@ + diff --git a/src/Android/Renderers/HybridWebViewRenderer.cs b/src/Android/Renderers/HybridWebViewRenderer.cs new file mode 100644 index 000000000..b5dc81824 --- /dev/null +++ b/src/Android/Renderers/HybridWebViewRenderer.cs @@ -0,0 +1,88 @@ +using System; +using Bit.App.Controls; +using Xamarin.Forms; +using Xamarin.Forms.Platform.Android; +using Android.Webkit; +using AWebkit = Android.Webkit; +using Java.Interop; +using Android.Content; +using Bit.Droid.Renderers; + +[assembly: ExportRenderer(typeof(HybridWebView), typeof(HybridWebViewRenderer))] +namespace Bit.Droid.Renderers +{ + public class HybridWebViewRenderer : ViewRenderer + { + private const string JSFunction = "function invokeCSharpAction(data){jsBridge.invokeAction(data);}"; + + private readonly Context _context; + + public HybridWebViewRenderer(Context context) + : base(context) + { + _context = context; + } + + protected override void OnElementChanged(ElementChangedEventArgs e) + { + base.OnElementChanged(e); + + if(Control == null) + { + var webView = new AWebkit.WebView(_context); + webView.Settings.JavaScriptEnabled = true; + webView.SetWebViewClient(new JSWebViewClient(string.Format("javascript: {0}", JSFunction))); + SetNativeControl(webView); + } + if(e.OldElement != null) + { + Control.RemoveJavascriptInterface("jsBridge"); + var hybridWebView = e.OldElement as HybridWebView; + hybridWebView.Cleanup(); + } + if(e.NewElement != null) + { + Control.AddJavascriptInterface(new JSBridge(this), "jsBridge"); + Control.LoadUrl(Element.Uri); + } + } + + public class JSBridge : Java.Lang.Object + { + private readonly WeakReference _hybridWebViewRenderer; + + public JSBridge(HybridWebViewRenderer hybridRenderer) + { + _hybridWebViewRenderer = new WeakReference(hybridRenderer); + } + + [JavascriptInterface] + [Export("invokeAction")] + public void InvokeAction(string data) + { + if(_hybridWebViewRenderer != null && + _hybridWebViewRenderer.TryGetTarget(out HybridWebViewRenderer hybridRenderer)) + { + hybridRenderer.Element.InvokeAction(data); + } + } + } + + public class JSWebViewClient : WebViewClient + { + private readonly string _javascript; + + public JSWebViewClient(string javascript) + { + _javascript = javascript; + + } + + public override void OnPageFinished(AWebkit.WebView view, string url) + { + base.OnPageFinished(view, url); + view.EvaluateJavascript(_javascript, null); + } + } + } +} diff --git a/src/App/Controls/HybridWebView.cs b/src/App/Controls/HybridWebView.cs new file mode 100644 index 000000000..f8976cce6 --- /dev/null +++ b/src/App/Controls/HybridWebView.cs @@ -0,0 +1,34 @@ +using System; +using Xamarin.Forms; + +namespace Bit.App.Controls +{ + public class HybridWebView : View + { + private Action _func; + + public static readonly BindableProperty UriProperty = BindableProperty.Create(propertyName: nameof(Uri), + returnType: typeof(string), declaringType: typeof(HybridWebView), defaultValue: default(string)); + + public string Uri + { + get { return (string)GetValue(UriProperty); } + set { SetValue(UriProperty, value); } + } + + public void RegisterAction(Action callback) + { + _func = callback; + } + + public void Cleanup() + { + _func = null; + } + + public void InvokeAction(string data) + { + _func?.Invoke(data); + } + } +} diff --git a/src/iOS/Renderers/HybridWebViewRenderer.cs b/src/iOS/Renderers/HybridWebViewRenderer.cs new file mode 100644 index 000000000..13b45a39c --- /dev/null +++ b/src/iOS/Renderers/HybridWebViewRenderer.cs @@ -0,0 +1,51 @@ +using Foundation; +using WebKit; +using Xamarin.Forms; +using Xamarin.Forms.Platform.iOS; +using Bit.App.Controls; +using Bit.iOS.Renderers; + +[assembly: ExportRenderer(typeof(HybridWebView), typeof(HybridWebViewRenderer))] +namespace Bit.iOS.Renderers +{ + public class HybridWebViewRenderer : ViewRenderer, IWKScriptMessageHandler + { + private const string JSFunction = + "function invokeCSharpAction(data){window.webkit.messageHandlers.invokeAction.postMessage(data);}"; + + private WKUserContentController _userController; + + protected override void OnElementChanged(ElementChangedEventArgs e) + { + base.OnElementChanged(e); + + if(Control == null) + { + _userController = new WKUserContentController(); + var script = new WKUserScript(new NSString(JSFunction), WKUserScriptInjectionTime.AtDocumentEnd, false); + _userController.AddUserScript(script); + _userController.AddScriptMessageHandler(this, "invokeAction"); + + var config = new WKWebViewConfiguration { UserContentController = _userController }; + var webView = new WKWebView(Frame, config); + SetNativeControl(webView); + } + if(e.OldElement != null) + { + _userController.RemoveAllUserScripts(); + _userController.RemoveScriptMessageHandler("invokeAction"); + var hybridWebView = e.OldElement as HybridWebView; + hybridWebView.Cleanup(); + } + if(e.NewElement != null) + { + Control.LoadRequest(new NSUrlRequest(new NSUrl(Element.Uri))); + } + } + + public void DidReceiveScriptMessage(WKUserContentController userContentController, WKScriptMessage message) + { + Element.InvokeAction(message.Body.ToString()); + } + } +} diff --git a/src/iOS/iOS.csproj b/src/iOS/iOS.csproj index 04925e687..d0b42c801 100644 --- a/src/iOS/iOS.csproj +++ b/src/iOS/iOS.csproj @@ -92,6 +92,7 @@ +