0

I have a Xamarin Forms app that needs to call a web page that contains a custom Bing Map and on the web page it needs to be able to get the users current location.
The app already has the required "app" permissions for both Android and iOS and I can get the users location internally in the app without issues.
When I call my external web page that has a custom Bing Map, it does allow me to get the users location in both Android and iOS BUT on iOS it asks the user "website... Would Like To Use Your Current Location. Don't Allow / OK" each time they visit the web page from inside my Xamarin app.

I found the following articles that help point me in the right directions but I just don't understand enough of iOS to piece it together.
How to prevent WKWebView to repeatedly ask for permission to access location? -- This looks like what I need but it is in iOS and does not use WkWebViewRender which is required for newer iOS apps.
https://learn.microsoft.com/en-us/xamarin/xamarin-forms/app-fundamentals/custom-renderer/hybridwebview -- This shows me how to add "script" to the iOS side but I can't get the DidReceiveScriptMessage method to fire.

Here is my current implementation of WkWebViewRender.

public class HybridWebViewRenderer : WkWebViewRenderer, IWKScriptMessageHandler
{
    public HybridWebViewRenderer() : this(new WKWebViewConfiguration())
    {
    }        

    const string JavaScriptFunctionTest = 
        "navigator.geolocation.getCurrentPosition = function(success, error, options) {window.webkit.messageHandlers.locationHandler.postMessage('getCurrentPosition');};";

    WKUserContentController userController;

    public HybridWebViewRenderer(WKWebViewConfiguration config) : base(config)
    {
        try
        {
            userController = config.UserContentController;
            var script = new WKUserScript(new NSString(JavaScriptFunctionTest), injectionTime: WKUserScriptInjectionTime.AtDocumentEnd, isForMainFrameOnly: true);
            userController.AddUserScript(script);
            userController.AddScriptMessageHandler(this, "locationHandler");
        }
        catch (System.Exception ex)
        {
                
        }
    }

    public void DidReceiveScriptMessage(WKUserContentController userContentController, WKScriptMessage message)
    {
        var msg = message.Body.ToString();
        System.Diagnostics.Debug.WriteLine(msg);
    }
}

And here is the only javascript functions in my html page.

function StartTracking() {
    //Add a pushpin to show the user's location.
    userPin = new Microsoft.Maps.Pushpin(map.getCenter(), { visible: true });
    map.entities.push(userPin);

    //Watch the users location.
    watchId = navigator.geolocation.watchPosition(UsersLocationUpdated);
}

function UsersLocationUpdated(position) {
    var loc = new Microsoft.Maps.Location(
        position.coords.latitude,
        position.coords.longitude);

    //Update the user pushpin.
    userPin.setLocation(loc);
    userPin.setOptions({ visible: true });

    //Center the map on the user's location.
    map.setView({ center: loc });
}

function StopTracking() {
    // Cancel the geolocation updates.
    navigator.geolocation.clearWatch(watchId);

    //Remove the user pushpin.
    map.entities.clear();
}

https://gist.github.com/hayahyts/2c369563b2e9f244356eb6228ffba261 is so close to what I need but I must be doing something wrong.
The DidReceiveScriptMessage does not get called if I use

const string JavaScriptFunction = 
    "navigator.geolocation.getCurrentPosition " + 
    " = function(success, error, options) " + 
    "{window.webkit.messageHandlers.locationHandler.postMessage('getCurrentPosition');};";

BUT DidReceiveScriptMessage does get called if I use

const string JavaScriptFunction = 
    "function invokeCSharpAction(data){window.webkit.messageHandlers.invokeAction.postMessage(data);}";

So I'm not sure what is wrong just yet but it must be with the locationHandler replacement code.

goroth
  • 2,510
  • 5
  • 35
  • 66

2 Answers2

1

Renderer namespace with ExportRenderer attribute:

[assembly: ExportRenderer(typeof(WebView), typeof(MyNamespace.MyWebViewRenderer))]
namespace MyNamespace
{

Renderer declaration with reference to IWKScriptMessageHandler:

 public class MyWebViewRenderer : WkWebViewRenderer, IWKScriptMessageHandler
 {

Renderer fields:

  const string MessageHandlerName = "locationHandler";
  const string JavaScriptFunction =
        "navigator.geolocation.getCurrentPosition = function(success, error, options) {window.webkit.messageHandlers.locationHandler.postMessage('getCurrentPosition');};";
  //const string HtmlSource = "html containing navigator.geolocation.getCurrentPosition"

Renderer SetupScripts in OnElementChanged override:

  protected override void OnElementChanged(VisualElementChangedEventArgs e)
  {
    base.OnElementChanged(e);
    if (e.NewElement != null)
    {
      SetupScripts(Configuration);
      //LoadHtmlString(HtmlSource, null); //example loading page
    }
  }

  void SetupScripts(WKWebViewConfiguration wkWebViewConfig)
  {
    wkWebViewConfig.UserContentController.AddScriptMessageHandler(this, MessageHandlerName);
    var jsFunction = new WKUserScript(new NSString(JavaScriptFunction), WKUserScriptInjectionTime.AtDocumentEnd, false);            
    wkWebViewConfig.UserContentController.AddUserScript(jsFunction);
  }

Renderer implementation of IWKScriptMessageHandler:

  //IWKScriptMessageHandler
  public void DidReceiveScriptMessage(WKUserContentController userContentController, WKScriptMessage message)
  {
     //should be invoked when getCurrentPosition in HtmlSource is invoked
  }
Benl
  • 2,777
  • 9
  • 13
  • The key piece I was missing was "locationHandler". Looks like it must match the name right before the "postMessage" to get the DidReceiveScriptMessage to fire. – goroth Jun 03 '21 at 18:28
0

Try to create a new class which inherit from WKScriptMessageHandler to handle the DidReceiveScriptMessage method .

Modify your code as below

  1. Change the Handler
userController.AddScriptMessageHandler(new myClass(), "invokeAction");
  1. Remove WKScriptMessageHandler interface from HybridWebViewRenderer .
public class HybridWebViewRenderer : WkWebViewRenderer
  1. Create a subclass of WKScriptMessageHandler .
    public class myClass : WKScriptMessageHandler
    {

        public override void DidReceiveScriptMessage(WKUserContentController userContentController, WKScriptMessage message)
        {
            //_webview.InvokeAction(message.Body.ToString());
        }
    }

Check the similar issue : https://forums.xamarin.com/discussion/169893/wkscriptmessagehandler-is-not-firing .

ColeX
  • 14,062
  • 5
  • 43
  • 240
  • I guess I should have clarified that the sample project for the HybridWebView does work in iOS in that it fires the DidReceiveScriptMessage when using the IWKScriptMessageHandler and calling the sample messageHandlers.invokeAction method but it does not work when calling messageHandlers.locationHandler. My best guess is that I am wiring up something wrong or that there is something wrong in my javascript. – goroth Jun 03 '21 at 13:10