Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ class GreetJsMessageHandler : IJsMessageHandler {
}
val param = processParams<GreetModel>(message)
val data = GreetModel("KMP Received ${param.message}")
callback(dataToJsonString(data))
navigator?.coroutineScope?.launch { callback(dataToJsonString(data)) }
// EventBus.post(NavigationEvent())
navigator?.coroutineScope?.launch {
FlowEventBus.publishEvent(NavigationEvent())
Expand Down
4 changes: 4 additions & 0 deletions webview/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,10 @@ kotlin {
implementation(libs.kotlin.serialization.json)
}

commonTest.dependencies {
implementation(libs.kotlin.test)
}

androidMain.dependencies {
api(libs.android.activity.compose)
api(libs.android.webkit)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ import androidx.compose.runtime.Immutable
import androidx.compose.runtime.remember
import com.multiplatform.webview.web.IWebView
import com.multiplatform.webview.web.WebViewNavigator
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json

/**
* Created By Kevin Zou On 2023/10/31
Expand Down Expand Up @@ -39,9 +41,35 @@ open class WebViewJsBridge(
private fun onCallback(
data: String,
callbackId: Int,
) = webView?.evaluateJavaScript("window.$jsBridgeName.onCallback($callbackId, '$data')")
) = webView?.evaluateJavaScript(callbackScript(jsBridgeName, callbackId, data))
}

/**
* The script that hands [data] to the web side's pending callback [callbackId].
*
* [data] is spliced in as a JavaScript string literal rather than pasted between quotes, so
* quotes, backslashes and line breaks in the payload reach the callback unchanged instead of
* breaking the script.
*/
internal fun callbackScript(
jsBridgeName: String,
callbackId: Int,
data: String,
): String = "window.$jsBridgeName.onCallback($callbackId, ${data.toJsStringLiteral()})"

/**
* Renders this string as a JavaScript string literal.
*
* JSON string escaping is a subset of JavaScript's, so the JSON encoding is a valid
* double-quoted literal. U+2028 and U+2029 are escaped as well: JSON allows them raw, but
* JavaScript treated them as line terminators before ES2019.
*/
internal fun String.toJsStringLiteral(): String =
Json
.encodeToString(this)
.replace("\u2028", "\\u2028")
.replace("\u2029", "\\u2029")

/**
* Create a [WebViewJsBridge] that is remembered across Compositions.
*/
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
package com.multiplatform.webview.jsbridge

import kotlin.test.Test
import kotlin.test.assertEquals
import kotlin.test.assertFalse

class WebViewJsBridgeTest {
@Test
fun plainTextIsQuoted() {
assertEquals("\"hello\"", "hello".toJsStringLiteral())
}

@Test
fun quotesAndBackslashesAreEscaped() {
assertEquals(
"\"it's \\\"quoted\\\" and \\\\ escaped\"",
"it's \"quoted\" and \\ escaped".toJsStringLiteral(),
)
}

@Test
fun lineTerminatorsAreEscaped() {
val literal = "line 1\nline 2\r\n\u2028\u2029".toJsStringLiteral()
assertEquals("\"line 1\\nline 2\\r\\n\\u2028\\u2029\"", literal)
assertFalse(literal.any { it == '\n' || it == '\r' || it == '\u2028' || it == '\u2029' })
}

@Test
fun jsonPayloadKeepsItsOwnEscapes() {
// #347: a JSON payload whose string field holds a newline. The encoder wrote that
// newline as the two characters `\n`; the literal must carry both so JSON.parse on
// the web side sees an escape sequence rather than a raw control character.
val payload = """{"text":"line 1\nline 2"}"""
assertEquals("\"{\\\"text\\\":\\\"line 1\\\\nline 2\\\"}\"", payload.toJsStringLiteral())
}

@Test
fun callbackScriptSplicesTheLiteral() {
assertEquals(
"window.kmpJsBridge.onCallback(7, \"it's\\ndone\")",
callbackScript("kmpJsBridge", 7, "it's\ndone"),
)
}
}