diff --git a/src/Controls/tests/DeviceTests/Elements/HybridWebView/HybridWebViewTestsBase.cs b/src/Controls/tests/DeviceTests/Elements/HybridWebView/HybridWebViewTestsBase.cs index f5c3e3609b37..68522216d53e 100644 --- a/src/Controls/tests/DeviceTests/Elements/HybridWebView/HybridWebViewTestsBase.cs +++ b/src/Controls/tests/DeviceTests/Elements/HybridWebView/HybridWebViewTestsBase.cs @@ -40,6 +40,7 @@ protected async Task RunTest(string? defaultFile, Func test HybridRoot = "HybridTestRoot", DefaultFile = defaultFile ?? "index.html", }; + await RunTest(hybridWebView, (handler, view) => test(view)); } @@ -58,7 +59,12 @@ await AttachAndRun(hybridWebView, async handler => { await WebViewHelpers.WaitForHybridWebViewLoaded(hybridWebView); - await test((HybridWebViewHandler)handler, 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); }); } diff --git a/src/Controls/tests/DeviceTests/Elements/HybridWebView/HybridWebViewTests_EvaluateJavaScriptAsync.cs b/src/Controls/tests/DeviceTests/Elements/HybridWebView/HybridWebViewTests_EvaluateJavaScriptAsync.cs index fcd6b52346cf..9b6332be89e6 100644 --- a/src/Controls/tests/DeviceTests/Elements/HybridWebView/HybridWebViewTests_EvaluateJavaScriptAsync.cs +++ b/src/Controls/tests/DeviceTests/Elements/HybridWebView/HybridWebViewTests_EvaluateJavaScriptAsync.cs @@ -11,15 +11,56 @@ namespace Microsoft.Maui.DeviceTests; public partial class HybridWebViewTests_EvaluateJavaScriptAsync : HybridWebViewTestsBase { [Fact] - public Task EvaluateJavaScriptAndGetResult() => + public Task EvaluateJavaScriptAsync_WithStringParameters() => 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 1d0b36a7faad..95fe209dd847 100644 --- a/src/Controls/tests/DeviceTests/Elements/HybridWebView/HybridWebViewTests_InvokeJavaScriptAsync.cs +++ b/src/Controls/tests/DeviceTests/Elements/HybridWebView/HybridWebViewTests_InvokeJavaScriptAsync.cs @@ -1,6 +1,8 @@ #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; @@ -29,7 +31,7 @@ public Task RequestFileFromJS(string url, int expectedStatus) => }); [Fact] - public Task InvokeJavaScriptMethodWithParametersAndNullsAndComplexResult() => + public Task InvokeJavaScript_WithParametersAndNulls_AndComplexResult() => RunTest(async (hybridWebView) => { var x = 123.456m; @@ -47,7 +49,7 @@ public Task InvokeJavaScriptMethodWithParametersAndNullsAndComplexResult() => }); [Fact] - public Task InvokeJavaScriptMethodWithParametersAndDecimalResult() => + public Task InvokeJavaScript_WithParameters_AndDecimalResult() => RunTest(async (hybridWebView) => { var x = 123.456m; @@ -66,7 +68,7 @@ public Task InvokeJavaScriptMethodWithParametersAndDecimalResult() => [InlineData(-123.456)] [InlineData(0.0)] [InlineData(123.456)] - public Task InvokeJavaScriptMethodWithParametersAndDoubleResult(double expected) => + public Task InvokeJavaScript_WithParameters_AndDoubleResult(double expected) => RunTest(async (hybridWebView) => { var result = await hybridWebView.InvokeJavaScriptAsync( @@ -83,7 +85,7 @@ public Task InvokeJavaScriptMethodWithParametersAndDoubleResult(double expected) [InlineData(-123.456)] [InlineData(0.0)] [InlineData(123.456)] - public Task InvokeJavaScriptMethodWithParametersAndNullableDoubleResult(double? expected) => + public Task InvokeJavaScript_WithParameters_AndNullableDoubleResult(double? expected) => RunTest(async (hybridWebView) => { var result = await hybridWebView.InvokeJavaScriptAsync( @@ -96,7 +98,7 @@ public Task InvokeJavaScriptMethodWithParametersAndNullableDoubleResult(double? }); [Fact] - public Task InvokeJavaScriptMethodWithParametersAndNewDoubleResult() => + public Task InvokeJavaScript_WithParameters_AndNewDoubleResult() => RunTest(async (hybridWebView) => { var x = 123.456m; @@ -115,7 +117,7 @@ public Task InvokeJavaScriptMethodWithParametersAndNewDoubleResult() => [InlineData(-123)] [InlineData(0)] [InlineData(123)] - public Task InvokeJavaScriptMethodWithParametersAndIntResult(int expected) => + public Task InvokeJavaScript_WithParameters_AndIntResult(int expected) => RunTest(async (hybridWebView) => { var result = await hybridWebView.InvokeJavaScriptAsync( @@ -132,7 +134,7 @@ public Task InvokeJavaScriptMethodWithParametersAndIntResult(int expected) => [InlineData(-123)] [InlineData(0)] [InlineData(123)] - public Task InvokeJavaScriptMethodWithParametersAndNullableIntResult(int? expected) => + public Task InvokeJavaScript_WithParameters_AndNullableIntResult(int? expected) => RunTest(async (hybridWebView) => { var result = await hybridWebView.InvokeJavaScriptAsync( @@ -145,7 +147,7 @@ public Task InvokeJavaScriptMethodWithParametersAndNullableIntResult(int? expect }); [Fact] - public Task InvokeJavaScriptMethodWithParametersAndNewIntResult() => + public Task InvokeJavaScript_WithParameters_AndNewIntResult() => RunTest(async (hybridWebView) => { var x = 123; @@ -166,7 +168,7 @@ public Task InvokeJavaScriptMethodWithParametersAndNewIntResult() => [InlineData("foo")] [InlineData("null")] [InlineData("undefined")] - public Task InvokeJavaScriptMethodWithParametersAndStringResult(string? expected) => + public Task InvokeJavaScript_WithParameters_AndStringResult(string? expected) => RunTest(async (hybridWebView) => { var result = await hybridWebView.InvokeJavaScriptAsync( @@ -179,7 +181,7 @@ public Task InvokeJavaScriptMethodWithParametersAndStringResult(string? expected }); [Fact] - public Task InvokeJavaScriptMethodWithParametersAndNewStringResult() => + public Task InvokeJavaScript_WithParameters_AndNewStringResult() => RunTest(async (hybridWebView) => { var x = "abc"; @@ -197,7 +199,7 @@ public Task InvokeJavaScriptMethodWithParametersAndNewStringResult() => [Theory] [InlineData(true)] [InlineData(false)] - public Task InvokeJavaScriptMethodWithParametersAndBoolResult(bool expected) => + public Task InvokeJavaScript_WithParameters_AndBoolResult(bool expected) => RunTest(async (hybridWebView) => { var result = await hybridWebView.InvokeJavaScriptAsync( @@ -210,7 +212,7 @@ public Task InvokeJavaScriptMethodWithParametersAndBoolResult(bool expected) => }); [Fact] - public Task InvokeJavaScriptMethodWithParametersAndComplexResult() => + public Task InvokeJavaScript_WithParameters_AndComplexResult() => RunTest(async (hybridWebView) => { var x = 123.456m; @@ -228,7 +230,7 @@ public Task InvokeJavaScriptMethodWithParametersAndComplexResult() => }); [Fact] - public Task InvokeAsyncJavaScriptMethodWithParametersAndComplexResult() => + public Task InvokeJavaScript_WithParameters_AndAsyncComplexResult() => RunTest(async (hybridWebView) => { var s1 = "new_key"; @@ -248,7 +250,7 @@ public Task InvokeAsyncJavaScriptMethodWithParametersAndComplexResult() => }); [Fact] - public Task InvokeJavaScriptMethodWithParametersAndVoidReturn() => + public Task InvokeJavaScript_WithParameters_AndVoidReturn() => RunTest(async (hybridWebView) => { var x = 123.456m; @@ -267,7 +269,7 @@ await hybridWebView.InvokeJavaScriptAsync( }); [Fact] - public Task InvokeJavaScriptMethodWithParametersAndVoidReturnUsingObjectReturnMethod() => + public Task InvokeJavaScript_WithParameters_AndVoidReturn_UsingObjectReturnMethod() => RunTest(async (hybridWebView) => { var x = 123.456m; @@ -289,7 +291,7 @@ public Task InvokeJavaScriptMethodWithParametersAndVoidReturnUsingObjectReturnMe }); [Fact] - public Task InvokeJavaScriptMethodWithParametersAndVoidReturnUsingNullReturnMethod() => + public Task InvokeJavaScript_WithParameters_AndVoidReturn_UsingNullReturnMethod() => RunTest(async (hybridWebView) => { var x = 123.456m; @@ -313,10 +315,10 @@ public Task InvokeJavaScriptMethodWithParametersAndVoidReturnUsingNullReturnMeth [Theory] [InlineData("")] [InlineData("Async")] - public Task InvokeJavaScriptMethodThatThrowsNumber(string type) => + public Task InvokeJavaScript_ThatThrowsNumber(string type) => RunExceptionTest("EvaluateMeWithParamsThatThrows" + type, 1, ex => { - Assert.Equal("InvokeJavaScript threw an exception: 777.777", ex.Message); + Assert.Equal("InvokeJavaScriptAsync 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); @@ -325,10 +327,10 @@ public Task InvokeJavaScriptMethodThatThrowsNumber(string type) => [Theory] [InlineData("")] [InlineData("Async")] - public Task InvokeJavaScriptMethodThatThrowsString(string type) => + public Task InvokeJavaScript_ThatThrowsString(string type) => RunExceptionTest("EvaluateMeWithParamsThatThrows" + type, 2, ex => { - Assert.Equal("InvokeJavaScript threw an exception: String: 777.777", ex.Message); + Assert.Equal("InvokeJavaScriptAsync 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); @@ -337,10 +339,10 @@ public Task InvokeJavaScriptMethodThatThrowsString(string type) => [Theory] [InlineData("")] [InlineData("Async")] - public Task InvokeJavaScriptMethodThatThrowsError(string type) => + public Task InvokeJavaScript_ThatThrowsError(string type) => RunExceptionTest("EvaluateMeWithParamsThatThrows" + type, 3, ex => { - Assert.Equal("InvokeJavaScript threw an exception: Generic Error: 777.777", ex.Message); + Assert.Equal("InvokeJavaScriptAsync 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); @@ -349,7 +351,7 @@ public Task InvokeJavaScriptMethodThatThrowsError(string type) => [Theory] [InlineData("")] [InlineData("Async")] - public Task InvokeJavaScriptMethodThatThrowsTypedNumber(string type) => + public Task InvokeJavaScript_ThatThrowsTypedNumber(string type) => RunExceptionTest("EvaluateMeWithParamsThatThrows" + type, 4, ex => { Assert.Contains("undefined", ex.Message, StringComparison.OrdinalIgnoreCase); @@ -358,6 +360,163 @@ public Task InvokeJavaScriptMethodThatThrowsTypedNumber(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 a904e3ccf467..2a9439ed743f 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 bd5ee137d8d4..49709cfa07da 100644 --- a/src/Controls/tests/DeviceTests/Resources/Raw/HybridTestRoot/index.html +++ b/src/Controls/tests/DeviceTests/Resources/Raw/HybridTestRoot/index.html @@ -158,6 +158,63 @@ // 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 876cb0f223f1..862117e34d24 100644 --- a/src/Core/src/Handlers/HybridWebView/HybridWebViewHandler.Standard.cs +++ b/src/Core/src/Handlers/HybridWebView/HybridWebViewHandler.Standard.cs @@ -8,6 +8,8 @@ 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 c686585c373e..14330d546594 100644 --- a/src/Core/src/Handlers/HybridWebView/HybridWebViewHandler.Tizen.cs +++ b/src/Core/src/Handlers/HybridWebView/HybridWebViewHandler.Tizen.cs @@ -8,6 +8,8 @@ public partial class HybridWebViewHandler : ViewHandler !AppContext.TryGetSwitch(InvokeJavaScriptThrowsExceptionsSwitch, out var enabled) || enabled; - 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)); - } - } +#if PLATFORM && !TIZEN + void MessageReceived(string rawMessage) => + HybridWebViewHelper.ProcessRawMessage(this, VirtualView, rawMessage); internal async Task InvokeDotNetAsync(Stream? streamBody = null, string? stringBody = null) { - 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; - } - } - - 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, - }; + var logger = MauiContext?.CreateLogger(); + return await HybridWebViewHelper.ProcessInvokeDotNetAsync( + VirtualView?.InvokeJavaScriptTarget, + VirtualView?.InvokeJavaScriptType, + logger, + streamBody, + stringBody); } - 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 - { - } - - +#endif #if PLATFORM && !TIZEN public static async void MapEvaluateJavaScriptAsync(IHybridWebViewHandler handler, IHybridWebView hybridWebView, object? arg) { - if (arg is not EvaluateJavaScriptAsyncRequest request || - handler.PlatformView is not MauiHybridWebView hybridPlatformWebView) + if (arg is not EvaluateJavaScriptAsyncRequest request) { return; } @@ -393,118 +135,45 @@ public static async void MapEvaluateJavaScriptAsync(IHybridWebViewHandler handle return; } - var script = request.Script; - // Make all the platforms mimic Android's implementation, which is by far the most complete. - if (!OperatingSystem.IsAndroid()) + try { - 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; + // Delegate to helper for all processing logic + var result = await HybridWebViewHelper.ProcessEvaluateJavaScriptAsync(handler, hybridWebView, request); - //if the js function errored or returned null/undefined treat it as null - if (result == "null") - { - result = null; + request.SetResult(result!); } - //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) + catch (Exception ex) { - result = result.Trim('"'); + request.SetException(ex); } - - request.SetResult(result!); - } -#endif public static async void MapInvokeJavaScriptAsync(IHybridWebViewHandler handler, IHybridWebView hybridWebView, object? arg) { -#if PLATFORM && !TIZEN - if (arg is not HybridWebViewInvokeJavaScriptRequest invokeJavaScriptRequest) + if (arg is not HybridWebViewInvokeJavaScriptRequest request) { return; } - try - { - var result = await MapInvokeJavaScriptAsyncImpl(handler, hybridWebView, invokeJavaScriptRequest); - - invokeJavaScriptRequest.SetResult(result); - } - catch (Exception ex) + if (handler.PlatformView is null) { - invokeJavaScriptRequest.SetException(ex); + request.SetCanceled(); + return; } -#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 + try { - var typedResult = JsonSerializer.Deserialize(stringResult, invokeJavaScriptRequest.ReturnTypeJsonTypeInfo); - return typedResult; - } - } + // Delegate to helper for all processing logic + var result = await HybridWebViewHelper.ProcessInvokeJavaScriptAsync(handler, hybridWebView, request); - internal static async Task GetAssetContentAsync(string assetPath) - { - using var stream = await GetAssetStreamAsync(assetPath); - if (stream == null) + request.SetResult(result); + } + catch (Exception ex) { - return null; + request.SetException(ex); } - using var reader = new StreamReader(stream); - - var contents = reader.ReadToEnd(); - - return contents; } +#endif 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 new file mode 100644 index 000000000000..65b82df20bf9 --- /dev/null +++ b/src/Core/src/Handlers/HybridWebView/HybridWebViewHelper.cs @@ -0,0 +1,482 @@ +#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