From f1fb3b6db2aca712c96ed2904ef098577f78b021 Mon Sep 17 00:00:00 2001
From: maureenorea-clores
<93700127+maureenorea-clores@users.noreply.github.com>
Date: Tue, 24 Sep 2024 17:24:46 +0900
Subject: [PATCH] feat: support modal background opacity through CustomJson
(RMCCX-7327)
---
dependency_check_suppressions.xml | 17 ++++++
inappmessaging/USERGUIDE.md | 1 +
.../data/customjson/ApplyClickableImage.kt | 7 +--
.../runtime/data/customjson/CustomJson.kt | 11 ++++
.../data/customjson/CustomJsonDeserializer.kt | 31 +++++++++++
.../runtime/data/customjson/MessageMapper.kt | 6 +-
.../runtime/data/responses/ping/Message.kt | 13 +++--
.../runtime/data/ui/UiMessage.kt | 1 +
.../runtime/view/InAppMessageModalView.kt | 19 +++++++
.../customjson/ApplyClickableImageSpec.kt | 2 +-
.../data/customjson/MessageMapperSpec.kt | 44 +++++++++++++++
.../data/responses/ping/MessageSpec.kt | 13 ++++-
.../runtime/view/InAppMessageModalViewSpec.kt | 55 +++++++++++++++++--
13 files changed, 201 insertions(+), 19 deletions(-)
create mode 100644 inappmessaging/src/main/java/com/rakuten/tech/mobile/inappmessaging/runtime/data/customjson/CustomJsonDeserializer.kt
diff --git a/dependency_check_suppressions.xml b/dependency_check_suppressions.xml
index f187fca9..10bad108 100755
--- a/dependency_check_suppressions.xml
+++ b/dependency_check_suppressions.xml
@@ -29,6 +29,7 @@
CVE-2021-22569
CVE-2022-3171
CVE-2022-3509
+ CVE-2024-7254
CVE-2022-3171
CVE-2022-3510
+
+
+ ^pkg:maven/com\.google\.protobuf/protobuf\-javalite@.*$
+ CVE-2024-7254
+
+
+
+
+
+ ^pkg:maven/com\.google\.protobuf/protobuf\-javalite@.*$
+ CVE-2024-7254
+
\ No newline at end of file
diff --git a/inappmessaging/USERGUIDE.md b/inappmessaging/USERGUIDE.md
index e5b457b0..f258a835 100644
--- a/inappmessaging/USERGUIDE.md
+++ b/inappmessaging/USERGUIDE.md
@@ -501,6 +501,7 @@ All the events "launch the app event, login event, purchase successful event, cu
- RMCCX-6876: Improved console logging.
* RMC SDK updates:
- RMCCX-7186: Supported Clickable Image through CustomJson.
+ - RMCCX-7327: Supported customizable modal campaign background opacity through CustomJson.
#### 7.6.0 (2024-09-17)
* Improvements:
diff --git a/inappmessaging/src/main/java/com/rakuten/tech/mobile/inappmessaging/runtime/data/customjson/ApplyClickableImage.kt b/inappmessaging/src/main/java/com/rakuten/tech/mobile/inappmessaging/runtime/data/customjson/ApplyClickableImage.kt
index a45b974d..b7afbebf 100644
--- a/inappmessaging/src/main/java/com/rakuten/tech/mobile/inappmessaging/runtime/data/customjson/ApplyClickableImage.kt
+++ b/inappmessaging/src/main/java/com/rakuten/tech/mobile/inappmessaging/runtime/data/customjson/ApplyClickableImage.kt
@@ -19,11 +19,8 @@ internal fun UiMessage.applyCustomClickableImage(clickableImage: ClickableImage?
return false
}
- return if (this.startsWith("http")) {
- Regex("https://.*").matches(this)
- } else {
- Regex(".*://.*").matches(this)
- }
+ val allowedPattern = Regex("https://.*|.*://.*")
+ return this.matches(allowedPattern)
}
@SuppressWarnings("ComplexCondition")
diff --git a/inappmessaging/src/main/java/com/rakuten/tech/mobile/inappmessaging/runtime/data/customjson/CustomJson.kt b/inappmessaging/src/main/java/com/rakuten/tech/mobile/inappmessaging/runtime/data/customjson/CustomJson.kt
index 62004350..0409fe15 100644
--- a/inappmessaging/src/main/java/com/rakuten/tech/mobile/inappmessaging/runtime/data/customjson/CustomJson.kt
+++ b/inappmessaging/src/main/java/com/rakuten/tech/mobile/inappmessaging/runtime/data/customjson/CustomJson.kt
@@ -3,6 +3,7 @@ package com.rakuten.tech.mobile.inappmessaging.runtime.data.customjson
internal data class CustomJson(
val pushPrimer: PushPrimer? = null,
val clickableImage: ClickableImage? = null,
+ val background: Background? = null,
)
internal data class PushPrimer(
@@ -18,3 +19,13 @@ internal data class ClickableImage(
*/
val url: String? = null,
)
+
+/**
+ * Backdrop color.
+ */
+internal data class Background(
+ /**
+ * Opacity from 0 (completely transparent) to 1 (completely opaque).
+ */
+ val opacity: Float? = null,
+)
diff --git a/inappmessaging/src/main/java/com/rakuten/tech/mobile/inappmessaging/runtime/data/customjson/CustomJsonDeserializer.kt b/inappmessaging/src/main/java/com/rakuten/tech/mobile/inappmessaging/runtime/data/customjson/CustomJsonDeserializer.kt
new file mode 100644
index 00000000..d1b5b251
--- /dev/null
+++ b/inappmessaging/src/main/java/com/rakuten/tech/mobile/inappmessaging/runtime/data/customjson/CustomJsonDeserializer.kt
@@ -0,0 +1,31 @@
+package com.rakuten.tech.mobile.inappmessaging.runtime.data.customjson
+
+import com.google.gson.JsonDeserializationContext
+import com.google.gson.JsonDeserializer
+import com.google.gson.JsonElement
+import com.rakuten.tech.mobile.inappmessaging.runtime.utils.InAppLogger
+import java.lang.reflect.Type
+
+internal class CustomJsonDeserializer : JsonDeserializer {
+
+ override fun deserialize(json: JsonElement?, typeOfT: Type?, context: JsonDeserializationContext?): CustomJson {
+ val jsonObject = json?.asJsonObject
+
+ val pushPrimer = context?.safeDeserialize(jsonObject?.get("pushPrimer"))
+ val clickableImage = context?.safeDeserialize(jsonObject?.get("clickableImage"))
+ val background = context?.safeDeserialize(jsonObject?.get("background"))
+
+ return CustomJson(pushPrimer, clickableImage, background)
+ }
+
+ @SuppressWarnings("TooGenericExceptionCaught")
+ private inline fun JsonDeserializationContext.safeDeserialize(jsonElement: JsonElement?): T? {
+ return try {
+ this.deserialize(jsonElement, T::class.java)
+ } catch (_: Exception) {
+ InAppLogger("IAM_CustomJsonDeserializer")
+ .warn("Invalid format for ${T::class.java.name.split(".").lastOrNull()}")
+ null
+ }
+ }
+}
diff --git a/inappmessaging/src/main/java/com/rakuten/tech/mobile/inappmessaging/runtime/data/customjson/MessageMapper.kt b/inappmessaging/src/main/java/com/rakuten/tech/mobile/inappmessaging/runtime/data/customjson/MessageMapper.kt
index ffe5fbfc..d91fbe27 100644
--- a/inappmessaging/src/main/java/com/rakuten/tech/mobile/inappmessaging/runtime/data/customjson/MessageMapper.kt
+++ b/inappmessaging/src/main/java/com/rakuten/tech/mobile/inappmessaging/runtime/data/customjson/MessageMapper.kt
@@ -13,6 +13,8 @@ import com.rakuten.tech.mobile.inappmessaging.runtime.data.ui.UiMessage
internal object MessageMapper : Mapper {
override fun mapFrom(from: Message): UiMessage {
+ val customJsonData = from.getCustomJsonData()
+
val uiModel = UiMessage(
id = from.campaignId,
type = from.type,
@@ -28,13 +30,13 @@ internal object MessageMapper : Mapper {
displaySettings = from.messagePayload.messageSettings.displaySettings,
content = from.messagePayload.messageSettings.controlSettings.content,
tooltipData = from.getTooltipConfig(),
+ backdropOpacity = customJsonData?.background?.opacity,
)
- // Apply CustomJson rules if exists
- val customJsonData = from.getCustomJsonData()
return if (customJsonData == null) {
uiModel
} else {
+ // Update any data that exists from main payload to CustomJson data if applicable
uiModel
.applyCustomPushPrimer(customJsonData.pushPrimer)
.applyCustomClickableImage(customJsonData.clickableImage, from.isPushPrimer)
diff --git a/inappmessaging/src/main/java/com/rakuten/tech/mobile/inappmessaging/runtime/data/responses/ping/Message.kt b/inappmessaging/src/main/java/com/rakuten/tech/mobile/inappmessaging/runtime/data/responses/ping/Message.kt
index de2246a8..5e2e6686 100644
--- a/inappmessaging/src/main/java/com/rakuten/tech/mobile/inappmessaging/runtime/data/responses/ping/Message.kt
+++ b/inappmessaging/src/main/java/com/rakuten/tech/mobile/inappmessaging/runtime/data/responses/ping/Message.kt
@@ -1,11 +1,13 @@
package com.rakuten.tech.mobile.inappmessaging.runtime.data.responses.ping
import com.google.gson.Gson
+import com.google.gson.GsonBuilder
import com.google.gson.JsonObject
import com.google.gson.JsonParseException
import com.google.gson.annotations.SerializedName
import com.rakuten.tech.mobile.inappmessaging.runtime.RmcHelper
import com.rakuten.tech.mobile.inappmessaging.runtime.data.customjson.CustomJson
+import com.rakuten.tech.mobile.inappmessaging.runtime.data.customjson.CustomJsonDeserializer
import com.rakuten.tech.mobile.inappmessaging.runtime.data.enums.InAppMessageType
import com.rakuten.tech.mobile.inappmessaging.runtime.data.models.Tooltip
import com.rakuten.tech.mobile.inappmessaging.runtime.utils.InAppLogger
@@ -79,16 +81,19 @@ internal data class Message(
return tooltip
}
+ @SuppressWarnings("TooGenericExceptionCaught")
fun getCustomJsonData(): CustomJson? {
if (!RmcHelper.isRmcIntegrated() || customJson == null || customJson.entrySet().isEmpty()) {
return null
}
if (customJsonData == null) {
try {
- customJsonData = Gson().fromJson(customJson, CustomJson::class.java)
- } catch (je: JsonParseException) {
- InAppLogger(TAG).warn("getCustomJsonData - invalid customJson format")
- InAppLogger(TAG).debug("parse exception: $je")
+ val gson = GsonBuilder()
+ .registerTypeAdapter(CustomJson::class.java, CustomJsonDeserializer())
+ .create()
+ customJsonData = gson.fromJson(customJson, CustomJson::class.java)
+ } catch (_: Exception) {
+ InAppLogger(TAG).debug("getCustomJsonData - invalid customJson format")
}
}
return customJsonData
diff --git a/inappmessaging/src/main/java/com/rakuten/tech/mobile/inappmessaging/runtime/data/ui/UiMessage.kt b/inappmessaging/src/main/java/com/rakuten/tech/mobile/inappmessaging/runtime/data/ui/UiMessage.kt
index 624268db..adcd7449 100644
--- a/inappmessaging/src/main/java/com/rakuten/tech/mobile/inappmessaging/runtime/data/ui/UiMessage.kt
+++ b/inappmessaging/src/main/java/com/rakuten/tech/mobile/inappmessaging/runtime/data/ui/UiMessage.kt
@@ -23,4 +23,5 @@ internal data class UiMessage(
val displaySettings: DisplaySettings,
val content: Content?,
val tooltipData: Tooltip?,
+ val backdropOpacity: Float? = null,
)
diff --git a/inappmessaging/src/main/java/com/rakuten/tech/mobile/inappmessaging/runtime/view/InAppMessageModalView.kt b/inappmessaging/src/main/java/com/rakuten/tech/mobile/inappmessaging/runtime/view/InAppMessageModalView.kt
index b32efff7..ecac7c9c 100644
--- a/inappmessaging/src/main/java/com/rakuten/tech/mobile/inappmessaging/runtime/view/InAppMessageModalView.kt
+++ b/inappmessaging/src/main/java/com/rakuten/tech/mobile/inappmessaging/runtime/view/InAppMessageModalView.kt
@@ -1,9 +1,11 @@
package com.rakuten.tech.mobile.inappmessaging.runtime.view
import android.content.Context
+import android.graphics.Color
import android.util.AttributeSet
import android.widget.LinearLayout
import androidx.annotation.VisibleForTesting
+import androidx.core.graphics.ColorUtils
import com.rakuten.tech.mobile.inappmessaging.runtime.R
import com.rakuten.tech.mobile.inappmessaging.runtime.data.ui.UiMessage
@@ -23,9 +25,26 @@ internal class InAppMessageModalView(
super.populateViewData(uiMessage)
setCloseButton()
+ setBackdropColor(uiMessage.backdropOpacity)
findModalLayout()?.setBackgroundColor(bgColor)
}
@VisibleForTesting
fun findModalLayout(): LinearLayout? = findViewById(R.id.modal)
+
+ private fun setBackdropColor(opacity: Float?) {
+ // The default color(R.color.in_app_message_frame_light) will be set through XML.
+ if (opacity == null ||
+ opacity !in 0f..1f
+ ) {
+ return
+ }
+
+ val blackWithAlpha = ColorUtils.setAlphaComponent(Color.BLACK, (opacity * MAX_COLOR_ALPHA).toInt())
+ this.setBackgroundColor(blackWithAlpha)
+ }
+
+ companion object {
+ const val MAX_COLOR_ALPHA = 255
+ }
}
diff --git a/inappmessaging/src/test/java/com/rakuten/tech/mobile/inappmessaging/runtime/data/customjson/ApplyClickableImageSpec.kt b/inappmessaging/src/test/java/com/rakuten/tech/mobile/inappmessaging/runtime/data/customjson/ApplyClickableImageSpec.kt
index a3ee88c3..4584f8a0 100644
--- a/inappmessaging/src/test/java/com/rakuten/tech/mobile/inappmessaging/runtime/data/customjson/ApplyClickableImageSpec.kt
+++ b/inappmessaging/src/test/java/com/rakuten/tech/mobile/inappmessaging/runtime/data/customjson/ApplyClickableImageSpec.kt
@@ -31,7 +31,7 @@ class ApplyClickableImageSpec {
uiMessage = message.applyCustomClickableImage(ClickableImage("ogle.124dsefsd"), false)
uiMessage shouldBeEqualTo message
- uiMessage = message.applyCustomClickableImage(ClickableImage("http://test.com"), false)
+ uiMessage = message.applyCustomClickableImage(ClickableImage("intent:/invalid/deeplink"), false)
uiMessage shouldBeEqualTo message
uiMessage = message.applyCustomClickableImage(ClickableImage(" myapp://open"), false)
diff --git a/inappmessaging/src/test/java/com/rakuten/tech/mobile/inappmessaging/runtime/data/customjson/MessageMapperSpec.kt b/inappmessaging/src/test/java/com/rakuten/tech/mobile/inappmessaging/runtime/data/customjson/MessageMapperSpec.kt
index 412aaa50..3e06c5f6 100644
--- a/inappmessaging/src/test/java/com/rakuten/tech/mobile/inappmessaging/runtime/data/customjson/MessageMapperSpec.kt
+++ b/inappmessaging/src/test/java/com/rakuten/tech/mobile/inappmessaging/runtime/data/customjson/MessageMapperSpec.kt
@@ -84,4 +84,48 @@ class MessageMapperSpec {
uiMessage.content?.onClick?.action shouldBeEqualTo ButtonActionType.REDIRECT.typeId
uiMessage.content?.onClick?.uri shouldBeEqualTo "https://test.com"
}
+
+ @Test
+ fun `should set opacity to null if opacity attribute does not exist`() {
+ val uiMessage = MessageMapper.mapFrom(
+ TestDataHelper.createDummyMessage(
+ customJson = JsonParser.parseString("""{"background": {}}""").asJsonObject,
+ ),
+ )
+
+ uiMessage.backdropOpacity shouldBeEqualTo null
+ }
+
+ @Test
+ fun `should set opacity to null if opacity attribute is set to null`() {
+ val uiMessage = MessageMapper.mapFrom(
+ TestDataHelper.createDummyMessage(
+ customJson = JsonParser.parseString("""{"background": {"opacity": null}}""").asJsonObject,
+ ),
+ )
+
+ uiMessage.backdropOpacity shouldBeEqualTo null
+ }
+
+ @Test
+ fun `should set opacity to null if opacity attribute is set to non-number`() {
+ val uiMessage = MessageMapper.mapFrom(
+ TestDataHelper.createDummyMessage(
+ customJson = JsonParser.parseString("""{"background": {"opacity": "abcd"}}""").asJsonObject,
+ ),
+ )
+
+ uiMessage.backdropOpacity shouldBeEqualTo null
+ }
+
+ @Test
+ fun `should correctly map valid opacity attribute`() {
+ val uiMessage = MessageMapper.mapFrom(
+ TestDataHelper.createDummyMessage(
+ customJson = JsonParser.parseString("""{"background": { "opacity": 0.6 }}""").asJsonObject,
+ ),
+ )
+
+ uiMessage.backdropOpacity shouldBeEqualTo 0.6f
+ }
}
diff --git a/inappmessaging/src/test/java/com/rakuten/tech/mobile/inappmessaging/runtime/data/responses/ping/MessageSpec.kt b/inappmessaging/src/test/java/com/rakuten/tech/mobile/inappmessaging/runtime/data/responses/ping/MessageSpec.kt
index ca6e5496..5048d1d5 100644
--- a/inappmessaging/src/test/java/com/rakuten/tech/mobile/inappmessaging/runtime/data/responses/ping/MessageSpec.kt
+++ b/inappmessaging/src/test/java/com/rakuten/tech/mobile/inappmessaging/runtime/data/responses/ping/MessageSpec.kt
@@ -2,6 +2,7 @@ package com.rakuten.tech.mobile.inappmessaging.runtime.data.responses.ping
import com.google.gson.JsonParser
import com.rakuten.tech.mobile.inappmessaging.runtime.RmcHelper
+import com.rakuten.tech.mobile.inappmessaging.runtime.data.customjson.Background
import com.rakuten.tech.mobile.inappmessaging.runtime.data.customjson.CustomJson
import com.rakuten.tech.mobile.inappmessaging.runtime.data.customjson.PushPrimer
import com.rakuten.tech.mobile.inappmessaging.runtime.data.enums.InAppMessageType
@@ -310,7 +311,7 @@ class MessageCustomJsonSpec {
val campaign = TestDataHelper.createDummyMessage(
customJson = JsonParser.parseString("""{"pushPrimer": true}""").asJsonObject,
)
- campaign.getCustomJsonData() shouldBeEqualTo null
+ campaign.getCustomJsonData()?.pushPrimer shouldBeEqualTo null
}
@Test
@@ -340,4 +341,14 @@ class MessageCustomJsonSpec {
)
campaign.getCustomJsonData() shouldBeEqualTo CustomJson(pushPrimer = PushPrimer(button = 1))
}
+
+ @Test
+ fun `should map CustomJson data even if there is a feature key with invalid attribute`() {
+ val campaign = TestDataHelper.createDummyMessage(
+ customJson = JsonParser.parseString(
+ """{ "pushPrimer": { "button": "abcdef" }, "background": { "opacity": 0.6 } }""",
+ ).asJsonObject,
+ )
+ campaign.getCustomJsonData() shouldBeEqualTo CustomJson(background = Background(opacity = 0.6f))
+ }
}
diff --git a/inappmessaging/src/test/java/com/rakuten/tech/mobile/inappmessaging/runtime/view/InAppMessageModalViewSpec.kt b/inappmessaging/src/test/java/com/rakuten/tech/mobile/inappmessaging/runtime/view/InAppMessageModalViewSpec.kt
index 5972ade7..577b49fd 100644
--- a/inappmessaging/src/test/java/com/rakuten/tech/mobile/inappmessaging/runtime/view/InAppMessageModalViewSpec.kt
+++ b/inappmessaging/src/test/java/com/rakuten/tech/mobile/inappmessaging/runtime/view/InAppMessageModalViewSpec.kt
@@ -1,14 +1,18 @@
package com.rakuten.tech.mobile.inappmessaging.runtime.view
+import android.graphics.Color
import android.widget.Button
import android.widget.CheckBox
import android.widget.LinearLayout
+import androidx.core.graphics.ColorUtils
import androidx.test.core.app.ApplicationProvider
import com.nhaarman.mockitokotlin2.verify
import com.rakuten.tech.mobile.inappmessaging.runtime.data.customjson.MessageMapper
import com.rakuten.tech.mobile.inappmessaging.runtime.data.responses.ping.Resource
import com.rakuten.tech.mobile.inappmessaging.runtime.testhelpers.TestDataHelper
+import com.rakuten.tech.mobile.inappmessaging.runtime.view.InAppMessageModalView.Companion.MAX_COLOR_ALPHA
+import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.Mockito.*
@@ -16,12 +20,17 @@ import org.robolectric.RobolectricTestRunner
@RunWith(RobolectricTestRunner::class)
class InAppMessageModalViewSpec {
- @Test
- fun `should call setBackgroundColor`() {
- val view = spy(InAppMessageModalView(ApplicationProvider.getApplicationContext(), null))
- val mockModal = mock(LinearLayout::class.java)
+ private val view = spy(InAppMessageModalView(ApplicationProvider.getApplicationContext(), null))
+ private val mockModal = mock(LinearLayout::class.java)
+
+ @Before
+ fun setup() {
doReturn(mockModal).`when`(view).findModalLayout()
+ }
+
+ @Test
+ fun `should call setBackgroundColor`() {
doReturn(null).`when`(view).findViewById