diff --git a/src/Controls/tests/DeviceTests/Elements/HybridWebView/HybridWebViewTestsBase.cs b/src/Controls/tests/DeviceTests/Elements/HybridWebView/HybridWebViewTestsBase.cs index 68522216d53e..f5c3e3609b37 100644 --- a/src/Controls/tests/DeviceTests/Elements/HybridWebView/HybridWebViewTestsBase.cs +++ b/src/Controls/tests/DeviceTests/Elements/HybridWebView/HybridWebViewTestsBase.cs @@ -40,7 +40,6 @@ protected async Task RunTest(string? defaultFile, Func test HybridRoot = "HybridTestRoot", DefaultFile = defaultFile ?? "index.html", }; - await RunTest(hybridWebView, (handler, view) => test(view)); } @@ -59,12 +58,7 @@ await AttachAndRun(hybridWebView, async handler => { await WebViewHelpers.WaitForHybridWebViewLoaded(hybridWebView); - // Use a cancellation token with a timeout - using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(15)); - - var testWrapper = test((HybridWebViewHandler)handler, hybridWebView); - - await testWrapper.WaitAsync(cts.Token); + await test((HybridWebViewHandler)handler, hybridWebView); }); } diff --git a/src/Controls/tests/DeviceTests/Elements/HybridWebView/HybridWebViewTests_EvaluateJavaScriptAsync.cs b/src/Controls/tests/DeviceTests/Elements/HybridWebView/HybridWebViewTests_EvaluateJavaScriptAsync.cs index 9b6332be89e6..fcd6b52346cf 100644 --- a/src/Controls/tests/DeviceTests/Elements/HybridWebView/HybridWebViewTests_EvaluateJavaScriptAsync.cs +++ b/src/Controls/tests/DeviceTests/Elements/HybridWebView/HybridWebViewTests_EvaluateJavaScriptAsync.cs @@ -11,56 +11,15 @@ namespace Microsoft.Maui.DeviceTests; public partial class HybridWebViewTests_EvaluateJavaScriptAsync : HybridWebViewTestsBase { [Fact] - public Task EvaluateJavaScriptAsync_WithStringParameters() => + public Task EvaluateJavaScriptAndGetResult() => RunTest(async (hybridWebView) => { // Run some JavaScript to call a method and get result var result1 = await hybridWebView.EvaluateJavaScriptAsync("EvaluateMeWithParamsAndReturn('abc', 'def')"); Assert.Equal("abcdef", result1); - }); - - [Fact] - public Task EvaluateJavaScriptAsync_WithNumberParameters() => - RunTest(async (hybridWebView) => - { - // Run some JavaScript to call a method and get result - var result1 = await hybridWebView.EvaluateJavaScriptAsync("EvaluateMeWithParamsAndReturn(1, 2)"); - Assert.Equal("3", result1); - }); - [Fact] - public Task EvaluateJavaScriptAsync_GetsProperty() => - RunTest(async (hybridWebView) => - { // Run some JavaScript to get an arbitrary result by running JavaScript var result2 = await hybridWebView.EvaluateJavaScriptAsync("window.TestKey"); Assert.Equal("test_value", result2); }); - - [Fact] - public Task EvaluateJavaScriptAsync_HandlesDoubleQuotes() => - RunTest(async (hybridWebView) => - { - // Run some JavaScript to call a method and get result - var result1 = await hybridWebView.EvaluateJavaScriptAsync("EvaluateMeWithParamsAndReturn('\"Hel', 'lo!\"')"); - Assert.Equal("\"Hello!\"", result1); - }); - - [Fact] - public Task EvaluateJavaScriptAsync_HandlesSingleQuotes() => - RunTest(async (hybridWebView) => - { - // Run some JavaScript to call a method and get result - var result1 = await hybridWebView.EvaluateJavaScriptAsync("EvaluateMeWithParamsAndReturn('\\'Hel', 'lo!\\'')"); - Assert.Equal("'Hello!'", result1); - }); - - [Fact] - public Task EvaluateJavaScriptAsync_HandlesDoubleAndSingleQuotes() => - RunTest(async (hybridWebView) => - { - // Run some JavaScript to call a method and get result - var result1 = await hybridWebView.EvaluateJavaScriptAsync("EvaluateMeWithParamsAndReturn('\"Hel', 'lo!\\'')"); - Assert.Equal("\"Hello!'", result1); - }); } diff --git a/src/Controls/tests/DeviceTests/Elements/HybridWebView/HybridWebViewTests_InvokeJavaScriptAsync.cs b/src/Controls/tests/DeviceTests/Elements/HybridWebView/HybridWebViewTests_InvokeJavaScriptAsync.cs index 95fe209dd847..1d0b36a7faad 100644 --- a/src/Controls/tests/DeviceTests/Elements/HybridWebView/HybridWebViewTests_InvokeJavaScriptAsync.cs +++ b/src/Controls/tests/DeviceTests/Elements/HybridWebView/HybridWebViewTests_InvokeJavaScriptAsync.cs @@ -1,8 +1,6 @@ #nullable enable using System; using System.Collections.Generic; -using System.Text; -using System.Text.Json; using System.Text.Json.Serialization; using System.Threading.Tasks; using Xunit; @@ -31,7 +29,7 @@ public Task RequestFileFromJS(string url, int expectedStatus) => }); [Fact] - public Task InvokeJavaScript_WithParametersAndNulls_AndComplexResult() => + public Task InvokeJavaScriptMethodWithParametersAndNullsAndComplexResult() => RunTest(async (hybridWebView) => { var x = 123.456m; @@ -49,7 +47,7 @@ public Task InvokeJavaScript_WithParametersAndNulls_AndComplexResult() => }); [Fact] - public Task InvokeJavaScript_WithParameters_AndDecimalResult() => + public Task InvokeJavaScriptMethodWithParametersAndDecimalResult() => RunTest(async (hybridWebView) => { var x = 123.456m; @@ -68,7 +66,7 @@ public Task InvokeJavaScript_WithParameters_AndDecimalResult() => [InlineData(-123.456)] [InlineData(0.0)] [InlineData(123.456)] - public Task InvokeJavaScript_WithParameters_AndDoubleResult(double expected) => + public Task InvokeJavaScriptMethodWithParametersAndDoubleResult(double expected) => RunTest(async (hybridWebView) => { var result = await hybridWebView.InvokeJavaScriptAsync( @@ -85,7 +83,7 @@ public Task InvokeJavaScript_WithParameters_AndDoubleResult(double expected) => [InlineData(-123.456)] [InlineData(0.0)] [InlineData(123.456)] - public Task InvokeJavaScript_WithParameters_AndNullableDoubleResult(double? expected) => + public Task InvokeJavaScriptMethodWithParametersAndNullableDoubleResult(double? expected) => RunTest(async (hybridWebView) => { var result = await hybridWebView.InvokeJavaScriptAsync( @@ -98,7 +96,7 @@ public Task InvokeJavaScript_WithParameters_AndNullableDoubleResult(double? expe }); [Fact] - public Task InvokeJavaScript_WithParameters_AndNewDoubleResult() => + public Task InvokeJavaScriptMethodWithParametersAndNewDoubleResult() => RunTest(async (hybridWebView) => { var x = 123.456m; @@ -117,7 +115,7 @@ public Task InvokeJavaScript_WithParameters_AndNewDoubleResult() => [InlineData(-123)] [InlineData(0)] [InlineData(123)] - public Task InvokeJavaScript_WithParameters_AndIntResult(int expected) => + public Task InvokeJavaScriptMethodWithParametersAndIntResult(int expected) => RunTest(async (hybridWebView) => { var result = await hybridWebView.InvokeJavaScriptAsync( @@ -134,7 +132,7 @@ public Task InvokeJavaScript_WithParameters_AndIntResult(int expected) => [InlineData(-123)] [InlineData(0)] [InlineData(123)] - public Task InvokeJavaScript_WithParameters_AndNullableIntResult(int? expected) => + public Task InvokeJavaScriptMethodWithParametersAndNullableIntResult(int? expected) => RunTest(async (hybridWebView) => { var result = await hybridWebView.InvokeJavaScriptAsync( @@ -147,7 +145,7 @@ public Task InvokeJavaScript_WithParameters_AndNullableIntResult(int? expected) }); [Fact] - public Task InvokeJavaScript_WithParameters_AndNewIntResult() => + public Task InvokeJavaScriptMethodWithParametersAndNewIntResult() => RunTest(async (hybridWebView) => { var x = 123; @@ -168,7 +166,7 @@ public Task InvokeJavaScript_WithParameters_AndNewIntResult() => [InlineData("foo")] [InlineData("null")] [InlineData("undefined")] - public Task InvokeJavaScript_WithParameters_AndStringResult(string? expected) => + public Task InvokeJavaScriptMethodWithParametersAndStringResult(string? expected) => RunTest(async (hybridWebView) => { var result = await hybridWebView.InvokeJavaScriptAsync( @@ -181,7 +179,7 @@ public Task InvokeJavaScript_WithParameters_AndStringResult(string? expected) => }); [Fact] - public Task InvokeJavaScript_WithParameters_AndNewStringResult() => + public Task InvokeJavaScriptMethodWithParametersAndNewStringResult() => RunTest(async (hybridWebView) => { var x = "abc"; @@ -199,7 +197,7 @@ public Task InvokeJavaScript_WithParameters_AndNewStringResult() => [Theory] [InlineData(true)] [InlineData(false)] - public Task InvokeJavaScript_WithParameters_AndBoolResult(bool expected) => + public Task InvokeJavaScriptMethodWithParametersAndBoolResult(bool expected) => RunTest(async (hybridWebView) => { var result = await hybridWebView.InvokeJavaScriptAsync( @@ -212,7 +210,7 @@ public Task InvokeJavaScript_WithParameters_AndBoolResult(bool expected) => }); [Fact] - public Task InvokeJavaScript_WithParameters_AndComplexResult() => + public Task InvokeJavaScriptMethodWithParametersAndComplexResult() => RunTest(async (hybridWebView) => { var x = 123.456m; @@ -230,7 +228,7 @@ public Task InvokeJavaScript_WithParameters_AndComplexResult() => }); [Fact] - public Task InvokeJavaScript_WithParameters_AndAsyncComplexResult() => + public Task InvokeAsyncJavaScriptMethodWithParametersAndComplexResult() => RunTest(async (hybridWebView) => { var s1 = "new_key"; @@ -250,7 +248,7 @@ public Task InvokeJavaScript_WithParameters_AndAsyncComplexResult() => }); [Fact] - public Task InvokeJavaScript_WithParameters_AndVoidReturn() => + public Task InvokeJavaScriptMethodWithParametersAndVoidReturn() => RunTest(async (hybridWebView) => { var x = 123.456m; @@ -269,7 +267,7 @@ await hybridWebView.InvokeJavaScriptAsync( }); [Fact] - public Task InvokeJavaScript_WithParameters_AndVoidReturn_UsingObjectReturnMethod() => + public Task InvokeJavaScriptMethodWithParametersAndVoidReturnUsingObjectReturnMethod() => RunTest(async (hybridWebView) => { var x = 123.456m; @@ -291,7 +289,7 @@ public Task InvokeJavaScript_WithParameters_AndVoidReturn_UsingObjectReturnMetho }); [Fact] - public Task InvokeJavaScript_WithParameters_AndVoidReturn_UsingNullReturnMethod() => + public Task InvokeJavaScriptMethodWithParametersAndVoidReturnUsingNullReturnMethod() => RunTest(async (hybridWebView) => { var x = 123.456m; @@ -315,10 +313,10 @@ public Task InvokeJavaScript_WithParameters_AndVoidReturn_UsingNullReturnMethod( [Theory] [InlineData("")] [InlineData("Async")] - public Task InvokeJavaScript_ThatThrowsNumber(string type) => + public Task InvokeJavaScriptMethodThatThrowsNumber(string type) => RunExceptionTest("EvaluateMeWithParamsThatThrows" + type, 1, ex => { - Assert.Equal("InvokeJavaScriptAsync threw an exception: 777.777", ex.Message); + 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); @@ -327,10 +325,10 @@ public Task InvokeJavaScript_ThatThrowsNumber(string type) => [Theory] [InlineData("")] [InlineData("Async")] - public Task InvokeJavaScript_ThatThrowsString(string type) => + public Task InvokeJavaScriptMethodThatThrowsString(string type) => RunExceptionTest("EvaluateMeWithParamsThatThrows" + type, 2, ex => { - Assert.Equal("InvokeJavaScriptAsync threw an exception: String: 777.777", ex.Message); + 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); @@ -339,10 +337,10 @@ public Task InvokeJavaScript_ThatThrowsString(string type) => [Theory] [InlineData("")] [InlineData("Async")] - public Task InvokeJavaScript_ThatThrowsError(string type) => + public Task InvokeJavaScriptMethodThatThrowsError(string type) => RunExceptionTest("EvaluateMeWithParamsThatThrows" + type, 3, ex => { - Assert.Equal("InvokeJavaScriptAsync threw an exception: Generic Error: 777.777", ex.Message); + 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); @@ -351,7 +349,7 @@ public Task InvokeJavaScript_ThatThrowsError(string type) => [Theory] [InlineData("")] [InlineData("Async")] - public Task InvokeJavaScript_ThatThrowsTypedNumber(string type) => + public Task InvokeJavaScriptMethodThatThrowsTypedNumber(string type) => RunExceptionTest("EvaluateMeWithParamsThatThrows" + type, 4, ex => { Assert.Contains("undefined", ex.Message, StringComparison.OrdinalIgnoreCase); @@ -360,163 +358,6 @@ public Task InvokeJavaScript_ThatThrowsTypedNumber(string type) => Assert.NotNull(ex.InnerException?.StackTrace); }); - [Fact] - public Task InvokeJavaScript_WithJsonStringArgument() => - RunTest(async (hybridWebView) => - { - // Create a dictionary that will be serialized to JSON - var contextArg = new Dictionary - { - { "userId", "userIdValue" }, - { "sessionId", "session123" }, - { "timestamp", "2025-11-11T01:30:00Z" } - }; - - // Serialize to JSON string (without base64 encoding) - string contextArgString = JsonSerializer.Serialize(contextArg); - - // This should not timeout - the JSON string should be handled correctly - var result = await hybridWebView.InvokeJavaScriptAsync( - "EchoJsonParameter", - InvokeJsonContext.Default.String, - [contextArgString], - [InvokeJsonContext.Default.String]); - - // Verify the result matches the input - Assert.Equal(contextArgString, result); - }); - - [Fact] - public Task InvokeJavaScript_WithDictionaryArgument() => - RunTest(async (hybridWebView) => - { - // Create a dictionary that will be serialized to JSON - var contextArg = new Dictionary - { - { "userId", "userIdValue" }, - { "sessionId", "session123" }, - { "timestamp", "2025-11-11T01:30:00Z" } - }; - - // This should not timeout - the JSON string should be handled correctly - var result = await hybridWebView.InvokeJavaScriptAsync( - "EchoJsonStringifyParameter", - InvokeJsonContext.Default.String, - [contextArg], - [InvokeJsonContext.Default.DictionaryStringString]); - - // Serialize to JSON string (without base64 encoding) - string contextArgString = JsonSerializer.Serialize(contextArg); - - // Verify the result matches the input - Assert.Equal(contextArgString, result); - }); - - [Fact] - public Task InvokeJavaScript_WithComplexJsonString() => - RunTest(async (hybridWebView) => - { - // Create a more complex JSON with special characters that might cause escaping issues - var complexObject = new Dictionary - { - { "string", "value with \"quotes\" and 'apostrophes'" }, - { "number", 123.456 }, - { "boolean", true }, - { "nested", new Dictionary - { - { "key1", "value1" }, - { "key2", "value2" } - } - } - }; - - string jsonString = JsonSerializer.Serialize(complexObject); - - var result = await hybridWebView.InvokeJavaScriptAsync( - "ParseAndStringifyJson", - InvokeJsonContext.Default.String, - [jsonString], - [InvokeJsonContext.Default.String]); - - // The JavaScript function should parse and re-stringify the JSON - // The result should be equivalent (though formatting might differ) - Assert.NotNull(result); - Assert.Contains("quotes", result, StringComparison.Ordinal); - Assert.Contains("apostrophes", result, StringComparison.Ordinal); - }); - - [Fact] - public Task InvokeJavaScript_WithMultipleJsonStringArguments() => - RunTest(async (hybridWebView) => - { - var firstJson = JsonSerializer.Serialize(new { type = "user", id = 1 }); - var secondJson = JsonSerializer.Serialize(new { type = "session", id = 2 }); - - var result = await hybridWebView.InvokeJavaScriptAsync( - "ConcatenateJsonStrings", - InvokeJsonContext.Default.String, - [firstJson, secondJson], - [InvokeJsonContext.Default.String, InvokeJsonContext.Default.String]); - - Assert.NotNull(result); - Assert.Contains("user", result, StringComparison.Ordinal); - Assert.Contains("session", result, StringComparison.Ordinal); - }); - - [Fact] - public Task InvokeJavaScript_WithBase64EncodedJsonString() => - RunTest(async (hybridWebView) => - { - var contextArg = new Dictionary - { - { "userId", "userIdValue" } - }; - - string contextArgString = JsonSerializer.Serialize(contextArg); - - // Base64 encode (the workaround from the issue) - string base64String = Convert.ToBase64String(Encoding.UTF8.GetBytes(contextArgString)); - - var result = await hybridWebView.InvokeJavaScriptAsync( - "DecodeBase64AndEcho", - InvokeJsonContext.Default.String, - [base64String], - [InvokeJsonContext.Default.String]); - - // The JavaScript function should decode and return the original JSON - Assert.Equal(contextArgString, result); - }); - - [Fact] - public Task InvokeJavaScript_WithJsonArrayArgument() => - RunTest(async (hybridWebView) => - { - var jsonArray = JsonSerializer.Serialize(new[] { "item1", "item2", "item3" }); - - var result = await hybridWebView.InvokeJavaScriptAsync( - "CountJsonArrayItems", - InvokeJsonContext.Default.Int32, - [jsonArray], - [InvokeJsonContext.Default.String]); - - Assert.Equal(3, result); - }); - - [Fact] - public Task InvokeJavaScript_WithEmptyJsonObject() => - RunTest(async (hybridWebView) => - { - var emptyJson = JsonSerializer.Serialize(new Dictionary()); - - var result = await hybridWebView.InvokeJavaScriptAsync( - "EchoJsonParameter", - InvokeJsonContext.Default.String, - [emptyJson], - [InvokeJsonContext.Default.String]); - - Assert.Equal("{}", result); - }); - Task RunExceptionTest(string method, int errorType, Action test) => RunTest(async (hybridWebView) => { diff --git a/src/Controls/tests/DeviceTests/Elements/HybridWebView/HybridWebViewTests_SetInvokeJavaScriptTarget.cs b/src/Controls/tests/DeviceTests/Elements/HybridWebView/HybridWebViewTests_SetInvokeJavaScriptTarget.cs index 2a9439ed743f..a904e3ccf467 100644 --- a/src/Controls/tests/DeviceTests/Elements/HybridWebView/HybridWebViewTests_SetInvokeJavaScriptTarget.cs +++ b/src/Controls/tests/DeviceTests/Elements/HybridWebView/HybridWebViewTests_SetInvokeJavaScriptTarget.cs @@ -139,8 +139,8 @@ private class InvokeJavaScriptAsyncTestData : IEnumerable { public IEnumerator GetEnumerator() { - const string ComplexResult = "{\"result\":123,\"operationName\":\"Test\"}"; - const string DictionaryResult = "{\"first\":111,\"second\":222,\"third\":333}"; + const string ComplexResult = "{\\\"result\\\":123,\\\"operationName\\\":\\\"Test\\\"}"; + const string DictionaryResult = "{\\\"first\\\":111,\\\"second\\\":222,\\\"third\\\":333}"; const int ValueTypeResult = 2; // Test variations of: diff --git a/src/Controls/tests/DeviceTests/Resources/Raw/HybridTestRoot/index.html b/src/Controls/tests/DeviceTests/Resources/Raw/HybridTestRoot/index.html index 49709cfa07da..bd5ee137d8d4 100644 --- a/src/Controls/tests/DeviceTests/Resources/Raw/HybridTestRoot/index.html +++ b/src/Controls/tests/DeviceTests/Resources/Raw/HybridTestRoot/index.html @@ -158,63 +158,6 @@ // test evaluate arbitrary javascript window.TestKey = 'test_value'; - // Test functions for JSON string arguments (issue 32438) - - // Echo back the JSON parameter as-is - function EchoJsonParameter(jsonString) { - return jsonString; - } - - // Echo back the parameter as a JSON string - function EchoJsonStringifyParameter(jsonString) { - return JSON.stringify(jsonString); - } - - - // Parse JSON string and stringify it back - function ParseAndStringifyJson(jsonString) { - try { - const parsed = JSON.parse(jsonString); - return JSON.stringify(parsed); - } catch (e) { - throw new Error(`Failed to parse JSON: ${e.message}`); - } - } - - // Concatenate two JSON strings into an array - function ConcatenateJsonStrings(json1, json2) { - try { - const obj1 = JSON.parse(json1); - const obj2 = JSON.parse(json2); - return JSON.stringify([obj1, obj2]); - } catch (e) { - throw new Error(`Failed to concatenate JSON strings: ${e.message}`); - } - } - - // Decode base64 string and echo back the decoded JSON - function DecodeBase64AndEcho(base64String) { - try { - const decoded = atob(base64String); - return decoded; - } catch (e) { - throw new Error(`Failed to decode base64: ${e.message}`); - } - } - - // Count items in a JSON array - function CountJsonArrayItems(jsonArrayString) { - try { - const array = JSON.parse(jsonArrayString); - if (!Array.isArray(array)) { - throw new Error('Argument is not a JSON array'); - } - return array.length; - } catch (e) { - throw new Error(`Failed to count array items: ${e.message}`); - } - } - diff --git a/src/Core/src/Handlers/HybridWebView/HybridWebViewHandler.Standard.cs b/src/Core/src/Handlers/HybridWebView/HybridWebViewHandler.Standard.cs index 862117e34d24..876cb0f223f1 100644 --- a/src/Core/src/Handlers/HybridWebView/HybridWebViewHandler.Standard.cs +++ b/src/Core/src/Handlers/HybridWebView/HybridWebViewHandler.Standard.cs @@ -8,8 +8,6 @@ public partial class HybridWebViewHandler : ViewHandler public static void MapEvaluateJavaScriptAsync(IHybridWebViewHandler handler, IHybridWebView hybridWebView, object? arg) { } - public static void MapInvokeJavaScriptAsync(IHybridWebViewHandler handler, IHybridWebView hybridWebView, object? arg) { } - public static void MapSendRawMessage(IHybridWebViewHandler handler, IHybridWebView hybridWebView, object? arg) { } } } \ No newline at end of file diff --git a/src/Core/src/Handlers/HybridWebView/HybridWebViewHandler.Tizen.cs b/src/Core/src/Handlers/HybridWebView/HybridWebViewHandler.Tizen.cs index 14330d546594..c686585c373e 100644 --- a/src/Core/src/Handlers/HybridWebView/HybridWebViewHandler.Tizen.cs +++ b/src/Core/src/Handlers/HybridWebView/HybridWebViewHandler.Tizen.cs @@ -8,8 +8,6 @@ public partial class HybridWebViewHandler : ViewHandler !AppContext.TryGetSwitch(InvokeJavaScriptThrowsExceptionsSwitch, out var enabled) || enabled; -#if PLATFORM && !TIZEN - void MessageReceived(string rawMessage) => - HybridWebViewHelper.ProcessRawMessage(this, VirtualView, rawMessage); + void MessageReceived(string rawMessage) + { + if (string.IsNullOrEmpty(rawMessage)) + { + throw new ArgumentException($"The raw message cannot be null or empty.", nameof(rawMessage)); + } +#if !NETSTANDARD2_0 + var indexOfPipe = rawMessage.IndexOf('|', StringComparison.Ordinal); +#else + var indexOfPipe = rawMessage.IndexOf("|", StringComparison.Ordinal); +#endif + if (indexOfPipe == -1) + { + throw new ArgumentException($"The raw message must contain a pipe character ('|').", nameof(rawMessage)); + } + + var messageType = rawMessage.Substring(0, indexOfPipe); + var messageContent = rawMessage.Substring(indexOfPipe + 1); + + switch (messageType) + { + case "__InvokeJavaScriptFailed": + case "__InvokeJavaScriptCompleted": + { +#if !NETSTANDARD2_0 + var indexOfPipeInContent = messageContent.IndexOf('|', StringComparison.Ordinal); +#else + var indexOfPipeInContent = messageContent.IndexOf("|", StringComparison.Ordinal); +#endif + if (indexOfPipeInContent == -1) + { + throw new ArgumentException($"The '{messageType}' message content must contain a pipe character ('|').", nameof(rawMessage)); + } + + var taskId = messageContent.Substring(0, indexOfPipeInContent); + var result = messageContent.Substring(indexOfPipeInContent + 1); + + var taskManager = this.GetRequiredService(); + if (messageType == "__InvokeJavaScriptFailed") + { + if (IsInvokeJavaScriptThrowsExceptionsEnabled) + { + if (string.IsNullOrWhiteSpace(result)) + { + taskManager.SetTaskFailed(taskId, new HybridWebViewInvokeJavaScriptException()); + } + else + { + var jsError = JsonSerializer.Deserialize(result, HybridWebViewHandlerJsonContext.Default.JSInvokeError); + var jsException = new HybridWebViewInvokeJavaScriptException(jsError?.Message, jsError?.Name, jsError?.StackTrace); + var ex = new HybridWebViewInvokeJavaScriptException($"InvokeJavaScript threw an exception: {jsException.Message}", jsException); + taskManager.SetTaskFailed(taskId, ex); + } + } + } + else + { + taskManager.SetTaskCompleted(taskId, result); + } + } + break; + case "__RawMessage": + VirtualView?.RawMessageReceived(messageContent); + break; + default: + throw new ArgumentException($"The message type '{messageType}' is not recognized.", nameof(rawMessage)); + } + } internal async Task InvokeDotNetAsync(Stream? streamBody = null, string? stringBody = null) { - var logger = MauiContext?.CreateLogger(); - return await HybridWebViewHelper.ProcessInvokeDotNetAsync( - VirtualView?.InvokeJavaScriptTarget, - VirtualView?.InvokeJavaScriptType, - logger, - streamBody, - stringBody); + try + { + var invokeTarget = VirtualView.InvokeJavaScriptTarget ?? throw new InvalidOperationException($"The {nameof(IHybridWebView)}.{nameof(IHybridWebView.InvokeJavaScriptTarget)} property must have a value in order to invoke a .NET method from JavaScript."); + var invokeTargetType = VirtualView.InvokeJavaScriptType ?? throw new InvalidOperationException($"The {nameof(IHybridWebView)}.{nameof(IHybridWebView.InvokeJavaScriptType)} property must have a value in order to invoke a .NET method from JavaScript."); + + JSInvokeMethodData? invokeData = null; + if (streamBody is not null) + { + invokeData = await JsonSerializer.DeserializeAsync(streamBody, HybridWebViewHandlerJsonContext.Default.JSInvokeMethodData); + } + else if (stringBody is not null && !string.IsNullOrWhiteSpace(stringBody)) + { + invokeData = JsonSerializer.Deserialize(stringBody, HybridWebViewHandlerJsonContext.Default.JSInvokeMethodData); + } + + if (invokeData?.MethodName is null) + { + throw new InvalidOperationException("The invoke data did not provide a method name."); + } + + var invokeResultRaw = await InvokeDotNetMethodAsync(invokeTargetType, invokeTarget, invokeData); + var invokeResult = CreateInvokeResult(invokeResultRaw); + var json = JsonSerializer.Serialize(invokeResult); + var contentBytes = Encoding.UTF8.GetBytes(json); + + return contentBytes; + } + catch (Exception ex) + { + MauiContext?.CreateLogger()?.LogError(ex, "An error occurred while invoking a .NET method from JavaScript: {ErrorMessage}", ex.Message); + + // Return error response instead of null so JavaScript can handle the error + var errorResult = CreateErrorResult(ex); + var errorJson = JsonSerializer.Serialize(errorResult, HybridWebViewHandlerJsonContext.Default.DotNetInvokeResult); + var errorBytes = Encoding.UTF8.GetBytes(errorJson); + return errorBytes; + } } -#endif + private static DotNetInvokeResult CreateInvokeResult(object? result) + { + // null invoke result means an empty result + if (result is null) + { + return new(); + } + + // a reference type or an array should be serialized to JSON + var resultType = result.GetType(); + if (resultType.IsArray || resultType.IsClass) + { + return new DotNetInvokeResult() + { + Result = JsonSerializer.Serialize(result), + IsJson = true, + }; + } + + // a value type should be returned as is + return new DotNetInvokeResult() + { + Result = result, + }; + } + + private static DotNetInvokeResult CreateErrorResult(Exception ex) + { + return new DotNetInvokeResult() + { + IsError = true, + ErrorMessage = ex.Message, + ErrorType = ex.GetType().Name, + ErrorStackTrace = ex.StackTrace + }; + } + + private static async Task InvokeDotNetMethodAsync( + [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] Type targetType, + object jsInvokeTarget, + JSInvokeMethodData invokeData) + { + var requestMethodName = invokeData.MethodName!; + var requestParams = invokeData.ParamValues; + + // get the method and its parameters from the .NET object instance + var dotnetMethod = targetType.GetMethod(requestMethodName, BindingFlags.Public | BindingFlags.Instance | BindingFlags.InvokeMethod); + if (dotnetMethod is null) + { + throw new InvalidOperationException($"The method {requestMethodName} couldn't be found on the {nameof(jsInvokeTarget)} of type {jsInvokeTarget.GetType().FullName}."); + } + var dotnetParams = dotnetMethod.GetParameters(); + if (requestParams is not null && dotnetParams.Length != requestParams.Length) + { + throw new InvalidOperationException($"The number of parameters on {nameof(jsInvokeTarget)}'s method {requestMethodName} ({dotnetParams.Length}) doesn't match the number of values passed from JavaScript code ({requestParams.Length})."); + } + + // deserialize the parameters from JSON to .NET types + object?[]? invokeParamValues = null; + if (requestParams is not null) + { + invokeParamValues = new object?[requestParams.Length]; + for (var i = 0; i < requestParams.Length; i++) + { + var reqValue = requestParams[i]; + var paramType = dotnetParams[i].ParameterType; + var deserialized = JsonSerializer.Deserialize(reqValue, paramType); + invokeParamValues[i] = deserialized; + } + } + + // invoke the .NET method + var dotnetReturnValue = GetDotNetMethodReturnValue(jsInvokeTarget, dotnetMethod, invokeParamValues); + + if (dotnetReturnValue is null) // null result + { + return null; + } + + if (dotnetReturnValue is Task task) // Task or Task result + { + await task; + + // Task + if (dotnetMethod.ReturnType.IsGenericType) + { + var resultProperty = dotnetMethod.ReturnType.GetProperty(nameof(Task.Result)); + return resultProperty?.GetValue(task); + } + + // Task + return null; + } + + return dotnetReturnValue; // regular result + } + + private static object? GetDotNetMethodReturnValue(object jsInvokeTarget, MethodInfo dotnetMethod, object?[]? invokeParamValues) + { + try + { + // invoke the .NET method + return dotnetMethod.Invoke(jsInvokeTarget, invokeParamValues); + } + catch (TargetInvocationException tie) // unwrap while preserving original stack trace + { + if (tie.InnerException is not null) + { + // Rethrow the underlying exception without losing its original stack trace + ExceptionDispatchInfo.Capture(tie.InnerException).Throw(); + + // unreachable, but required for compiler flow analysis + throw; + } + + // no inner exception; rethrow the TargetInvocationException itself preserving its stack + throw; + } + } + + private sealed class JSInvokeMethodData + { + public string? MethodName { get; set; } + public string[]? ParamValues { get; set; } + } + + private sealed class JSInvokeError + { + public string? Name { get; set; } + public string? Message { get; set; } + public string? StackTrace { get; set; } + } + + private sealed class DotNetInvokeResult + { + public object? Result { get; set; } + public bool IsJson { get; set; } + public bool IsError { get; set; } + public string? ErrorMessage { get; set; } + public string? ErrorType { get; set; } + public string? ErrorStackTrace { get; set; } + } + + [JsonSourceGenerationOptions()] + [JsonSerializable(typeof(JSInvokeMethodData))] + [JsonSerializable(typeof(JSInvokeError))] + [JsonSerializable(typeof(DotNetInvokeResult))] + private partial class HybridWebViewHandlerJsonContext : JsonSerializerContext + { + } + + #if PLATFORM && !TIZEN public static async void MapEvaluateJavaScriptAsync(IHybridWebViewHandler handler, IHybridWebView hybridWebView, object? arg) { - if (arg is not EvaluateJavaScriptAsyncRequest request) + if (arg is not EvaluateJavaScriptAsyncRequest request || + handler.PlatformView is not MauiHybridWebView hybridPlatformWebView) { return; } @@ -135,45 +393,118 @@ public static async void MapEvaluateJavaScriptAsync(IHybridWebViewHandler handle return; } - try + var script = request.Script; + // Make all the platforms mimic Android's implementation, which is by far the most complete. + if (!OperatingSystem.IsAndroid()) { - // Delegate to helper for all processing logic - var result = await HybridWebViewHelper.ProcessEvaluateJavaScriptAsync(handler, hybridWebView, request); + script = WebViewHelper.EscapeJsString(script); + + if (!OperatingSystem.IsWindows()) + { + // Use JSON.stringify() method to converts a JavaScript value to a JSON string + script = "try{JSON.stringify(eval('" + script + "'))}catch(e){'null'};"; + } + else + { + script = "try{eval('" + script + "')}catch(e){'null'};"; + } + } + + // Use the handler command to evaluate the JS + var innerRequest = new EvaluateJavaScriptAsyncRequest(script); + EvaluateJavaScript(handler, hybridWebView, innerRequest); + + var result = await innerRequest.Task; - request.SetResult(result!); + //if the js function errored or returned null/undefined treat it as null + if (result == "null") + { + result = null; } - catch (Exception ex) + //JSON.stringify wraps the result in literal quotes, we just want the actual returned result + //note that if the js function returns the string "null" we will get here and not above + else if (result != null) { - request.SetException(ex); + result = result.Trim('"'); } + + request.SetResult(result!); + } +#endif public static async void MapInvokeJavaScriptAsync(IHybridWebViewHandler handler, IHybridWebView hybridWebView, object? arg) { - if (arg is not HybridWebViewInvokeJavaScriptRequest request) - { - return; - } - - if (handler.PlatformView is null) +#if PLATFORM && !TIZEN + if (arg is not HybridWebViewInvokeJavaScriptRequest invokeJavaScriptRequest) { - request.SetCanceled(); return; } try { - // Delegate to helper for all processing logic - var result = await HybridWebViewHelper.ProcessInvokeJavaScriptAsync(handler, hybridWebView, request); + var result = await MapInvokeJavaScriptAsyncImpl(handler, hybridWebView, invokeJavaScriptRequest); - request.SetResult(result); + invokeJavaScriptRequest.SetResult(result); } catch (Exception ex) { - request.SetException(ex); + invokeJavaScriptRequest.SetException(ex); } - } +#else + await Task.CompletedTask; #endif + } + + static async Task MapInvokeJavaScriptAsyncImpl(IHybridWebViewHandler handler, IHybridWebView hybridWebView, HybridWebViewInvokeJavaScriptRequest invokeJavaScriptRequest) + { + // Create a callback for async JavaScript methods to invoke when they are done + var taskManager = handler.GetRequiredService(); + var (currentInvokeTaskId, callback) = taskManager.CreateTask(); + + var paramsValuesStringArray = + invokeJavaScriptRequest.ParamValues == null + ? string.Empty + : string.Join( + ", ", + invokeJavaScriptRequest.ParamValues.Select((v, i) => (v == null ? "null" : JsonSerializer.Serialize(v, invokeJavaScriptRequest.ParamJsonTypeInfos![i]!)))); + + await handler.InvokeAsync(nameof(IHybridWebView.EvaluateJavaScriptAsync), + new EvaluateJavaScriptAsyncRequest($"window.HybridWebView.__InvokeJavaScript({currentInvokeTaskId}, {invokeJavaScriptRequest.MethodName}, [{paramsValuesStringArray}])")); + + var stringResult = await callback.Task; + + // if there is no result or if the result was null/undefined, then treat it as null + if (stringResult is null || stringResult == "null" || stringResult == "undefined") + { + return null; + } + // if we are not looking for a return object, then return null + else if (invokeJavaScriptRequest.ReturnTypeJsonTypeInfo is null) + { + return null; + } + // if we are expecting a result, then deserialize what we have + else + { + var typedResult = JsonSerializer.Deserialize(stringResult, invokeJavaScriptRequest.ReturnTypeJsonTypeInfo); + return typedResult; + } + } + + internal static async Task GetAssetContentAsync(string assetPath) + { + using var stream = await GetAssetStreamAsync(assetPath); + if (stream == null) + { + return null; + } + using var reader = new StreamReader(stream); + + var contents = reader.ReadToEnd(); + + return contents; + } internal static async Task GetAssetStreamAsync(string assetPath) { diff --git a/src/Core/src/Handlers/HybridWebView/HybridWebViewHelper.cs b/src/Core/src/Handlers/HybridWebView/HybridWebViewHelper.cs deleted file mode 100644 index 65b82df20bf9..000000000000 --- a/src/Core/src/Handlers/HybridWebView/HybridWebViewHelper.cs +++ /dev/null @@ -1,482 +0,0 @@ -#if PLATFORM && !TIZEN -using System; -using System.Diagnostics.CodeAnalysis; -using System.IO; -using System.Linq; -using System.Reflection; -using System.Runtime.ExceptionServices; -using System.Text; -using System.Text.Json; -using System.Text.Json.Serialization; -using System.Text.Json.Serialization.Metadata; -using System.Threading.Tasks; -using Microsoft.Extensions.Logging; -using Microsoft.Maui.Handlers; -using Microsoft.Maui.Platform; - -namespace Microsoft.Maui; - -/// -/// Helper class containing all business logic for HybridWebView operations. -/// Keeps both Controls and Handler layers thin by centralizing processing logic. -/// -[RequiresUnreferencedCode("HybridWebView uses dynamic System.Text.Json serialization features.")] -#if !NETSTANDARD -[RequiresDynamicCode("HybridWebView uses dynamic System.Text.Json serialization features.")] -#endif -internal static partial class HybridWebViewHelper -{ - /// - /// Processes an EvaluateJavaScriptAsync request by wrapping the script with error handling - /// and processing the result. - /// - public static async Task ProcessEvaluateJavaScriptAsync(IHybridWebViewHandler handler, IHybridWebView hybridWebView, EvaluateJavaScriptAsyncRequest request) - { - var script = request.Script; - - if (script == null) - { - return null; - } - - // Escape and wrap script with try-catch and error handling - var escapedScript = WebViewHelper.EscapeJsString(script); - var wrappedScript = - $$""" - (function() { - try { - let result = eval('{{escapedScript}}'); - let resultObj = { - IsError: false, - Result: JSON.stringify(result) - }; - return JSON.stringify(resultObj); - } catch (error) { - console.error(error); - let errorObj; - if (!error) { - errorObj = { - IsError: true, - Message: 'Unknown error', - StackTrace: Error().stack - }; - } else if (error instanceof Error) { - errorObj = { - IsError: true, - Name: error.name, - Message: error.message, - StackTrace: error.stack - }; - } else if (typeof error === 'string') { - errorObj = { - IsError: true, - Message: error, - StackTrace: Error().stack - }; - } else { - errorObj = { - IsError: true, - Message: JSON.stringify(error), - StackTrace: Error().stack - }; - } - return JSON.stringify(errorObj); - } - })() - """; - - // Use the handler command to evaluate the JS - var innerRequest = new EvaluateJavaScriptAsyncRequest(wrappedScript); - - // Execute via platform evaluator - handler.PlatformView.EvaluateJavaScript(innerRequest); - - var result = await innerRequest.Task; - - if (result == null) - return null; - - // Android's WebView automatically JSON-encodes the return value, so we need to unwrap it - // Check if the result is a JSON-encoded string (starts and ends with quotes) - if (OperatingSystem.IsAndroid()) - { - // Deserialize once to unwrap the JSON string - result = JsonSerializer.Deserialize(result); - if (result == null) - return null; - } - - var jsResult = JsonSerializer.Deserialize(result, HybridWebViewHelperJsonContext.Default.JSInvokeResult); - if (jsResult?.IsError == true) - { - var jsException = new HybridWebViewInvokeJavaScriptException(jsResult?.Message, jsResult?.Name, jsResult?.StackTrace); - throw new HybridWebViewInvokeJavaScriptException($"EvaluateJavaScriptAsync threw an exception: {jsException.Message}", jsException); - } - - var returnValue = jsResult?.Result; - - //if the js function errored or returned null/undefined treat it as null - if (returnValue == "null" || returnValue == "undefined") - { - returnValue = null; - } - //JSON.stringify wraps the result - we need to unwrap it properly - //note that if the js function returns the string "null" we will get here and not above - else if (returnValue != null) - { - // Check if the result is a JSON string (starts and ends with quotes) - if (returnValue.Length >= 2 && returnValue[0] == '"' && returnValue[^1] == '"') - { - // Properly deserialize the JSON string to handle escaped characters - returnValue = JsonSerializer.Deserialize(returnValue); - } - // Otherwise it's a primitive value (number, boolean, etc.) that's already in string form - // No need to deserialize - just return as-is - } - - return returnValue; - } - - /// - /// Processes an InvokeJavaScriptAsync request by building the JS call string, - /// executing it, and processing the result. - /// - public static async Task ProcessInvokeJavaScriptAsync(IHybridWebViewHandler handler, IHybridWebView hybridWebView, HybridWebViewInvokeJavaScriptRequest request) - { - var taskManager = handler.GetRequiredService(); - - // Create a callback for async JavaScript methods to invoke when they are done - var task = taskManager.CreateTask(); - - var paramsValuesStringArray = request.ParamValues == null - ? string.Empty - : string.Join( - ", ", - request.ParamValues.Select((v, i) => v is null ? "null" : JsonSerializer.Serialize(v, request.ParamJsonTypeInfos![i]!))); - - var js = $"window.HybridWebView.__InvokeJavaScript({task.TaskId}, {request.MethodName}, [{paramsValuesStringArray}])"; - - var innerRequest = new EvaluateJavaScriptAsyncRequest(js); - - handler.PlatformView.EvaluateJavaScript(innerRequest); - - // Don't await innerRequest.Task because __InvokeJavaScript is async and returns a Promise, - // which iOS can't convert to a string. Instead, we wait for the callback message from JavaScript. - // The JavaScript function will call invokeJavaScriptCallbackInDotNet() when done. - - var stringResult = await task.TaskCompletionSource.Task; - - // if there is no result or if the result was null/undefined, then treat it as null - if (stringResult is null || stringResult == "null" || stringResult == "undefined") - { - return null; - } - // if we are not looking for a return object, then return null - else if (request.ReturnTypeJsonTypeInfo is null) - { - return null; - } - // if we are expecting a result, then deserialize what we have - else - { - var typedResult = JsonSerializer.Deserialize(stringResult, request.ReturnTypeJsonTypeInfo); - return typedResult; - } - } - - /// - /// Invokes a .NET method from JavaScript. - /// - public static async Task ProcessInvokeDotNetAsync( - object? invokeTarget, - [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] Type? invokeTargetType, - ILogger? logger, - Stream? streamBody = null, - string? stringBody = null) - { - try - { - if (invokeTarget is null) - { - throw new InvalidOperationException($"The InvokeJavaScriptTarget property must have a value in order to invoke a .NET method from JavaScript."); - } - - if (invokeTargetType is null) - { - throw new InvalidOperationException($"The InvokeJavaScriptType property must have a value in order to invoke a .NET method from JavaScript."); - } - - JSInvokeMethodData? invokeData = null; - if (streamBody is not null) - { - invokeData = await JsonSerializer.DeserializeAsync(streamBody, HybridWebViewHelperJsonContext.Default.JSInvokeMethodData); - } - else if (stringBody is not null && !string.IsNullOrWhiteSpace(stringBody)) - { - invokeData = JsonSerializer.Deserialize(stringBody, HybridWebViewHelperJsonContext.Default.JSInvokeMethodData); - } - - if (invokeData?.MethodName is null) - { - throw new InvalidOperationException("The invoke data did not provide a method name."); - } - - var invokeResultRaw = await InvokeDotNetMethodAsync(invokeTargetType, invokeTarget, invokeData); - var invokeResult = CreateInvokeResult(invokeResultRaw); - var json = JsonSerializer.Serialize(invokeResult); - var contentBytes = Encoding.UTF8.GetBytes(json); - - return contentBytes; - } - catch (Exception ex) - { - logger?.LogError(ex, "An error occurred while invoking a .NET method from JavaScript: {ErrorMessage}", ex.Message); - - // Return error response instead of null so JavaScript can handle the error - var errorResult = CreateErrorResult(ex); - var errorJson = JsonSerializer.Serialize(errorResult, HybridWebViewHelperJsonContext.Default.DotNetInvokeResult); - var errorBytes = Encoding.UTF8.GetBytes(errorJson); - return errorBytes; - } - } - - private static DotNetInvokeResult CreateInvokeResult(object? result) - { - // null invoke result means an empty result - if (result is null) - { - return new(); - } - - // a reference type or an array should be serialized to JSON - var resultType = result.GetType(); - if (resultType.IsArray || resultType.IsClass) - { - return new DotNetInvokeResult() - { - Result = JsonSerializer.Serialize(result), - IsJson = true, - }; - } - - // a value type should be returned as is - return new DotNetInvokeResult() - { - Result = result, - }; - } - - private static DotNetInvokeResult CreateErrorResult(Exception ex) - { - return new DotNetInvokeResult() - { - IsError = true, - ErrorMessage = ex.Message, - ErrorType = ex.GetType().Name, - ErrorStackTrace = ex.StackTrace - }; - } - - private static async Task InvokeDotNetMethodAsync( - [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] Type targetType, - object jsInvokeTarget, - JSInvokeMethodData invokeData) - { - var requestMethodName = invokeData.MethodName!; - var requestParams = invokeData.ParamValues; - - // get the method and its parameters from the .NET object instance - var dotnetMethod = targetType.GetMethod(requestMethodName, BindingFlags.Public | BindingFlags.Instance | BindingFlags.InvokeMethod); - if (dotnetMethod is null) - { - throw new InvalidOperationException($"The method {requestMethodName} couldn't be found on the {nameof(jsInvokeTarget)} of type {jsInvokeTarget.GetType().FullName}."); - } - var dotnetParams = dotnetMethod.GetParameters(); - if (requestParams is not null && dotnetParams.Length != requestParams.Length) - { - throw new InvalidOperationException($"The number of parameters on {nameof(jsInvokeTarget)}'s method {requestMethodName} ({dotnetParams.Length}) doesn't match the number of values passed from JavaScript code ({requestParams.Length})."); - } - - // deserialize the parameters from JSON to .NET types - object?[]? invokeParamValues = null; - if (requestParams is not null) - { - invokeParamValues = new object?[requestParams.Length]; - for (var i = 0; i < requestParams.Length; i++) - { - var reqValue = requestParams[i]; - var paramType = dotnetParams[i].ParameterType; - var deserialized = JsonSerializer.Deserialize(reqValue, paramType); - invokeParamValues[i] = deserialized; - } - } - - // invoke the .NET method - var dotnetReturnValue = GetDotNetMethodReturnValue(jsInvokeTarget, dotnetMethod, invokeParamValues); - - if (dotnetReturnValue is null) // null result - { - return null; - } - - if (dotnetReturnValue is Task task) // Task or Task result - { - await task; - - // Task - if (dotnetMethod.ReturnType.IsGenericType) - { - var resultProperty = dotnetMethod.ReturnType.GetProperty(nameof(Task.Result)); - return resultProperty?.GetValue(task); - } - - // Task - return null; - } - - return dotnetReturnValue; // regular result - } - - private static object? GetDotNetMethodReturnValue(object jsInvokeTarget, MethodInfo dotnetMethod, object?[]? invokeParamValues) - { - try - { - // invoke the .NET method - return dotnetMethod.Invoke(jsInvokeTarget, invokeParamValues); - } - catch (TargetInvocationException tie) // unwrap while preserving original stack trace - { - if (tie.InnerException is not null) - { - // Rethrow the underlying exception without losing its original stack trace - ExceptionDispatchInfo.Capture(tie.InnerException).Throw(); - - // unreachable, but required for compiler flow analysis - throw; - } - - // no inner exception; rethrow the TargetInvocationException itself preserving its stack - throw; - } - } - - /// - /// Processes raw messages from the web view, handling special message types like JavaScript invoke results. - /// - public static void ProcessRawMessage(IHybridWebViewHandler handler, IHybridWebView virtualView, string rawMessage) - { - if (string.IsNullOrEmpty(rawMessage)) - { - throw new ArgumentException($"The raw message cannot be null or empty.", nameof(rawMessage)); - } -#if !NETSTANDARD2_0 - var indexOfPipe = rawMessage.IndexOf('|', StringComparison.Ordinal); -#else - var indexOfPipe = rawMessage.IndexOf("|", StringComparison.Ordinal); -#endif - if (indexOfPipe == -1) - { - throw new ArgumentException($"The raw message must contain a pipe character ('|').", nameof(rawMessage)); - } - - var messageType = rawMessage.Substring(0, indexOfPipe); - var messageContent = rawMessage.Substring(indexOfPipe + 1); - - switch (messageType) - { - case "__InvokeJavaScriptFailed": - case "__InvokeJavaScriptCompleted": - { -#if !NETSTANDARD2_0 - var indexOfPipeInContent = messageContent.IndexOf('|', StringComparison.Ordinal); -#else - var indexOfPipeInContent = messageContent.IndexOf("|", StringComparison.Ordinal); -#endif - if (indexOfPipeInContent == -1) - { - throw new ArgumentException($"The '{messageType}' message content must contain a pipe character ('|').", nameof(rawMessage)); - } - - var taskId = messageContent.Substring(0, indexOfPipeInContent); - var result = messageContent.Substring(indexOfPipeInContent + 1); - - var taskManager = handler.GetRequiredService(); - if (messageType == "__InvokeJavaScriptFailed") - { - if (IsInvokeJavaScriptThrowsExceptionsEnabled) - { - if (string.IsNullOrWhiteSpace(result)) - { - taskManager.SetTaskFailed(taskId, new HybridWebViewInvokeJavaScriptException()); - } - else - { - var jsError = JsonSerializer.Deserialize(result, HybridWebViewHelperJsonContext.Default.JSInvokeError); - var jsException = new HybridWebViewInvokeJavaScriptException(jsError?.Message, jsError?.Name, jsError?.StackTrace); - var ex = new HybridWebViewInvokeJavaScriptException($"InvokeJavaScriptAsync threw an exception: {jsException.Message}", jsException); - taskManager.SetTaskFailed(taskId, ex); - } - } - } - else - { - taskManager.SetTaskCompleted(taskId, result); - } - } - break; - case "__RawMessage": - virtualView?.RawMessageReceived(messageContent); - break; - default: - throw new ArgumentException($"The message type '{messageType}' is not recognized.", nameof(rawMessage)); - } - } - - private const string InvokeJavaScriptThrowsExceptionsSwitch = "HybridWebView.InvokeJavaScriptThrowsExceptions"; - - private static bool IsInvokeJavaScriptThrowsExceptionsEnabled => - !AppContext.TryGetSwitch(InvokeJavaScriptThrowsExceptionsSwitch, out var enabled) || enabled; - - // DTOs for JSON serialization - internal sealed class JSInvokeResult - { - public string? Result { get; set; } - public bool IsError { get; set; } - public string? Name { get; set; } - public string? Message { get; set; } - public string? StackTrace { get; set; } - } - - internal sealed class JSInvokeMethodData - { - public string? MethodName { get; set; } - public string[]? ParamValues { get; set; } - } - - internal sealed class JSInvokeError - { - public string? Name { get; set; } - public string? Message { get; set; } - public string? StackTrace { get; set; } - } - - internal sealed class DotNetInvokeResult - { - public object? Result { get; set; } - public bool IsJson { get; set; } - public bool IsError { get; set; } - public string? ErrorMessage { get; set; } - public string? ErrorType { get; set; } - public string? ErrorStackTrace { get; set; } - } - - [JsonSourceGenerationOptions()] - [JsonSerializable(typeof(JSInvokeResult))] - [JsonSerializable(typeof(JSInvokeMethodData))] - [JsonSerializable(typeof(JSInvokeError))] - [JsonSerializable(typeof(DotNetInvokeResult))] - internal partial class HybridWebViewHelperJsonContext : JsonSerializerContext - { - } -} -#endif