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