-
Notifications
You must be signed in to change notification settings - Fork 53
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Workaround bug in android 4 for JSON objects with List<String> (#942)
Fixes https://linear.app/revenuecat/issue/SDK-3052/400-from-post-receipts-because-product-did-not-have-quotes https://revenuecats.atlassian.net/browse/CSDK-681 Works around the following bug in android 4: https://stackoverflow.com/q/37317669 http://fupeg.blogspot.com/2011/07/android-json-bug.html Essentially, this bug means that when you get a list of product_ids, instead of sending a list of strings, it sends a list with a string in it. | Before | After | | :-: | :-: | | "product_ids": "[item1, item2]" | "product_ids": ["item1, "item2"] |
- Loading branch information
Showing
3 changed files
with
151 additions
and
27 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
57 changes: 57 additions & 0 deletions
57
common/src/main/java/com/revenuecat/purchases/common/networking/MapConverter.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,57 @@ | ||
package com.revenuecat.purchases.common.networking | ||
|
||
import org.json.JSONArray | ||
import org.json.JSONObject | ||
|
||
/** | ||
* A class to convert a Map<String, Any?> into a JSONObject. | ||
* This was created to workaround a bug in Android 4 , where a List<String> would be incorrectly converted into | ||
* a single string instead of a JSONArray of strings. (i.e.: "[\"value1\", \"value2\"]" instead of "[value1, value2]") | ||
* This class handles nested maps, lists, and other JSON-compatible types. | ||
*/ | ||
class MapConverter { | ||
|
||
/** | ||
* Converts the given [inputMap] into a JSONObject. | ||
* | ||
* @param inputMap The input map to convert. | ||
* @return A JSONObject representing the input map. | ||
*/ | ||
internal fun convertToJSON(inputMap: Map<String, Any?>): JSONObject { | ||
val mapWithoutInnerMaps = inputMap.mapValues { (_, value) -> | ||
when (value) { | ||
is List<*> -> { | ||
if (value.all { it is String }) { | ||
JSONObject(mapOf("temp_key" to JSONArray(value))).getJSONArray("temp_key") | ||
} else { | ||
value | ||
} | ||
} | ||
else -> value.tryCast<Map<String, Any?>>(ifSuccess = { convertToJSON(this) }) | ||
} | ||
} | ||
return createJSONObject(mapWithoutInnerMaps) | ||
} | ||
|
||
internal fun createJSONObject(inputMap: Map<String, Any?>): JSONObject { | ||
return JSONObject(inputMap) | ||
} | ||
|
||
/** To avoid Java type erasure, we use a Kotlin inline function with a reified parameter | ||
* so that we can check the type on runtime. | ||
* | ||
* Doing something like: | ||
* if (value is Map<*, *>) (value as Map<String, Any?>).convert() | ||
* | ||
* Would give an unchecked cast warning due to Java type erasure | ||
*/ | ||
private inline fun <reified T> Any?.tryCast( | ||
ifSuccess: T.() -> Any? | ||
): Any? { | ||
return if (this is T) { | ||
this.ifSuccess() | ||
} else { | ||
this | ||
} | ||
} | ||
} |
90 changes: 90 additions & 0 deletions
90
common/src/test/java/com/revenuecat/purchases/common/networking/MapConverterTest.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,90 @@ | ||
package com.revenuecat.purchases.common.networking | ||
|
||
import androidx.test.ext.junit.runners.AndroidJUnit4 | ||
import io.mockk.every | ||
import io.mockk.spyk | ||
import org.json.JSONArray | ||
import org.json.JSONObject | ||
import org.junit.Assert.assertEquals | ||
import org.junit.Before | ||
import org.junit.Test | ||
import org.junit.runner.RunWith | ||
import org.robolectric.annotation.Config | ||
|
||
@RunWith(AndroidJUnit4::class) | ||
@Config(manifest = Config.NONE) | ||
class MapConverterTest { | ||
|
||
private lateinit var mapConverter: MapConverter | ||
|
||
@Before | ||
fun setUp() { | ||
mapConverter = MapConverter() | ||
} | ||
|
||
@Test | ||
fun `test convert to JSON`() { | ||
val inputMap = mapOf( | ||
"key1" to "value1", | ||
"key2" to listOf("value2", "value3"), | ||
"key3" to mapOf("nestedKey" to "nestedValue") | ||
) | ||
|
||
val expectedJson = JSONObject() | ||
.put("key1", "value1") | ||
.put("key2", JSONArray(listOf("value2", "value3"))) | ||
.put("key3", JSONObject().put("nestedKey", "nestedValue")) | ||
|
||
val result = mapConverter.convertToJSON(inputMap) | ||
assertEquals(expectedJson.toString(), result.toString()) | ||
} | ||
|
||
@Test | ||
fun `test convert to JSON with nested array of strings`() { | ||
val inputMap = mapOf( | ||
"key1" to "value1", | ||
"key2" to listOf("value2", "value3"), | ||
"key3" to mapOf("nestedKey" to "nestedValue"), | ||
"key4" to mapOf("nestedArray" to listOf("value4", "value5")), | ||
) | ||
|
||
val expectedJson = JSONObject() | ||
.put("key1", "value1") | ||
.put("key2", JSONArray(listOf("value2", "value3"))) | ||
.put("key3", JSONObject().put("nestedKey", "nestedValue")) | ||
.put("key4", JSONObject().put("nestedArray", JSONArray(listOf("value4", "value5")))) | ||
|
||
val result = mapConverter.convertToJSON(inputMap) | ||
assertEquals(expectedJson.toString(), result.toString()) | ||
} | ||
|
||
/** | ||
* This tests workaround for a bug in Android 4 , where a List<String> would be incorrectly converted into | ||
* a single string instead of a JSONArray of strings. | ||
* (i.e.: "[\"value1\", \"value2\"]" instead of "[value1, value2]") | ||
*/ | ||
@Test | ||
fun `test map conversion fixes wrong treatment of arrays of strings in JSON library`() { | ||
val mapConverterPartialMock = spyk<MapConverter>() | ||
|
||
val inputMap = mapOf( | ||
"product_ids" to listOf("product_1", "product_2") | ||
) | ||
|
||
val mapContainingInputMap = mapOf( | ||
"subscriber_info" to inputMap | ||
) | ||
|
||
val incorrectJsonArrayString = "[product_1,product_2]" | ||
val correctedJSONArray = JSONArray(listOf("product_1", "product_2")) | ||
|
||
every { | ||
mapConverterPartialMock.createJSONObject(match { it == inputMap }) | ||
} returns JSONObject(mapOf("product_ids" to incorrectJsonArrayString)) | ||
|
||
val resultJson = mapConverterPartialMock.convertToJSON(mapContainingInputMap) | ||
val resultArrayString = resultJson.optJSONObject("subscriber_info")?.optJSONArray("product_ids") | ||
|
||
assertEquals(correctedJSONArray, resultArrayString) | ||
} | ||
} |