diff --git a/src/Controls/src/Core/HybridWebView/HybridWebViewWebResourceRequestedEventArgs.cs b/src/Controls/src/Core/HybridWebView/HybridWebViewWebResourceRequestedEventArgs.cs index 9593caac770d..41b770a34390 100644 --- a/src/Controls/src/Core/HybridWebView/HybridWebViewWebResourceRequestedEventArgs.cs +++ b/src/Controls/src/Core/HybridWebView/HybridWebViewWebResourceRequestedEventArgs.cs @@ -50,13 +50,13 @@ public HybridWebViewWebResourceRequestedEventArgs(Uri uri, string method) /// Gets the headers associated with the request. /// public IReadOnlyDictionary Headers => - _headers ??= PlatformArgs?.GetRequestHeaders() ?? new Dictionary(); + _headers ??= PlatformArgs?.GetRequestHeaders() ?? new Dictionary(StringComparer.OrdinalIgnoreCase); /// /// Gets the query parameters from the URI. /// public IReadOnlyDictionary QueryParameters => - _queryParams ??= WebUtils.ParseQueryString(Uri, false) ?? new Dictionary(); + _queryParams ??= WebUtils.ParseQueryString(Uri, false) ?? new Dictionary(StringComparer.Ordinal); /// /// Gets or sets a value indicating whether the request has been handled. diff --git a/src/Controls/src/Core/HybridWebView/PlatformHybridWebViewWebResourceRequestedEventArgs.cs b/src/Controls/src/Core/HybridWebView/PlatformHybridWebViewWebResourceRequestedEventArgs.cs index a97c8bec64a4..e12e74a97795 100644 --- a/src/Controls/src/Core/HybridWebView/PlatformHybridWebViewWebResourceRequestedEventArgs.cs +++ b/src/Controls/src/Core/HybridWebView/PlatformHybridWebViewWebResourceRequestedEventArgs.cs @@ -11,6 +11,9 @@ namespace Microsoft.Maui.Controls; public class PlatformHybridWebViewWebResourceRequestedEventArgs { #if WINDOWS + + IReadOnlyDictionary? _headers; + internal PlatformHybridWebViewWebResourceRequestedEventArgs( global::Microsoft.Web.WebView2.Core.CoreWebView2 sender, global::Microsoft.Web.WebView2.Core.CoreWebView2WebResourceRequestedEventArgs eventArgs) @@ -53,50 +56,13 @@ internal PlatformHybridWebViewWebResourceRequestedEventArgs(WebResourceRequested internal string? GetRequestMethod() => Request.Method; - internal IReadOnlyDictionary? GetRequestHeaders() => new WrappedHeadersDictionary(Request.Headers); - - class WrappedHeadersDictionary : IReadOnlyDictionary - { - private global::Microsoft.Web.WebView2.Core.CoreWebView2HttpRequestHeaders _headers; - - public WrappedHeadersDictionary(global::Microsoft.Web.WebView2.Core.CoreWebView2HttpRequestHeaders headers) - { - _headers = headers; - } - - public string this[string key] => - _headers.Contains(key) - ? _headers.GetHeader(key) - : throw new KeyNotFoundException($"The key '{key}' was not found."); - - public IEnumerable Keys => - _headers.Select(header => header.Key); - - public IEnumerable Values => - _headers.Select(header => header.Value); - - public int Count => _headers.Count(); - - public bool ContainsKey(string key) => _headers.Contains(key); - - public bool TryGetValue(string key, out string value) - { - if (_headers.Contains(key)) - { - value = _headers.GetHeader(key); - return true; - } - value = string.Empty; - return false; - } - - public IEnumerator> GetEnumerator() => _headers.GetEnumerator(); - - IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); - } + internal IReadOnlyDictionary GetRequestHeaders() => + _headers ??= new Dictionary(Request.Headers, StringComparer.OrdinalIgnoreCase); #elif IOS || MACCATALYST + IReadOnlyDictionary? _headers; + internal PlatformHybridWebViewWebResourceRequestedEventArgs( global::WebKit.WKWebView sender, global::WebKit.IWKUrlSchemeTask urlSchemeTask) @@ -139,61 +105,30 @@ internal PlatformHybridWebViewWebResourceRequestedEventArgs(WebResourceRequested internal string? GetRequestMethod() => Request.HttpMethod; - internal IReadOnlyDictionary? GetRequestHeaders() => new WrappedHeadersDictionary(Request.Headers); + internal IReadOnlyDictionary GetRequestHeaders() => + _headers ??= CreateHeadersDictionary(); - class WrappedHeadersDictionary : IReadOnlyDictionary + Dictionary CreateHeadersDictionary() { - Foundation.NSDictionary _headers; - - public WrappedHeadersDictionary(Foundation.NSDictionary headers) - { - _headers = headers; - } - - public string this[string key] => - TryGetValue(key, out var value) - ? value - : throw new KeyNotFoundException($"The key '{key}' was not found."); - - public IEnumerable Keys => _headers.Keys.Select(k => k.ToString()); - - public IEnumerable Values => _headers.Values.Select(v => v.ToString()); - - public int Count => (int)_headers.Count; - - public bool ContainsKey(string key) + var headers = new Dictionary(StringComparer.OrdinalIgnoreCase); + if (Request.Headers is { } rh) { - using var nskey = new Foundation.NSString(key); - return _headers.ContainsKey(nskey); - } - - public bool TryGetValue(string key, out string value) - { - using var nsKey = new Foundation.NSString(key); - if (_headers.ContainsKey(nsKey)) + foreach (var key in rh.Keys) { - value = _headers[nsKey].ToString(); - return true; + if (key is Foundation.NSString keyString) + { + headers[keyString] = rh[keyString].ToString(); + } } - value = string.Empty; - return false; } - - public IEnumerator> GetEnumerator() - { - foreach (var pair in _headers) - { - yield return new KeyValuePair(pair.Key.ToString(), pair.Value.ToString()); - } - } - - IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); + return headers; } #elif ANDROID Action _setResponse; - private global::Android.Webkit.WebResourceResponse? _response; + global::Android.Webkit.WebResourceResponse? _response; + IReadOnlyDictionary? _headers; internal PlatformHybridWebViewWebResourceRequestedEventArgs( global::Android.Webkit.WebView sender, @@ -228,9 +163,9 @@ internal PlatformHybridWebViewWebResourceRequestedEventArgs(WebResourceRequested /// /// Gets or sets the native response to return to the web view. - /// + /// /// This property must be set to a valid response if the property is set to true. - /// + /// /// This is only available on Android. /// public global::Android.Webkit.WebResourceResponse? Response @@ -247,7 +182,10 @@ internal PlatformHybridWebViewWebResourceRequestedEventArgs(WebResourceRequested internal string? GetRequestMethod() => Request.Method; - internal IReadOnlyDictionary? GetRequestHeaders() => Request.RequestHeaders?.AsReadOnly(); + internal IReadOnlyDictionary GetRequestHeaders() => + _headers ??= Request.RequestHeaders is { } rh + ? new Dictionary(rh, StringComparer.OrdinalIgnoreCase) + : new Dictionary(StringComparer.OrdinalIgnoreCase); #else diff --git a/src/Controls/tests/DeviceTests/Elements/HybridWebView/HybridWebViewTests.cs b/src/Controls/tests/DeviceTests/Elements/HybridWebView/HybridWebViewTests.cs index afd960201b1b..90d9616750bb 100644 --- a/src/Controls/tests/DeviceTests/Elements/HybridWebView/HybridWebViewTests.cs +++ b/src/Controls/tests/DeviceTests/Elements/HybridWebView/HybridWebViewTests.cs @@ -5,6 +5,7 @@ using System.IO; using System.IO.Pipelines; using System.Runtime.CompilerServices; +using System.Text; using System.Text.Json; using System.Text.Json.Serialization; using System.Threading; @@ -400,82 +401,50 @@ public Task InvokeDotNet(string methodName, string expectedReturnValue) => [Theory] [InlineData("")] [InlineData("Async")] - public async Task InvokeJavaScriptMethodThatThrowsNumber(string type) - { -#if ANDROID - // NOTE: skip this test on older Android devices because it is not currently supported on these versions - if (!System.OperatingSystem.IsAndroidVersionAtLeast(24)) + public Task InvokeJavaScriptMethodThatThrowsNumber(string type) => + RunExceptionTest("EvaluateMeWithParamsThatThrows" + type, 1, ex => { - return; - } -#endif - - var ex = await RunExceptionTest("EvaluateMeWithParamsThatThrows" + type, 1); - Assert.Equal("InvokeJavaScript threw an exception: 777.777", ex.Message); - Assert.Equal("777.777", ex.InnerException.Message); - Assert.Null(ex.InnerException.Data["JavaScriptErrorName"]); - Assert.NotNull(ex.InnerException.StackTrace); - } + Assert.Equal("InvokeJavaScript threw an exception: 777.777", ex.Message); + Assert.Equal("777.777", ex.InnerException.Message); + Assert.Null(ex.InnerException.Data["JavaScriptErrorName"]); + Assert.NotNull(ex.InnerException.StackTrace); + }); [Theory] [InlineData("")] [InlineData("Async")] - public async Task InvokeJavaScriptMethodThatThrowsString(string type) - { -#if ANDROID - // NOTE: skip this test on older Android devices because it is not currently supported on these versions - if (!System.OperatingSystem.IsAndroidVersionAtLeast(24)) + public Task InvokeJavaScriptMethodThatThrowsString(string type) => + RunExceptionTest("EvaluateMeWithParamsThatThrows" + type, 2, ex => { - return; - } -#endif - - var ex = await RunExceptionTest("EvaluateMeWithParamsThatThrows" + type, 2); - Assert.Equal("InvokeJavaScript threw an exception: String: 777.777", ex.Message); - Assert.Equal("String: 777.777", ex.InnerException.Message); - Assert.Null(ex.InnerException.Data["JavaScriptErrorName"]); - Assert.NotNull(ex.InnerException.StackTrace); - } + Assert.Equal("InvokeJavaScript threw an exception: String: 777.777", ex.Message); + Assert.Equal("String: 777.777", ex.InnerException.Message); + Assert.Null(ex.InnerException.Data["JavaScriptErrorName"]); + Assert.NotNull(ex.InnerException.StackTrace); + }); [Theory] [InlineData("")] [InlineData("Async")] - public async Task InvokeJavaScriptMethodThatThrowsError(string type) - { -#if ANDROID - // NOTE: skip this test on older Android devices because it is not currently supported on these versions - if (!System.OperatingSystem.IsAndroidVersionAtLeast(24)) + public Task InvokeJavaScriptMethodThatThrowsError(string type) => + RunExceptionTest("EvaluateMeWithParamsThatThrows" + type, 3, ex => { - return; - } -#endif - - var ex = await RunExceptionTest("EvaluateMeWithParamsThatThrows" + type, 3); - Assert.Equal("InvokeJavaScript threw an exception: Generic Error: 777.777", ex.Message); - Assert.Equal("Generic Error: 777.777", ex.InnerException.Message); - Assert.Equal("Error", ex.InnerException.Data["JavaScriptErrorName"]); - Assert.NotNull(ex.InnerException.StackTrace); - } + Assert.Equal("InvokeJavaScript threw an exception: Generic Error: 777.777", ex.Message); + Assert.Equal("Generic Error: 777.777", ex.InnerException.Message); + Assert.Equal("Error", ex.InnerException.Data["JavaScriptErrorName"]); + Assert.NotNull(ex.InnerException.StackTrace); + }); [Theory] [InlineData("")] [InlineData("Async")] - public async Task InvokeJavaScriptMethodThatThrowsTypedNumber(string type) - { -#if ANDROID - // NOTE: skip this test on older Android devices because it is not currently supported on these versions - if (!System.OperatingSystem.IsAndroidVersionAtLeast(24)) + public Task InvokeJavaScriptMethodThatThrowsTypedNumber(string type) => + RunExceptionTest("EvaluateMeWithParamsThatThrows" + type, 4, ex => { - return; - } -#endif - - var ex = await RunExceptionTest("EvaluateMeWithParamsThatThrows" + type, 4); - Assert.Contains("undefined", ex.Message, StringComparison.OrdinalIgnoreCase); - Assert.Contains("undefined", ex.InnerException.Message, StringComparison.OrdinalIgnoreCase); - Assert.Equal("TypeError", ex.InnerException.Data["JavaScriptErrorName"]); - Assert.NotNull(ex.InnerException.StackTrace); - } + Assert.Contains("undefined", ex.Message, StringComparison.OrdinalIgnoreCase); + Assert.Contains("undefined", ex.InnerException.Message, StringComparison.OrdinalIgnoreCase); + Assert.Equal("TypeError", ex.InnerException.Data["JavaScriptErrorName"]); + Assert.NotNull(ex.InnerException.StackTrace); + }); [Fact] public Task RequestsCanBeInterceptedAndCustomDataReturned() => @@ -557,6 +526,12 @@ static async Task GetDataAsync(IReadOnlyDictionary query public Task RequestsCanBeInterceptedAndCustomDataReturnedForDifferentHosts(string uriBase, string function) => RunTest(async (hybridWebView) => { + // NOTE: skip this test on older Android devices because it is not currently supported on these versions + if (OperatingSystem.IsAndroid() && !OperatingSystem.IsAndroidVersionAtLeast(25)) + { + return; + } + hybridWebView.WebResourceRequested += (sender, e) => { if (new Uri(uriBase).IsBaseOf(e.Uri) && !e.Method.Equals("OPTIONS", StringComparison.OrdinalIgnoreCase)) @@ -607,11 +582,17 @@ public Task RequestsCanBeInterceptedAndCustomDataReturnedForDifferentHosts(strin public Task RequestsCanBeInterceptedAndHeadersAddedForDifferentHosts(string uriBase, string function) => RunTest(async (hybridWebView) => { + // NOTE: skip this test on older Android devices because it is not currently supported on these versions + if (OperatingSystem.IsAndroid() && !OperatingSystem.IsAndroidVersionAtLeast(25)) + { + return; + } + const string ExpectedHeaderValue = "My Header Value"; hybridWebView.WebResourceRequested += (sender, e) => { - if (new Uri(uriBase).IsBaseOf(e.Uri)) + if (new Uri(uriBase).IsBaseOf(e.Uri) && !e.Method.Equals("OPTIONS", StringComparison.OrdinalIgnoreCase)) { #if WINDOWS // Add the desired header for Windows by modifying the request @@ -747,38 +728,102 @@ await Assert.ThrowsAsync(() => HybridWebViewTestContext.Default.ResponseObject)); }); - async Task RunExceptionTest(string method, int errorType) - { - Exception exception = null; - await RunTest(async (hybridWebView) => + [Theory] +#if !ANDROID // Custom schemes are not supported on Android +#if !WINDOWS // TODO: There seems to be a bug with the implementation in the WASDK version of WebView2 + [InlineData("app://echoservice/", "RequestsWithCustomSchemeCanBeIntercepted")] +#endif +#endif +#if !IOS && !MACCATALYST // Cannot intercept https requests on iOS/MacCatalyst + [InlineData("https://echo.free.beeceptor.com/", "RequestsCanBeIntercepted")] +#endif + public Task RequestsCanBeInterceptedAndCaseInsensitiveHeadersRead(string uriBase, string function) => + RunTest(async (hybridWebView) => + { + // NOTE: skip this test on older Android devices because it is not currently supported on these versions + if (OperatingSystem.IsAndroid() && !OperatingSystem.IsAndroidVersionAtLeast(25)) + { + return; + } + + var headerValues = new Dictionary(); + + hybridWebView.WebResourceRequested += (sender, e) => + { + if (new Uri(uriBase).IsBaseOf(e.Uri) && !e.Method.Equals("OPTIONS", StringComparison.OrdinalIgnoreCase)) + { + // Should be exactly as set in the JS + try + { headerValues["X-Echo-Name"] = e.Headers["X-Echo-Name"]; } + catch (Exception ex) + { headerValues["X-Echo-Name"] = ex.Message; } + + // Sometimes lowercase is used + try + { headerValues["x-echo-name"] = e.Headers["x-echo-name"]; } + catch (Exception ex) + { headerValues["x-echo-name"] = ex.Message; } + + // This should never actually occur + try + { headerValues["X-ECHO-name"] = e.Headers["X-ECHO-name"]; } + catch (Exception ex) + { headerValues["X-ECHO-name"] = ex.Message; } + + // If the request is for the app:// resources, we return an empty response + // because the tests are not doing anything with the response. + if (e.Uri.Scheme == "app") + { + var headers = new Dictionary + { + ["Content-Type"] = "application/json", + ["Access-Control-Allow-Origin"] = "*", + ["Access-Control-Allow-Headers"] = "*", + ["Access-Control-Allow-Methods"] = "GET", + }; + e.SetResponse(200, "OK", headers, new MemoryStream(Encoding.UTF8.GetBytes("{}"))); + e.Handled = true; + } + } + }; + + var responseObject = await hybridWebView.InvokeJavaScriptAsync( + function, + HybridWebViewTestContext.Default.EchoResponseObject); + + Assert.NotEmpty(headerValues); + Assert.Equal("Matthew", headerValues["X-Echo-Name"]); + Assert.Equal("Matthew", headerValues["x-echo-name"]); + Assert.Equal("Matthew", headerValues["X-ECHO-name"]); + }); + + Task RunExceptionTest(string method, int errorType, Action test) => + RunTest(async (hybridWebView) => { var x = 123.456m; var y = 654.321m; - exception = await Assert.ThrowsAnyAsync(() => + var exception = await Assert.ThrowsAnyAsync(() => hybridWebView.InvokeJavaScriptAsync( method, HybridWebViewTestContext.Default.Decimal, [x, y, errorType], [HybridWebViewTestContext.Default.Decimal, HybridWebViewTestContext.Default.Decimal, HybridWebViewTestContext.Default.Int32])); - }); - return exception; - } + test(exception); + }); Task RunTest(Func test) => RunTest(null, test); async Task RunTest(string defaultFile, Func test) { -#if ANDROID // NOTE: skip this test on older Android devices because it is not currently supported on these versions - if (!System.OperatingSystem.IsAndroidVersionAtLeast(24)) + if (OperatingSystem.IsAndroid() && !OperatingSystem.IsAndroidVersionAtLeast(24)) { return; } -#endif SetupBuilder(); diff --git a/src/Core/src/Handlers/HybridWebView/HybridWebView.js b/src/Core/src/Handlers/HybridWebView/HybridWebView.js index 6cbce2c552db..e76a75b65ba3 100644 --- a/src/Core/src/Handlers/HybridWebView/HybridWebView.js +++ b/src/Core/src/Handlers/HybridWebView/HybridWebView.js @@ -18,13 +18,13 @@ window.dispatchEvent(event); } // Determine the mechanism to receive messages from the host application. - if (window.chrome?.webview?.addEventListener) { + if (window.chrome && window.chrome.webview && window.chrome.webview.addEventListener) { // Windows WebView2 window.chrome.webview.addEventListener('message', (arg) => { dispatchHybridWebViewMessage(arg.data); }); } - else if (window.webkit?.messageHandlers?.webwindowinterop) { + else if (window.webkit && window.webkit.messageHandlers && window.webkit.messageHandlers.webwindowinterop) { // iOS and MacCatalyst WKWebView // @ts-ignore - We are extending the global object here window.external = { @@ -40,11 +40,11 @@ }); } // Determine the function to use to send messages to the host application. - if (window.chrome?.webview) { + if (window.chrome && window.chrome.webview) { // Windows WebView2 sendMessageFunction = msg => window.chrome.webview.postMessage(msg); } - else if (window.webkit?.messageHandlers?.webwindowinterop) { + else if (window.webkit && window.webkit.messageHandlers && window.webkit.messageHandlers.webwindowinterop) { // iOS and MacCatalyst WKWebView sendMessageFunction = msg => window.webkit.messageHandlers.webwindowinterop.postMessage(msg); } @@ -108,78 +108,82 @@ const json = JSON.stringify(errorObj); sendMessageToDotNet('__InvokeJavaScriptFailed', taskId + '|' + json); } - const HybridWebView = { - /* - * Send a raw message to the .NET host application. - * The message is sent directly and not processed or serialized. - * - * @param message The message to send to the .NET host application. - */ - SendRawMessage: function (message) { - sendMessageToDotNet('__RawMessage', message); - }, - /* - * Invoke a .NET method on the InvokeJavaScriptTarget instance. - * The method name and parameters are serialized and sent to the .NET host application. - * - * @param methodName The name of the .NET method to invoke. - * @param paramValues The parameters to pass to the .NET method. If the method takes no parameters, this can be omitted. - * - * @returns A promise that resolves with the result of the .NET method invocation. - */ - InvokeDotNet: async function (methodName, paramValues) { - const body = { - MethodName: methodName - }; - // if parameters were provided, serialize them first - if (paramValues !== undefined) { - if (!Array.isArray(paramValues)) { - paramValues = [paramValues]; - } - for (let i = 0; i < paramValues.length; i++) { - paramValues[i] = JSON.stringify(paramValues[i]); - } - if (paramValues.length > 0) { - body.ParamValues = paramValues; - } - } - const message = JSON.stringify(body); - const requestUrl = `${window.location.origin}/__hwvInvokeDotNet?data=${encodeURIComponent(message)}`; - const rawResponse = await fetch(requestUrl, { - method: 'GET', - headers: { - 'Accept': 'application/json' - } - }); - const response = await rawResponse.json(); - if (!response) { - return null; + /* + * Send a raw message to the .NET host application. + * The message is sent directly and not processed or serialized. + * + * @param message The message to send to the .NET host application. + */ + function sendRawMessage(message) { + sendMessageToDotNet('__RawMessage', message); + } + /* + * Invoke a .NET method on the InvokeJavaScriptTarget instance. + * The method name and parameters are serialized and sent to the .NET host application. + * + * @param methodName The name of the .NET method to invoke. + * @param paramValues The parameters to pass to the .NET method. If the method takes no parameters, this can be omitted. + * + * @returns A promise that resolves with the result of the .NET method invocation. + */ + async function invokeDotNet(methodName, paramValues) { + const body = { + MethodName: methodName + }; + // if parameters were provided, serialize them first + if (paramValues !== undefined) { + if (!Array.isArray(paramValues)) { + paramValues = [paramValues]; } - if (response.IsJson) { - return JSON.parse(response.Result); + for (let i = 0; i < paramValues.length; i++) { + paramValues[i] = JSON.stringify(paramValues[i]); } - return response.Result; - }, - /* - * Invoke a JavaScript method from the .NET host application. - * This method is called from the HybridWebViewHandler and is not intended to be used by user applications. - * - * @param taskId The task ID that was provided by the .NET host application. - * @param methodName The JavaScript method to invoke in the global scope. - * @param args The arguments to pass to the JavaScript method. - * - * @returns A promise. - */ - __InvokeJavaScript: async function (taskId, methodName, args) { - try { - const result = await methodName(...args); - invokeJavaScriptCallbackInDotNet(taskId, result); + if (paramValues.length > 0) { + body.ParamValues = paramValues; } - catch (ex) { - console.error(ex); - invokeJavaScriptFailedInDotNet(taskId, ex); + } + const message = JSON.stringify(body); + const requestUrl = `${window.location.origin}/__hwvInvokeDotNet?data=${encodeURIComponent(message)}`; + const rawResponse = await fetch(requestUrl, { + method: 'GET', + headers: { + 'Accept': 'application/json' } + }); + const response = await rawResponse.json(); + if (!response) { + return null; + } + if (response.IsJson) { + return JSON.parse(response.Result); } + return response.Result; + } + /* + * Invoke a JavaScript method from the .NET host application. + * This method is called from the HybridWebViewHandler and is not intended to be used by user applications. + * + * @param taskId The task ID that was provided by the .NET host application. + * @param methodName The JavaScript method to invoke in the global scope. + * @param args The arguments to pass to the JavaScript method. + * + * @returns A promise. + */ + async function invokeJavaScript(taskId, methodName, args) { + try { + const result = await methodName(...args); + invokeJavaScriptCallbackInDotNet(taskId, result); + } + catch (ex) { + console.error(ex); + invokeJavaScriptFailedInDotNet(taskId, ex); + } + } + // Define the public API of the HybridWebView control. + const HybridWebView = { + SendRawMessage: sendRawMessage, + InvokeDotNet: invokeDotNet, + __InvokeJavaScript: invokeJavaScript }; // Make the following APIs available in global scope for invocation from JS // @ts-ignore - We are extending the global object here diff --git a/src/Core/src/Handlers/HybridWebView/HybridWebView.ts b/src/Core/src/Handlers/HybridWebView/HybridWebView.ts index f1b77bd72439..f09b3c991831 100644 --- a/src/Core/src/Handlers/HybridWebView/HybridWebView.ts +++ b/src/Core/src/Handlers/HybridWebView/HybridWebView.ts @@ -68,12 +68,12 @@ interface DotNetInvokeResult { } // Determine the mechanism to receive messages from the host application. - if (window.chrome?.webview?.addEventListener) { + if (window.chrome && window.chrome.webview && window.chrome.webview.addEventListener) { // Windows WebView2 window.chrome.webview.addEventListener('message', (arg: any) => { dispatchHybridWebViewMessage(arg.data); }); - } else if (window.webkit?.messageHandlers?.webwindowinterop) { + } else if (window.webkit && window.webkit.messageHandlers && window.webkit.messageHandlers.webwindowinterop) { // iOS and MacCatalyst WKWebView // @ts-ignore - We are extending the global object here window.external = { @@ -89,10 +89,10 @@ interface DotNetInvokeResult { } // Determine the function to use to send messages to the host application. - if (window.chrome?.webview) { + if (window.chrome && window.chrome.webview) { // Windows WebView2 sendMessageFunction = msg => window.chrome.webview.postMessage(msg); - } else if (window.webkit?.messageHandlers?.webwindowinterop) { + } else if (window.webkit && window.webkit.messageHandlers && window.webkit.messageHandlers.webwindowinterop) { // iOS and MacCatalyst WKWebView sendMessageFunction = msg => window.webkit.messageHandlers.webwindowinterop.postMessage(msg); } else if (window.hybridWebViewHost) { @@ -160,89 +160,93 @@ interface DotNetInvokeResult { sendMessageToDotNet('__InvokeJavaScriptFailed', taskId + '|' + json); } - const HybridWebView = { - - /* - * Send a raw message to the .NET host application. - * The message is sent directly and not processed or serialized. - * - * @param message The message to send to the .NET host application. - */ - SendRawMessage: function (message: string) { - sendMessageToDotNet('__RawMessage', message); - }, - - /* - * Invoke a .NET method on the InvokeJavaScriptTarget instance. - * The method name and parameters are serialized and sent to the .NET host application. - * - * @param methodName The name of the .NET method to invoke. - * @param paramValues The parameters to pass to the .NET method. If the method takes no parameters, this can be omitted. - * - * @returns A promise that resolves with the result of the .NET method invocation. - */ - InvokeDotNet: async function (methodName: string, paramValues?: any) { - const body: JSInvokeMethodData = { - MethodName: methodName - }; + /* + * Send a raw message to the .NET host application. + * The message is sent directly and not processed or serialized. + * + * @param message The message to send to the .NET host application. + */ + function sendRawMessage(message: string) { + sendMessageToDotNet('__RawMessage', message); + } - // if parameters were provided, serialize them first - if (paramValues !== undefined) { - if (!Array.isArray(paramValues)) { - paramValues = [paramValues]; - } + /* + * Invoke a .NET method on the InvokeJavaScriptTarget instance. + * The method name and parameters are serialized and sent to the .NET host application. + * + * @param methodName The name of the .NET method to invoke. + * @param paramValues The parameters to pass to the .NET method. If the method takes no parameters, this can be omitted. + * + * @returns A promise that resolves with the result of the .NET method invocation. + */ + async function invokeDotNet(methodName: string, paramValues?: any) { + const body: JSInvokeMethodData = { + MethodName: methodName + }; - for (let i = 0; i < paramValues.length; i++) { - paramValues[i] = JSON.stringify(paramValues[i]); - } + // if parameters were provided, serialize them first + if (paramValues !== undefined) { + if (!Array.isArray(paramValues)) { + paramValues = [paramValues]; + } - if (paramValues.length > 0) { - body.ParamValues = paramValues; - } + for (let i = 0; i < paramValues.length; i++) { + paramValues[i] = JSON.stringify(paramValues[i]); } - const message = JSON.stringify(body); + if (paramValues.length > 0) { + body.ParamValues = paramValues; + } + } - const requestUrl = `${window.location.origin}/__hwvInvokeDotNet?data=${encodeURIComponent(message)}`; + const message = JSON.stringify(body); - const rawResponse = await fetch(requestUrl, { - method: 'GET', - headers: { - 'Accept': 'application/json' - } - }); - const response: DotNetInvokeResult = await rawResponse.json(); + const requestUrl = `${window.location.origin}/__hwvInvokeDotNet?data=${encodeURIComponent(message)}`; - if (!response) { - return null; + const rawResponse = await fetch(requestUrl, { + method: 'GET', + headers: { + 'Accept': 'application/json' } + }); + const response: DotNetInvokeResult = await rawResponse.json(); - if (response.IsJson) { - return JSON.parse(response.Result); - } + if (!response) { + return null; + } - return response.Result; - }, - - /* - * Invoke a JavaScript method from the .NET host application. - * This method is called from the HybridWebViewHandler and is not intended to be used by user applications. - * - * @param taskId The task ID that was provided by the .NET host application. - * @param methodName The JavaScript method to invoke in the global scope. - * @param args The arguments to pass to the JavaScript method. - * - * @returns A promise. - */ - __InvokeJavaScript: async function (taskId: string, methodName: Function, args: any[]) { - try { - const result = await methodName(...args); - invokeJavaScriptCallbackInDotNet(taskId, result); - } catch (ex) { - console.error(ex); - invokeJavaScriptFailedInDotNet(taskId, ex); - } + if (response.IsJson) { + return JSON.parse(response.Result); + } + + return response.Result; + } + + /* + * Invoke a JavaScript method from the .NET host application. + * This method is called from the HybridWebViewHandler and is not intended to be used by user applications. + * + * @param taskId The task ID that was provided by the .NET host application. + * @param methodName The JavaScript method to invoke in the global scope. + * @param args The arguments to pass to the JavaScript method. + * + * @returns A promise. + */ + async function invokeJavaScript(taskId: string, methodName: Function, args: any[]) { + try { + const result = await methodName(...args); + invokeJavaScriptCallbackInDotNet(taskId, result); + } catch (ex) { + console.error(ex); + invokeJavaScriptFailedInDotNet(taskId, ex); } + } + + // Define the public API of the HybridWebView control. + const HybridWebView = { + SendRawMessage: sendRawMessage, + InvokeDotNet: invokeDotNet, + __InvokeJavaScript: invokeJavaScript }; // Make the following APIs available in global scope for invocation from JS