From 98d83627031f7e1186ea990cfea4e58838e05263 Mon Sep 17 00:00:00 2001 From: Wenxi Zeng Date: Wed, 5 Nov 2025 12:40:11 -0600 Subject: [PATCH 1/9] implement JSStorage --- .../analytics/liveplugins/kotlin/JSStorage.kt | 105 ++++++++++++++++++ .../liveplugins/kotlin/LivePlugins.kt | 2 + 2 files changed, 107 insertions(+) create mode 100644 analytics-kotlin-live/src/main/java/com/segment/analytics/liveplugins/kotlin/JSStorage.kt diff --git a/analytics-kotlin-live/src/main/java/com/segment/analytics/liveplugins/kotlin/JSStorage.kt b/analytics-kotlin-live/src/main/java/com/segment/analytics/liveplugins/kotlin/JSStorage.kt new file mode 100644 index 0000000..a04c3bb --- /dev/null +++ b/analytics-kotlin-live/src/main/java/com/segment/analytics/liveplugins/kotlin/JSStorage.kt @@ -0,0 +1,105 @@ +package com.segment.analytics.liveplugins.kotlin + +import android.content.SharedPreferences +import androidx.core.content.edit +import com.segment.analytics.kotlin.core.utilities.getBoolean +import com.segment.analytics.kotlin.core.utilities.getDouble +import com.segment.analytics.kotlin.core.utilities.getInt +import com.segment.analytics.kotlin.core.utilities.getLong +import com.segment.analytics.kotlin.core.utilities.getString +import com.segment.analytics.substrata.kotlin.JSArray +import com.segment.analytics.substrata.kotlin.JSObject +import com.segment.analytics.substrata.kotlin.JSScope +import com.segment.analytics.substrata.kotlin.JsonElementConverter +import kotlinx.serialization.encodeToString +import kotlinx.serialization.json.Json +import kotlinx.serialization.json.JsonObject +import kotlinx.serialization.json.buildJsonObject +import kotlinx.serialization.json.put + +class JSStorage { + + internal var sharedPreferences: SharedPreferences? = null + + private var engine: JSScope? = null + + // JSEngine requires an empty constructor to be able to export this class + constructor() {} + + constructor(sharedPreferences: SharedPreferences, engine: JSScope) { + this.sharedPreferences = sharedPreferences + this.engine = engine + } + + fun setValue(key: String, value: Boolean) { + save(key, value, TYPE_BOOLEAN) + } + + fun setValue(key: String, value: Double) { + save(key, value, TYPE_DOUBLE) + } + + fun setValue(key: String, value: Int) { + save(key, value, TYPE_INT) + } + + fun setValue(key: String, value: String) { + save(key, value, TYPE_STRING) + } + + fun setValue(key: String, value: Long) { + save(key, value, TYPE_LONG) + } + + fun setValue(key: String, value: JSObject) { + save(key, value, TYPE_OBJECT) + } + + fun setValue(key: String, value: JSArray) { + save(key, value, TYPE_ARRAY) + } + + fun getValue(key: String): Any? { + return this.sharedPreferences?.getString(key, null)?.let { + Json.decodeFromString(it).unwrap() + } + } + + private fun save(key: String, value: Any, type: String) { + val jsonObject = buildJsonObject { + put(PROP_TYPE, type) + put(PROP_VALUE, Json.encodeToString(value)) + } + + this.sharedPreferences?.edit(commit = true) { putString(key, Json.encodeToString(jsonObject)) } + } + + private fun JsonObject.unwrap(): Any? { + return when(this.getString(PROP_TYPE)) { + TYPE_BOOLEAN -> this.getBoolean(PROP_VALUE) + TYPE_INT -> this.getInt(PROP_VALUE) + TYPE_DOUBLE -> this.getDouble(PROP_VALUE) + TYPE_STRING -> this.getString(PROP_VALUE) + TYPE_LONG -> this.getLong(PROP_VALUE) + else -> { + this[PROP_VALUE]?.let { + engine?.await { + JsonElementConverter.write(it, context) + } + } + } + } + } + + companion object { + const val PROP_TYPE = "type" + const val PROP_VALUE = "value" + const val TYPE_BOOLEAN = "boolean" + const val TYPE_INT = "int" + const val TYPE_DOUBLE = "double" + const val TYPE_STRING = "string" + const val TYPE_LONG = "long" + const val TYPE_OBJECT = "object" + const val TYPE_ARRAY = "array" + } +} \ No newline at end of file diff --git a/analytics-kotlin-live/src/main/java/com/segment/analytics/liveplugins/kotlin/LivePlugins.kt b/analytics-kotlin-live/src/main/java/com/segment/analytics/liveplugins/kotlin/LivePlugins.kt index 4157bd9..d1e8588 100644 --- a/analytics-kotlin-live/src/main/java/com/segment/analytics/liveplugins/kotlin/LivePlugins.kt +++ b/analytics-kotlin-live/src/main/java/com/segment/analytics/liveplugins/kotlin/LivePlugins.kt @@ -123,6 +123,8 @@ class LivePlugins( private fun configureEngine() = engine.sync { val jsAnalytics = JSAnalytics(analytics, engine) export(jsAnalytics, "Analytics","analytics") + val jsStorage = JSStorage(sharedPreferences, engine) + export(jsStorage, "Storage", "storage") evaluate(EmbeddedJS.ENUM_SETUP_SCRIPT) evaluate(EmbeddedJS.LIVE_PLUGINS_BASE_SETUP_SCRIPT) From 829c00615ce6f239c8595334af8b9ac8c738d0bd Mon Sep 17 00:00:00 2001 From: Wenxi Zeng Date: Wed, 5 Nov 2025 13:11:42 -0600 Subject: [PATCH 2/9] add unit test --- .../liveplugins/kotlin/JSStorageTest.kt | 55 +++++++++++++++++++ .../analytics/liveplugins/kotlin/JSStorage.kt | 2 +- 2 files changed, 56 insertions(+), 1 deletion(-) create mode 100644 analytics-kotlin-live/src/androidTest/java/com/segment/analytics/liveplugins/kotlin/JSStorageTest.kt diff --git a/analytics-kotlin-live/src/androidTest/java/com/segment/analytics/liveplugins/kotlin/JSStorageTest.kt b/analytics-kotlin-live/src/androidTest/java/com/segment/analytics/liveplugins/kotlin/JSStorageTest.kt new file mode 100644 index 0000000..a82d865 --- /dev/null +++ b/analytics-kotlin-live/src/androidTest/java/com/segment/analytics/liveplugins/kotlin/JSStorageTest.kt @@ -0,0 +1,55 @@ +package com.segment.analytics.liveplugins.kotlin + +import androidx.test.ext.junit.runners.AndroidJUnit4 +import com.segment.analytics.liveplugins.kotlin.utils.MemorySharedPreferences +import com.segment.analytics.substrata.kotlin.JSScope +import kotlinx.coroutines.ExperimentalCoroutinesApi +import org.junit.Assert.assertEquals +import org.junit.Assert.assertNull +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith + + +@OptIn(ExperimentalCoroutinesApi::class) +@RunWith(AndroidJUnit4::class) +class JSStorageTest { + + private lateinit var engine: JSScope + private lateinit var jsStorage: JSStorage + private var exceptionThrown: Throwable? = null + + @Before + fun setUp() { + exceptionThrown = null + + engine = JSScope{ exception -> + exceptionThrown = exception + } + jsStorage = JSStorage(MemorySharedPreferences(), engine) + // Setup the engine similar to LivePlugins.configureEngine + engine.sync { + export(jsStorage, "Storage", "storage") + } + } + + @Test + fun testJSStorageWithInt() { + // set from js + var value = engine.await { + evaluate("""storage.setValue("int", 1)""") + evaluate("""storage.getValue("int")""") + } + assertNull(exceptionThrown) + assertEquals(1, value) + assertEquals(1, jsStorage.getValue("int")) + + // set from native + jsStorage.setValue("int", 2) + value = engine.await { + evaluate("""storage.getValue("int")""") + } + assertEquals(2, value) + assertEquals(2, jsStorage.getValue("int")) + } +} \ No newline at end of file diff --git a/analytics-kotlin-live/src/main/java/com/segment/analytics/liveplugins/kotlin/JSStorage.kt b/analytics-kotlin-live/src/main/java/com/segment/analytics/liveplugins/kotlin/JSStorage.kt index a04c3bb..a0a42bc 100644 --- a/analytics-kotlin-live/src/main/java/com/segment/analytics/liveplugins/kotlin/JSStorage.kt +++ b/analytics-kotlin-live/src/main/java/com/segment/analytics/liveplugins/kotlin/JSStorage.kt @@ -65,7 +65,7 @@ class JSStorage { } } - private fun save(key: String, value: Any, type: String) { + private inline fun save(key: String, value: T, type: String) { val jsonObject = buildJsonObject { put(PROP_TYPE, type) put(PROP_VALUE, Json.encodeToString(value)) From 4527d9202fb7065cefb99158d23d663a3097f7d2 Mon Sep 17 00:00:00 2001 From: Wenxi Zeng Date: Wed, 5 Nov 2025 13:18:52 -0600 Subject: [PATCH 3/9] add unit test for other types --- .../liveplugins/kotlin/JSStorageTest.kt | 136 ++++++++++++++++++ 1 file changed, 136 insertions(+) diff --git a/analytics-kotlin-live/src/androidTest/java/com/segment/analytics/liveplugins/kotlin/JSStorageTest.kt b/analytics-kotlin-live/src/androidTest/java/com/segment/analytics/liveplugins/kotlin/JSStorageTest.kt index a82d865..2705fb5 100644 --- a/analytics-kotlin-live/src/androidTest/java/com/segment/analytics/liveplugins/kotlin/JSStorageTest.kt +++ b/analytics-kotlin-live/src/androidTest/java/com/segment/analytics/liveplugins/kotlin/JSStorageTest.kt @@ -2,6 +2,8 @@ package com.segment.analytics.liveplugins.kotlin import androidx.test.ext.junit.runners.AndroidJUnit4 import com.segment.analytics.liveplugins.kotlin.utils.MemorySharedPreferences +import com.segment.analytics.substrata.kotlin.JSArray +import com.segment.analytics.substrata.kotlin.JSObject import com.segment.analytics.substrata.kotlin.JSScope import kotlinx.coroutines.ExperimentalCoroutinesApi import org.junit.Assert.assertEquals @@ -52,4 +54,138 @@ class JSStorageTest { assertEquals(2, value) assertEquals(2, jsStorage.getValue("int")) } + + @Test + fun testJSStorageWithBoolean() { + // set from js + var value = engine.await { + evaluate("""storage.setValue("boolean", true)""") + evaluate("""storage.getValue("boolean")""") + } + assertNull(exceptionThrown) + assertEquals(true, value) + assertEquals(true, jsStorage.getValue("boolean")) + + // set from native + jsStorage.setValue("boolean", false) + value = engine.await { + evaluate("""storage.getValue("boolean")""") + } + assertEquals(false, value) + assertEquals(false, jsStorage.getValue("boolean")) + } + + @Test + fun testJSStorageWithDouble() { + // set from js + var value = engine.await { + evaluate("""storage.setValue("double", 3.14)""") + evaluate("""storage.getValue("double")""") + } + assertNull(exceptionThrown) + assertEquals(3.14, value) + assertEquals(3.14, jsStorage.getValue("double")) + + // set from native + jsStorage.setValue("double", 2.71) + value = engine.await { + evaluate("""storage.getValue("double")""") + } + assertEquals(2.71, value) + assertEquals(2.71, jsStorage.getValue("double")) + } + + @Test + fun testJSStorageWithString() { + // set from js + var value = engine.await { + evaluate("""storage.setValue("string", "hello")""") + evaluate("""storage.getValue("string")""") + } + assertNull(exceptionThrown) + assertEquals("hello", value) + assertEquals("hello", jsStorage.getValue("string")) + + // set from native + jsStorage.setValue("string", "world") + value = engine.await { + evaluate("""storage.getValue("string")""") + } + assertEquals("world", value) + assertEquals("world", jsStorage.getValue("string")) + } + + @Test + fun testJSStorageWithLong() { + // set from js + var value = engine.await { + evaluate("""storage.setValue("long", 1234567890123)""") + evaluate("""storage.getValue("long")""") + } + assertNull(exceptionThrown) + assertEquals(1234567890123L, value) + assertEquals(1234567890123L, jsStorage.getValue("long")) + + // set from native + jsStorage.setValue("long", 9876543210987L) + value = engine.await { + evaluate("""storage.getValue("long")""") + } + assertEquals(9876543210987L, value) + assertEquals(9876543210987L, jsStorage.getValue("long")) + } + + @Test + fun testJSStorageWithJSObject() { + // set from js + var value = engine.await { + evaluate("""storage.setValue("object", {name: "test", value: 42})""") + evaluate("""storage.getValue("object")""") + } + assertNull(exceptionThrown) + val jsObjectValue = value as JSObject + assertEquals("test", jsObjectValue.getString("name")) + assertEquals(42, jsObjectValue.getInt("value")) + + // set from native + val nativeObject = engine.await { + evaluate("""{name: "native", value: 100}""") + } as JSObject + jsStorage.setValue("object", nativeObject) + value = engine.await { + evaluate("""storage.getValue("object")""") + } + val retrievedObject = value as JSObject + assertEquals("native", retrievedObject.getString("name")) + assertEquals(100, retrievedObject.getInt("value")) + } + + @Test + fun testJSStorageWithJSArray() { + // set from js + var value = engine.await { + evaluate("""storage.setValue("array", [1, "test", true])""") + evaluate("""storage.getValue("array")""") + } + assertNull(exceptionThrown) + val jsArrayValue = value as JSArray + assertEquals(3, jsArrayValue.size) + assertEquals(1, jsArrayValue.getInt(0)) + assertEquals("test", jsArrayValue.getString(1)) + assertEquals(true, jsArrayValue.getBoolean(2)) + + // set from native + val nativeArray = engine.await { + evaluate("""[42, "native", false]""") + } as JSArray + jsStorage.setValue("array", nativeArray) + value = engine.await { + evaluate("""storage.getValue("array")""") + } + val retrievedArray = value as JSArray + assertEquals(3, retrievedArray.size) + assertEquals(42, retrievedArray.getInt(0)) + assertEquals("native", retrievedArray.getString(1)) + assertEquals(false, retrievedArray.getBoolean(2)) + } } \ No newline at end of file From 633488e893e49f63fb2034009aac503d988e3100 Mon Sep 17 00:00:00 2001 From: Wenxi Zeng Date: Wed, 5 Nov 2025 14:21:17 -0600 Subject: [PATCH 4/9] fix string double quote issue --- .../java/com/segment/analytics/liveplugins/kotlin/JSStorage.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/analytics-kotlin-live/src/main/java/com/segment/analytics/liveplugins/kotlin/JSStorage.kt b/analytics-kotlin-live/src/main/java/com/segment/analytics/liveplugins/kotlin/JSStorage.kt index a0a42bc..929800d 100644 --- a/analytics-kotlin-live/src/main/java/com/segment/analytics/liveplugins/kotlin/JSStorage.kt +++ b/analytics-kotlin-live/src/main/java/com/segment/analytics/liveplugins/kotlin/JSStorage.kt @@ -79,7 +79,7 @@ class JSStorage { TYPE_BOOLEAN -> this.getBoolean(PROP_VALUE) TYPE_INT -> this.getInt(PROP_VALUE) TYPE_DOUBLE -> this.getDouble(PROP_VALUE) - TYPE_STRING -> this.getString(PROP_VALUE) + TYPE_STRING -> this.getString(PROP_VALUE)?.let { Json.decodeFromString(it) } TYPE_LONG -> this.getLong(PROP_VALUE) else -> { this[PROP_VALUE]?.let { From 2850cadc8b239e93443a74c4bafac9540e33c0d1 Mon Sep 17 00:00:00 2001 From: Wenxi Zeng Date: Wed, 5 Nov 2025 14:29:31 -0600 Subject: [PATCH 5/9] fix long conversion issue --- .../segment/analytics/liveplugins/kotlin/JSStorageTest.kt | 8 ++++---- .../com/segment/analytics/liveplugins/kotlin/JSStorage.kt | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/analytics-kotlin-live/src/androidTest/java/com/segment/analytics/liveplugins/kotlin/JSStorageTest.kt b/analytics-kotlin-live/src/androidTest/java/com/segment/analytics/liveplugins/kotlin/JSStorageTest.kt index 2705fb5..c3750d5 100644 --- a/analytics-kotlin-live/src/androidTest/java/com/segment/analytics/liveplugins/kotlin/JSStorageTest.kt +++ b/analytics-kotlin-live/src/androidTest/java/com/segment/analytics/liveplugins/kotlin/JSStorageTest.kt @@ -123,16 +123,16 @@ class JSStorageTest { evaluate("""storage.getValue("long")""") } assertNull(exceptionThrown) - assertEquals(1234567890123L, value) - assertEquals(1234567890123L, jsStorage.getValue("long")) + assertEquals(1234567890123L.toDouble(), value) + assertEquals(1234567890123L.toDouble(), jsStorage.getValue("long")) // set from native jsStorage.setValue("long", 9876543210987L) value = engine.await { evaluate("""storage.getValue("long")""") } - assertEquals(9876543210987L, value) - assertEquals(9876543210987L, jsStorage.getValue("long")) + assertEquals(9876543210987L.toDouble(), value) + assertEquals(9876543210987L.toDouble(), jsStorage.getValue("long")) } @Test diff --git a/analytics-kotlin-live/src/main/java/com/segment/analytics/liveplugins/kotlin/JSStorage.kt b/analytics-kotlin-live/src/main/java/com/segment/analytics/liveplugins/kotlin/JSStorage.kt index 929800d..2beb115 100644 --- a/analytics-kotlin-live/src/main/java/com/segment/analytics/liveplugins/kotlin/JSStorage.kt +++ b/analytics-kotlin-live/src/main/java/com/segment/analytics/liveplugins/kotlin/JSStorage.kt @@ -80,7 +80,7 @@ class JSStorage { TYPE_INT -> this.getInt(PROP_VALUE) TYPE_DOUBLE -> this.getDouble(PROP_VALUE) TYPE_STRING -> this.getString(PROP_VALUE)?.let { Json.decodeFromString(it) } - TYPE_LONG -> this.getLong(PROP_VALUE) + TYPE_LONG -> this.getLong(PROP_VALUE)?.toDouble() else -> { this[PROP_VALUE]?.let { engine?.await { From 37ecb4f603eb67551ab81617f270772dc3adcfaf Mon Sep 17 00:00:00 2001 From: Wenxi Zeng Date: Wed, 5 Nov 2025 15:35:45 -0600 Subject: [PATCH 6/9] fix jsObject conversion issue --- .../liveplugins/kotlin/JSStorageTest.kt | 32 +++++++++++++------ .../analytics/liveplugins/kotlin/JSStorage.kt | 20 +++++++++--- 2 files changed, 37 insertions(+), 15 deletions(-) diff --git a/analytics-kotlin-live/src/androidTest/java/com/segment/analytics/liveplugins/kotlin/JSStorageTest.kt b/analytics-kotlin-live/src/androidTest/java/com/segment/analytics/liveplugins/kotlin/JSStorageTest.kt index c3750d5..d62e9a5 100644 --- a/analytics-kotlin-live/src/androidTest/java/com/segment/analytics/liveplugins/kotlin/JSStorageTest.kt +++ b/analytics-kotlin-live/src/androidTest/java/com/segment/analytics/liveplugins/kotlin/JSStorageTest.kt @@ -1,11 +1,15 @@ package com.segment.analytics.liveplugins.kotlin import androidx.test.ext.junit.runners.AndroidJUnit4 +import com.segment.analytics.kotlin.core.utilities.getInt +import com.segment.analytics.kotlin.core.utilities.getString import com.segment.analytics.liveplugins.kotlin.utils.MemorySharedPreferences import com.segment.analytics.substrata.kotlin.JSArray import com.segment.analytics.substrata.kotlin.JSObject import com.segment.analytics.substrata.kotlin.JSScope +import com.segment.analytics.substrata.kotlin.JsonElementConverter import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.serialization.json.jsonObject import org.junit.Assert.assertEquals import org.junit.Assert.assertNull import org.junit.Before @@ -138,24 +142,32 @@ class JSStorageTest { @Test fun testJSStorageWithJSObject() { // set from js - var value = engine.await { + var value = engine.await(true) { evaluate("""storage.setValue("object", {name: "test", value: 42})""") evaluate("""storage.getValue("object")""") } assertNull(exceptionThrown) - val jsObjectValue = value as JSObject - assertEquals("test", jsObjectValue.getString("name")) - assertEquals(42, jsObjectValue.getInt("value")) + val jsonObject = JsonElementConverter.read(value).jsonObject + assertEquals("test", jsonObject.getString("name")) + assertEquals(42, jsonObject.getInt("value")) // set from native - val nativeObject = engine.await { - evaluate("""{name: "native", value: 100}""") - } as JSObject - jsStorage.setValue("object", nativeObject) + val nativeObject = engine.await(true) { + evaluate(""" + let obj = {name: "native", value: 100} + obj + """.trimIndent()) + } + jsStorage.setValue("object", nativeObject as JSObject) value = engine.await { - evaluate("""storage.getValue("object")""") + evaluate(""" + let obj2 = storage.getValue("object") + obj.name == obj2.name && obj.value == obj2.value + """.trimIndent()) } - val retrievedObject = value as JSObject + assertEquals(true, value) + val jsValue = jsStorage.getValue("object") + val retrievedObject = JsonElementConverter.read(jsValue).jsonObject assertEquals("native", retrievedObject.getString("name")) assertEquals(100, retrievedObject.getInt("value")) } diff --git a/analytics-kotlin-live/src/main/java/com/segment/analytics/liveplugins/kotlin/JSStorage.kt b/analytics-kotlin-live/src/main/java/com/segment/analytics/liveplugins/kotlin/JSStorage.kt index 2beb115..09dd801 100644 --- a/analytics-kotlin-live/src/main/java/com/segment/analytics/liveplugins/kotlin/JSStorage.kt +++ b/analytics-kotlin-live/src/main/java/com/segment/analytics/liveplugins/kotlin/JSStorage.kt @@ -13,6 +13,7 @@ import com.segment.analytics.substrata.kotlin.JSScope import com.segment.analytics.substrata.kotlin.JsonElementConverter import kotlinx.serialization.encodeToString import kotlinx.serialization.json.Json +import kotlinx.serialization.json.JsonElement import kotlinx.serialization.json.JsonObject import kotlinx.serialization.json.buildJsonObject import kotlinx.serialization.json.put @@ -52,11 +53,19 @@ class JSStorage { } fun setValue(key: String, value: JSObject) { - save(key, value, TYPE_OBJECT) + save( + key, + JsonElementConverter.read(value), + TYPE_OBJECT + ) } fun setValue(key: String, value: JSArray) { - save(key, value, TYPE_ARRAY) + save( + key, + JsonElementConverter.read(value), + TYPE_ARRAY + ) } fun getValue(key: String): Any? { @@ -82,9 +91,10 @@ class JSStorage { TYPE_STRING -> this.getString(PROP_VALUE)?.let { Json.decodeFromString(it) } TYPE_LONG -> this.getLong(PROP_VALUE)?.toDouble() else -> { - this[PROP_VALUE]?.let { - engine?.await { - JsonElementConverter.write(it, context) + this.getString(PROP_VALUE)?.let { + val json = Json.decodeFromString(it) + engine?.await(true) { + JsonElementConverter.write(json, context) } } } From 733b9a7502b7cd83c4afe8e3dc45acbbbcdaa606 Mon Sep 17 00:00:00 2001 From: Wenxi Zeng Date: Wed, 5 Nov 2025 15:39:33 -0600 Subject: [PATCH 7/9] fix jsArray conversion issue --- .../liveplugins/kotlin/JSStorageTest.kt | 40 ++++++++++++------- 1 file changed, 25 insertions(+), 15 deletions(-) diff --git a/analytics-kotlin-live/src/androidTest/java/com/segment/analytics/liveplugins/kotlin/JSStorageTest.kt b/analytics-kotlin-live/src/androidTest/java/com/segment/analytics/liveplugins/kotlin/JSStorageTest.kt index d62e9a5..5843972 100644 --- a/analytics-kotlin-live/src/androidTest/java/com/segment/analytics/liveplugins/kotlin/JSStorageTest.kt +++ b/analytics-kotlin-live/src/androidTest/java/com/segment/analytics/liveplugins/kotlin/JSStorageTest.kt @@ -9,7 +9,9 @@ import com.segment.analytics.substrata.kotlin.JSObject import com.segment.analytics.substrata.kotlin.JSScope import com.segment.analytics.substrata.kotlin.JsonElementConverter import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.serialization.json.jsonArray import kotlinx.serialization.json.jsonObject +import kotlinx.serialization.json.jsonPrimitive import org.junit.Assert.assertEquals import org.junit.Assert.assertNull import org.junit.Before @@ -175,29 +177,37 @@ class JSStorageTest { @Test fun testJSStorageWithJSArray() { // set from js - var value = engine.await { + var value = engine.await(true) { evaluate("""storage.setValue("array", [1, "test", true])""") evaluate("""storage.getValue("array")""") } assertNull(exceptionThrown) - val jsArrayValue = value as JSArray - assertEquals(3, jsArrayValue.size) - assertEquals(1, jsArrayValue.getInt(0)) - assertEquals("test", jsArrayValue.getString(1)) - assertEquals(true, jsArrayValue.getBoolean(2)) + val jsonArray = JsonElementConverter.read(value).jsonArray + assertEquals(3, jsonArray.size) + assertEquals(1, jsonArray[0].jsonPrimitive.content.toInt()) + assertEquals("test", jsonArray[1].jsonPrimitive.content) + assertEquals(true, jsonArray[2].jsonPrimitive.content.toBoolean()) // set from native - val nativeArray = engine.await { - evaluate("""[42, "native", false]""") - } as JSArray - jsStorage.setValue("array", nativeArray) + val nativeArray = engine.await(true) { + evaluate(""" + let arr = [42, "native", false] + arr + """.trimIndent()) + } + jsStorage.setValue("array", nativeArray as JSArray) value = engine.await { - evaluate("""storage.getValue("array")""") + evaluate(""" + let arr2 = storage.getValue("array") + arr.length == arr2.length && arr[0] == arr2[0] && arr[1] == arr2[1] && arr[2] == arr2[2] + """.trimIndent()) } - val retrievedArray = value as JSArray + assertEquals(true, value) + val jsValue = jsStorage.getValue("array") + val retrievedArray = JsonElementConverter.read(jsValue).jsonArray assertEquals(3, retrievedArray.size) - assertEquals(42, retrievedArray.getInt(0)) - assertEquals("native", retrievedArray.getString(1)) - assertEquals(false, retrievedArray.getBoolean(2)) + assertEquals(42, retrievedArray[0].jsonPrimitive.content.toInt()) + assertEquals("native", retrievedArray[1].jsonPrimitive.content) + assertEquals(false, retrievedArray[2].jsonPrimitive.content.toBoolean()) } } \ No newline at end of file From 11091356b60e23704390e8c1d32361b50d88f1a2 Mon Sep 17 00:00:00 2001 From: Wenxi Zeng Date: Wed, 5 Nov 2025 15:41:24 -0600 Subject: [PATCH 8/9] add removeValue --- .../com/segment/analytics/liveplugins/kotlin/JSStorage.kt | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/analytics-kotlin-live/src/main/java/com/segment/analytics/liveplugins/kotlin/JSStorage.kt b/analytics-kotlin-live/src/main/java/com/segment/analytics/liveplugins/kotlin/JSStorage.kt index 09dd801..783ea21 100644 --- a/analytics-kotlin-live/src/main/java/com/segment/analytics/liveplugins/kotlin/JSStorage.kt +++ b/analytics-kotlin-live/src/main/java/com/segment/analytics/liveplugins/kotlin/JSStorage.kt @@ -74,6 +74,10 @@ class JSStorage { } } + fun removeValue(key: String) { + this.sharedPreferences?.edit(commit = true) { remove(key) } + } + private inline fun save(key: String, value: T, type: String) { val jsonObject = buildJsonObject { put(PROP_TYPE, type) From e1816567bd34925b66a208f95d2b60e084108fed Mon Sep 17 00:00:00 2001 From: Wenxi Zeng Date: Wed, 5 Nov 2025 15:50:02 -0600 Subject: [PATCH 9/9] add unit test for remove value --- .../liveplugins/kotlin/JSStorageTest.kt | 49 +++++++++++++++++++ 1 file changed, 49 insertions(+) diff --git a/analytics-kotlin-live/src/androidTest/java/com/segment/analytics/liveplugins/kotlin/JSStorageTest.kt b/analytics-kotlin-live/src/androidTest/java/com/segment/analytics/liveplugins/kotlin/JSStorageTest.kt index 5843972..c4d9f7b 100644 --- a/analytics-kotlin-live/src/androidTest/java/com/segment/analytics/liveplugins/kotlin/JSStorageTest.kt +++ b/analytics-kotlin-live/src/androidTest/java/com/segment/analytics/liveplugins/kotlin/JSStorageTest.kt @@ -210,4 +210,53 @@ class JSStorageTest { assertEquals("native", retrievedArray[1].jsonPrimitive.content) assertEquals(false, retrievedArray[2].jsonPrimitive.content.toBoolean()) } + + @Test + fun testJSStorageRemoveValue() { + // 1. set from js and remove from js + engine.sync { + evaluate("""storage.setValue("jsJs", "value1")""") + evaluate("""storage.removeValue("jsJs")""") + } + assertNull(exceptionThrown) + assertNull(jsStorage.getValue("jsJs")) + val jsJsValue = engine.await(true) { + evaluate("""storage.getValue("jsJs")""") + } + assertNull(jsJsValue) + + // 2. set from native and remove from native + jsStorage.setValue("nativeNative", "value2") + assertEquals("value2", jsStorage.getValue("nativeNative")) + jsStorage.removeValue("nativeNative") + assertNull(jsStorage.getValue("nativeNative")) + val nativeNativeValue = engine.await { + evaluate("""storage.getValue("nativeNative")""") + } + assertNull(nativeNativeValue) + + // 3. set from js and remove from native + engine.sync { + evaluate("""storage.setValue("jsNative", "value3")""") + } + assertEquals("value3", jsStorage.getValue("jsNative")) + jsStorage.removeValue("jsNative") + assertNull(jsStorage.getValue("jsNative")) + val jsNativeValue = engine.await(true) { + evaluate("""storage.getValue("jsNative")""") + } + assertNull(jsNativeValue) + + // 4. set from native and remove from js + jsStorage.setValue("nativeJs", "value4") + assertEquals("value4", jsStorage.getValue("nativeJs")) + engine.sync { + evaluate("""storage.removeValue("nativeJs")""") + } + assertNull(jsStorage.getValue("nativeJs")) + val nativeJsValue = engine.await(true) { + evaluate("""storage.getValue("nativeJs")""") + } + assertNull(nativeJsValue) + } } \ No newline at end of file