Skip to content

Commit

Permalink
feat: support modal background opacity through CustomJson (RMCCX-7327)
Browse files Browse the repository at this point in the history
  • Loading branch information
maureenorea-clores authored Sep 24, 2024
1 parent 4a4ffba commit f1fb3b6
Show file tree
Hide file tree
Showing 13 changed files with 201 additions and 19 deletions.
17 changes: 17 additions & 0 deletions dependency_check_suppressions.xml
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
<vulnerabilityName>CVE-2021-22569</vulnerabilityName>
<vulnerabilityName>CVE-2022-3171</vulnerabilityName>
<vulnerabilityName>CVE-2022-3509</vulnerabilityName>
<vulnerabilityName>CVE-2024-7254</vulnerabilityName>
</suppress>
<suppress until="2024-12-31Z">
<notes><![CDATA[
Expand Down Expand Up @@ -107,4 +108,20 @@
<vulnerabilityName>CVE-2022-3171</vulnerabilityName>
<vulnerabilityName>CVE-2022-3510</vulnerabilityName>
</suppress>
<suppress until="2024-12-31Z">
<notes><![CDATA[
file name: work-runtime-2.7.1.aar: inspector.jar (shaded: com.google.protobuf:protobuf-javalite:3.10.0)
]]></notes>
<packageUrl regex="true">^pkg:maven/com\.google\.protobuf/protobuf\-javalite@.*$</packageUrl>
<vulnerabilityName>CVE-2024-7254</vulnerabilityName>
</suppress>

<!-- Compose (Sample app) -->
<suppress until="2024-12-31Z">
<notes><![CDATA[
file name: ui-1.2.0-rc02.aar: inspector.jar (shaded: com.google.protobuf:protobuf-javalite:3.19.4)
]]></notes>
<packageUrl regex="true">^pkg:maven/com\.google\.protobuf/protobuf\-javalite@.*$</packageUrl>
<vulnerabilityName>CVE-2024-7254</vulnerabilityName>
</suppress>
</suppressions>
1 change: 1 addition & 0 deletions inappmessaging/USERGUIDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand All @@ -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,
)
Original file line number Diff line number Diff line change
@@ -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<CustomJson> {

override fun deserialize(json: JsonElement?, typeOfT: Type?, context: JsonDeserializationContext?): CustomJson {
val jsonObject = json?.asJsonObject

val pushPrimer = context?.safeDeserialize<PushPrimer>(jsonObject?.get("pushPrimer"))
val clickableImage = context?.safeDeserialize<ClickableImage>(jsonObject?.get("clickableImage"))
val background = context?.safeDeserialize<Background>(jsonObject?.get("background"))

return CustomJson(pushPrimer, clickableImage, background)
}

@SuppressWarnings("TooGenericExceptionCaught")
private inline fun <reified T> JsonDeserializationContext.safeDeserialize(jsonElement: JsonElement?): T? {
return try {
this.deserialize<T>(jsonElement, T::class.java)
} catch (_: Exception) {
InAppLogger("IAM_CustomJsonDeserializer")
.warn("Invalid format for ${T::class.java.name.split(".").lastOrNull()}")
null
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ import com.rakuten.tech.mobile.inappmessaging.runtime.data.ui.UiMessage
internal object MessageMapper : Mapper<Message, UiMessage> {

override fun mapFrom(from: Message): UiMessage {
val customJsonData = from.getCustomJsonData()

val uiModel = UiMessage(
id = from.campaignId,
type = from.type,
Expand All @@ -28,13 +30,13 @@ internal object MessageMapper : Mapper<Message, UiMessage> {
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)
Expand Down
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,4 +23,5 @@ internal data class UiMessage(
val displaySettings: DisplaySettings,
val content: Content?,
val tooltipData: Tooltip?,
val backdropOpacity: Float? = null,
)
Original file line number Diff line number Diff line change
@@ -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

Expand All @@ -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
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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))
}
}
Original file line number Diff line number Diff line change
@@ -1,27 +1,36 @@

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.*
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<Button>(anyInt())
doReturn(null).`when`(view).findViewById<CheckBox>(anyInt())

Expand All @@ -42,8 +51,6 @@ class InAppMessageModalViewSpec {

@Test
fun `should not call setBackgroundColor when modal layout is null`() {
val view = spy(InAppMessageModalView(ApplicationProvider.getApplicationContext(), null))

doReturn(null).`when`(view).findModalLayout()

view.populateViewData(
Expand All @@ -54,4 +61,40 @@ class InAppMessageModalViewSpec {

verify(view, never()).setBackgroundColor(anyInt())
}

@Test
fun `should not call setBackgroundColor when opacity is null`() {
view.populateViewData(MessageMapper.mapFrom(TestDataHelper.createDummyMessage()).copy(backdropOpacity = null))
verify(view, never()).setBackgroundColor(anyInt())
}

@Test
fun `should not call setBackgroundColor when opacity is below 0`() {
view.populateViewData(MessageMapper.mapFrom(TestDataHelper.createDummyMessage()).copy(backdropOpacity = -0.5f))
verify(view, never()).setBackgroundColor(anyInt())
}

@Test
fun `should not call setBackgroundColor when opacity is greater than 1`() {
view.populateViewData(MessageMapper.mapFrom(TestDataHelper.createDummyMessage()).copy(backdropOpacity = 3f))
verify(view, never()).setBackgroundColor(anyInt())
}

@Test
fun `should call setBackgroundColor when opacity is set to minimum`() {
view.populateViewData(MessageMapper.mapFrom(TestDataHelper.createDummyMessage()).copy(backdropOpacity = 0f))
verify(view).setBackgroundColor(ColorUtils.setAlphaComponent(Color.BLACK, 0))
}

@Test
fun `should call setBackgroundColor when opacity is set to maximum`() {
view.populateViewData(MessageMapper.mapFrom(TestDataHelper.createDummyMessage()).copy(backdropOpacity = 1f))
verify(view).setBackgroundColor(ColorUtils.setAlphaComponent(Color.BLACK, MAX_COLOR_ALPHA))
}

@Test
fun `should call setBackgroundColor when opacity is valid`() {
view.populateViewData(MessageMapper.mapFrom(TestDataHelper.createDummyMessage()).copy(backdropOpacity = 0.3f))
verify(view).setBackgroundColor(ColorUtils.setAlphaComponent(Color.BLACK, (MAX_COLOR_ALPHA * 0.3).toInt()))
}
}

0 comments on commit f1fb3b6

Please sign in to comment.