From a1a3eb70c7b2ab9a968df562dc9f49452d322efa Mon Sep 17 00:00:00 2001 From: Ana Capatina Date: Thu, 6 Nov 2025 18:01:44 +0000 Subject: [PATCH 1/7] Add What's New message type for RMF. Introduce a new message type for RMF called What's New. This message type uses a cards_list content structure, allowing for a title, description, and a list of items, each with its own title, description, image, and action. --- .../duckduckgo/common/ui/view/MessageCta.kt | 6 + .../src/main/res/drawable/ic_image_ai.xml | 110 ++++++++++++++++++ .../src/main/res/drawable/ic_key_import.xml | 80 +++++++++++++ .../src/main/res/drawable/ic_radar.xml | 85 ++++++++++++++ .../browser/newtab/NewTabLegacyPageView.kt | 6 + .../newtab/NewTabLegacyPageViewModel.kt | 1 + .../remotemessage/CommandActionMapper.kt | 3 +- .../remotemessage/RemoteMessageMapper.kt | 13 +++ .../api/MessageActionMapperPlugin.kt | 10 ++ .../remote/messaging/api/RemoteMessage.kt | 35 ++++++ .../impl/mappers/JsonActionMappers.kt | 14 +++ .../impl/mappers/JsonRemoteMessageMapper.kt | 42 ++++++- .../impl/mappers/RemoteMessageMapper.kt | 13 +++ .../impl/models/JsonRemoteMessagingConfig.kt | 3 + .../impl/newtab/RemoteMessageView.kt | 5 + .../impl/newtab/RemoteMessageViewModel.kt | 3 + .../messaging/fixtures/FakeActionPlugins.kt | 2 + 17 files changed, 426 insertions(+), 5 deletions(-) create mode 100644 android-design-system/design-system/src/main/res/drawable/ic_image_ai.xml create mode 100644 android-design-system/design-system/src/main/res/drawable/ic_key_import.xml create mode 100644 android-design-system/design-system/src/main/res/drawable/ic_radar.xml diff --git a/android-design-system/design-system/src/main/java/com/duckduckgo/common/ui/view/MessageCta.kt b/android-design-system/design-system/src/main/java/com/duckduckgo/common/ui/view/MessageCta.kt index c686c4c2f922..a7361b11cd3b 100644 --- a/android-design-system/design-system/src/main/java/com/duckduckgo/common/ui/view/MessageCta.kt +++ b/android-design-system/design-system/src/main/java/com/duckduckgo/common/ui/view/MessageCta.kt @@ -28,6 +28,7 @@ import androidx.core.view.isVisible import com.airbnb.lottie.LottieAnimationView import com.duckduckgo.common.ui.view.MessageCta.MessageType.REMOTE_MESSAGE import com.duckduckgo.common.ui.view.MessageCta.MessageType.REMOTE_PROMO_MESSAGE +import com.duckduckgo.common.ui.view.MessageCta.MessageType.REMOTE_WHATS_NEW_MESSAGE import com.duckduckgo.common.ui.viewbinding.viewBinding import com.duckduckgo.mobile.android.databinding.ViewMessageCtaBinding @@ -73,6 +74,9 @@ class MessageCta : FrameLayout { when (message.messageType) { REMOTE_MESSAGE -> setRemoteMessage(message) REMOTE_PROMO_MESSAGE -> setPromoMessage(message) + REMOTE_WHATS_NEW_MESSAGE -> { + // TODO: ANA set what's new message + } } } @@ -199,5 +203,7 @@ class MessageCta : FrameLayout { enum class MessageType { REMOTE_MESSAGE, REMOTE_PROMO_MESSAGE, + + REMOTE_WHATS_NEW_MESSAGE, } } diff --git a/android-design-system/design-system/src/main/res/drawable/ic_image_ai.xml b/android-design-system/design-system/src/main/res/drawable/ic_image_ai.xml new file mode 100644 index 000000000000..4626b1c6bb93 --- /dev/null +++ b/android-design-system/design-system/src/main/res/drawable/ic_image_ai.xml @@ -0,0 +1,110 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/android-design-system/design-system/src/main/res/drawable/ic_key_import.xml b/android-design-system/design-system/src/main/res/drawable/ic_key_import.xml new file mode 100644 index 000000000000..52b84fc1b38d --- /dev/null +++ b/android-design-system/design-system/src/main/res/drawable/ic_key_import.xml @@ -0,0 +1,80 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/android-design-system/design-system/src/main/res/drawable/ic_radar.xml b/android-design-system/design-system/src/main/res/drawable/ic_radar.xml new file mode 100644 index 000000000000..f6d9557703b0 --- /dev/null +++ b/android-design-system/design-system/src/main/res/drawable/ic_radar.xml @@ -0,0 +1,85 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/java/com/duckduckgo/app/browser/newtab/NewTabLegacyPageView.kt b/app/src/main/java/com/duckduckgo/app/browser/newtab/NewTabLegacyPageView.kt index a5995714db31..8c5a2fd13728 100644 --- a/app/src/main/java/com/duckduckgo/app/browser/newtab/NewTabLegacyPageView.kt +++ b/app/src/main/java/com/duckduckgo/app/browser/newtab/NewTabLegacyPageView.kt @@ -41,6 +41,7 @@ import com.duckduckgo.app.browser.newtab.NewTabLegacyPageViewModel.Command.Launc import com.duckduckgo.app.browser.newtab.NewTabLegacyPageViewModel.Command.LaunchScreen import com.duckduckgo.app.browser.newtab.NewTabLegacyPageViewModel.Command.SharePromoLinkRMF import com.duckduckgo.app.browser.newtab.NewTabLegacyPageViewModel.Command.SubmitUrl +import com.duckduckgo.app.browser.newtab.NewTabLegacyPageViewModel.Command.SubmitUrlInContext import com.duckduckgo.app.browser.newtab.NewTabLegacyPageViewModel.NewTabLegacyPageViewModelFactory import com.duckduckgo.app.browser.newtab.NewTabLegacyPageViewModel.NewTabLegacyPageViewModelProviderFactory import com.duckduckgo.app.browser.newtab.NewTabLegacyPageViewModel.ViewState @@ -169,6 +170,7 @@ class NewTabLegacyPageView @JvmOverloads constructor( is LaunchScreen -> launchScreen(command.screen, command.payload) is SharePromoLinkRMF -> launchSharePromoRMFPageChooser(command.url, command.shareTitle) is SubmitUrl -> submitUrl(command.url) + is SubmitUrlInContext -> submitUrlInContext(command.url) } } @@ -217,6 +219,10 @@ class NewTabLegacyPageView @JvmOverloads constructor( context.startActivity(browserNav.openInCurrentTab(context, url)) } + private fun submitUrlInContext(url: String) { + // TODO: ANA open a webview activity here + } + private fun showRemoteMessage( message: RemoteMessage, newMessage: Boolean, diff --git a/app/src/main/java/com/duckduckgo/app/browser/newtab/NewTabLegacyPageViewModel.kt b/app/src/main/java/com/duckduckgo/app/browser/newtab/NewTabLegacyPageViewModel.kt index 77a449495c0c..4f62182954a7 100644 --- a/app/src/main/java/com/duckduckgo/app/browser/newtab/NewTabLegacyPageViewModel.kt +++ b/app/src/main/java/com/duckduckgo/app/browser/newtab/NewTabLegacyPageViewModel.kt @@ -98,6 +98,7 @@ class NewTabLegacyPageViewModel @AssistedInject constructor( data object DismissMessage : Command() data class LaunchPlayStore(val appPackage: String) : Command() data class SubmitUrl(val url: String) : Command() + data class SubmitUrlInContext(val url: String) : Command() data object LaunchDefaultBrowser : Command() data object LaunchAppTPOnboarding : Command() data class SharePromoLinkRMF( diff --git a/app/src/main/java/com/duckduckgo/app/browser/remotemessage/CommandActionMapper.kt b/app/src/main/java/com/duckduckgo/app/browser/remotemessage/CommandActionMapper.kt index 677cbb652f22..4966ad03c75a 100644 --- a/app/src/main/java/com/duckduckgo/app/browser/remotemessage/CommandActionMapper.kt +++ b/app/src/main/java/com/duckduckgo/app/browser/remotemessage/CommandActionMapper.kt @@ -16,8 +16,6 @@ package com.duckduckgo.app.browser.remotemessage -import com.duckduckgo.app.browser.commands.Command -import com.duckduckgo.app.browser.commands.Command.* import com.duckduckgo.app.browser.newtab.NewTabLegacyPageViewModel import com.duckduckgo.di.scopes.ActivityScope import com.duckduckgo.remote.messaging.api.Action @@ -39,6 +37,7 @@ class RealCommandActionMapper @Inject constructor( is Dismiss -> NewTabLegacyPageViewModel.Command.DismissMessage is PlayStore -> NewTabLegacyPageViewModel.Command.LaunchPlayStore(action.value) is Url -> NewTabLegacyPageViewModel.Command.SubmitUrl(action.value) + is UrlInContext -> NewTabLegacyPageViewModel.Command.SubmitUrlInContext(action.value) is DefaultBrowser -> NewTabLegacyPageViewModel.Command.LaunchDefaultBrowser is AppTpOnboarding -> NewTabLegacyPageViewModel.Command.LaunchAppTPOnboarding is Share -> NewTabLegacyPageViewModel.Command.SharePromoLinkRMF(action.value, action.title) diff --git a/app/src/main/java/com/duckduckgo/app/browser/remotemessage/RemoteMessageMapper.kt b/app/src/main/java/com/duckduckgo/app/browser/remotemessage/RemoteMessageMapper.kt index 2e91c9ea268c..8cc972c5cdd2 100644 --- a/app/src/main/java/com/duckduckgo/app/browser/remotemessage/RemoteMessageMapper.kt +++ b/app/src/main/java/com/duckduckgo/app/browser/remotemessage/RemoteMessageMapper.kt @@ -21,6 +21,7 @@ import com.duckduckgo.common.ui.view.MessageCta.MessageType import com.duckduckgo.mobile.android.R import com.duckduckgo.remote.messaging.api.Content.BigSingleAction import com.duckduckgo.remote.messaging.api.Content.BigTwoActions +import com.duckduckgo.remote.messaging.api.Content.CardsList import com.duckduckgo.remote.messaging.api.Content.Medium import com.duckduckgo.remote.messaging.api.Content.Placeholder import com.duckduckgo.remote.messaging.api.Content.Placeholder.ANNOUNCE @@ -29,8 +30,11 @@ import com.duckduckgo.remote.messaging.api.Content.Placeholder.CRITICAL_UPDATE import com.duckduckgo.remote.messaging.api.Content.Placeholder.DDG_ANNOUNCE import com.duckduckgo.remote.messaging.api.Content.Placeholder.DUCK_AI import com.duckduckgo.remote.messaging.api.Content.Placeholder.DUCK_AI_OLD +import com.duckduckgo.remote.messaging.api.Content.Placeholder.IMAGE_AI +import com.duckduckgo.remote.messaging.api.Content.Placeholder.KEY_IMPORT import com.duckduckgo.remote.messaging.api.Content.Placeholder.MAC_AND_WINDOWS import com.duckduckgo.remote.messaging.api.Content.Placeholder.PRIVACY_SHIELD +import com.duckduckgo.remote.messaging.api.Content.Placeholder.RADAR import com.duckduckgo.remote.messaging.api.Content.Placeholder.VISUAL_DESIGN_UPDATE import com.duckduckgo.remote.messaging.api.Content.PromoSingleAction import com.duckduckgo.remote.messaging.api.Content.Small @@ -71,6 +75,12 @@ fun RemoteMessage.asMessage(isLightModeEnabled: Boolean): Message { promoAction = content.actionText, messageType = MessageType.REMOTE_PROMO_MESSAGE, ) + is CardsList -> Message( + title = content.titleText, + subtitle = content.descriptionText, + action = content.primaryActionText, + messageType = MessageType.REMOTE_WHATS_NEW_MESSAGE, + ) } } @@ -89,5 +99,8 @@ private fun Placeholder.drawable(isLightModeEnabled: Boolean): Int { } else { R.drawable.ic_visual_design_update_artwork_dark } + IMAGE_AI -> R.drawable.ic_image_ai + RADAR -> R.drawable.ic_radar + KEY_IMPORT -> R.drawable.ic_key_import } } diff --git a/remote-messaging/remote-messaging-api/src/main/java/com/duckduckgo/remote/messaging/api/MessageActionMapperPlugin.kt b/remote-messaging/remote-messaging-api/src/main/java/com/duckduckgo/remote/messaging/api/MessageActionMapperPlugin.kt index 00aeafd73438..3254c879a7a6 100644 --- a/remote-messaging/remote-messaging-api/src/main/java/com/duckduckgo/remote/messaging/api/MessageActionMapperPlugin.kt +++ b/remote-messaging/remote-messaging-api/src/main/java/com/duckduckgo/remote/messaging/api/MessageActionMapperPlugin.kt @@ -29,6 +29,7 @@ data class JsonMessageAction( @Suppress("ktlint:standard:class-naming") sealed class JsonActionType(val jsonValue: String) { data object URL : JsonActionType("url") + data object URL_IN_CONTEXT : JsonActionType("url_in_context") data object PLAYSTORE : JsonActionType("playstore") data object DEFAULT_BROWSER : JsonActionType("defaultBrowser") data object DISMISS : JsonActionType("dismiss") @@ -37,3 +38,12 @@ sealed class JsonActionType(val jsonValue: String) { data object NAVIGATION : JsonActionType("navigation") data object SURVEY : JsonActionType("survey") } + +data class JsonListItem( + val id: String, + val type: String, + val titleText: String, + val descriptionText: String, + val placeholder: String = "", + val primaryAction: JsonMessageAction?, +) diff --git a/remote-messaging/remote-messaging-api/src/main/java/com/duckduckgo/remote/messaging/api/RemoteMessage.kt b/remote-messaging/remote-messaging-api/src/main/java/com/duckduckgo/remote/messaging/api/RemoteMessage.kt index b8eceb590e71..cd9becb2d24a 100644 --- a/remote-messaging/remote-messaging-api/src/main/java/com/duckduckgo/remote/messaging/api/RemoteMessage.kt +++ b/remote-messaging/remote-messaging-api/src/main/java/com/duckduckgo/remote/messaging/api/RemoteMessage.kt @@ -23,6 +23,7 @@ import com.duckduckgo.remote.messaging.api.Content.MessageType.BIG_TWO_ACTION import com.duckduckgo.remote.messaging.api.Content.MessageType.MEDIUM import com.duckduckgo.remote.messaging.api.Content.MessageType.PROMO_SINGLE_ACTION import com.duckduckgo.remote.messaging.api.Content.MessageType.SMALL +import com.duckduckgo.remote.messaging.api.Content.Placeholder import com.duckduckgo.remote.messaging.api.JsonActionType.APP_TP_ONBOARDING import com.duckduckgo.remote.messaging.api.JsonActionType.DEFAULT_BROWSER import com.duckduckgo.remote.messaging.api.JsonActionType.DISMISS @@ -67,12 +68,22 @@ sealed class Content(val messageType: MessageType) { val action: Action, ) : Content(PROMO_SINGLE_ACTION) + data class CardsList( + val titleText: String, + val descriptionText: String, + val placeholder: Placeholder, + val primaryActionText: String, + val primaryAction: Action, + val listItems: List, + ) : Content(MessageType.CARDS_LIST) + enum class MessageType { SMALL, MEDIUM, BIG_SINGLE_ACTION, BIG_TWO_ACTION, PROMO_SINGLE_ACTION, + CARDS_LIST, } enum class Placeholder(val jsonValue: String) { @@ -85,6 +96,9 @@ sealed class Content(val messageType: MessageType) { DUCK_AI_OLD("Duck.ai"), DUCK_AI("DuckAi"), VISUAL_DESIGN_UPDATE("VisualDesignUpdate"), + IMAGE_AI("ImageAI"), + RADAR("Radar"), + KEY_IMPORT("KeyImport"), ; companion object { @@ -97,6 +111,7 @@ sealed class Content(val messageType: MessageType) { sealed class Action(val actionType: String, open val value: String, open val additionalParameters: Map?) { data class Url(override val value: String) : Action(URL.jsonValue, value, null) + data class UrlInContext(override val value: String) : Action(JsonActionType.URL_IN_CONTEXT.jsonValue, value, null) data class PlayStore(override val value: String) : Action(PLAYSTORE.jsonValue, value, null) data object DefaultBrowser : Action(DEFAULT_BROWSER.jsonValue, "", null) data object Dismiss : Action(DISMISS.jsonValue, "", null) @@ -122,3 +137,23 @@ sealed class Action(val actionType: String, open val value: String, open val add override val additionalParameters: Map?, ) : Action(JsonActionType.SURVEY.jsonValue, value, additionalParameters) } + +data class CardItem( + val id: String, + val type: CardItemType, + val titleText: String, + val descriptionText: String, + val placeholder: Placeholder, + val primaryAction: Action, +) + +enum class CardItemType(val jsonValue: String) { + TWO_LINE_LIST_ITEM("two_line_list_item"), + ; + + companion object { + fun from(jsonValue: String): CardItemType { + return CardItemType.values().first { it.jsonValue == jsonValue } + } + } +} diff --git a/remote-messaging/remote-messaging-impl/src/main/java/com/duckduckgo/remote/messaging/impl/mappers/JsonActionMappers.kt b/remote-messaging/remote-messaging-impl/src/main/java/com/duckduckgo/remote/messaging/impl/mappers/JsonActionMappers.kt index cea501a61076..cccf1bd9162f 100644 --- a/remote-messaging/remote-messaging-impl/src/main/java/com/duckduckgo/remote/messaging/impl/mappers/JsonActionMappers.kt +++ b/remote-messaging/remote-messaging-impl/src/main/java/com/duckduckgo/remote/messaging/impl/mappers/JsonActionMappers.kt @@ -21,6 +21,7 @@ import com.duckduckgo.di.scopes.AppScope import com.duckduckgo.navigation.api.GlobalActivityStarter import com.duckduckgo.navigation.api.GlobalActivityStarter.DeeplinkActivityParams import com.duckduckgo.remote.messaging.api.Action +import com.duckduckgo.remote.messaging.api.JsonActionType import com.duckduckgo.remote.messaging.api.JsonActionType.DEFAULT_BROWSER import com.duckduckgo.remote.messaging.api.JsonActionType.DISMISS import com.duckduckgo.remote.messaging.api.JsonActionType.NAVIGATION @@ -45,6 +46,19 @@ class UrlActionMapper @Inject constructor() : MessageActionMapperPlugin { } } +@ContributesMultibinding( + AppScope::class, +) +class UrlInContextActionMapper @Inject constructor() : MessageActionMapperPlugin { + override fun evaluate(jsonMessageAction: JsonMessageAction): Action? { + return if (jsonMessageAction.type == JsonActionType.URL_IN_CONTEXT.jsonValue) { + Action.UrlInContext(jsonMessageAction.value) + } else { + null + } + } +} + @ContributesMultibinding( AppScope::class, ) diff --git a/remote-messaging/remote-messaging-impl/src/main/java/com/duckduckgo/remote/messaging/impl/mappers/JsonRemoteMessageMapper.kt b/remote-messaging/remote-messaging-impl/src/main/java/com/duckduckgo/remote/messaging/impl/mappers/JsonRemoteMessageMapper.kt index 93bf4dfb0571..aacdefd28707 100644 --- a/remote-messaging/remote-messaging-impl/src/main/java/com/duckduckgo/remote/messaging/impl/mappers/JsonRemoteMessageMapper.kt +++ b/remote-messaging/remote-messaging-impl/src/main/java/com/duckduckgo/remote/messaging/impl/mappers/JsonRemoteMessageMapper.kt @@ -17,19 +17,24 @@ package com.duckduckgo.remote.messaging.impl.mappers import com.duckduckgo.remote.messaging.api.Action +import com.duckduckgo.remote.messaging.api.CardItem +import com.duckduckgo.remote.messaging.api.CardItemType import com.duckduckgo.remote.messaging.api.Content import com.duckduckgo.remote.messaging.api.Content.BigSingleAction import com.duckduckgo.remote.messaging.api.Content.BigTwoActions +import com.duckduckgo.remote.messaging.api.Content.CardsList import com.duckduckgo.remote.messaging.api.Content.Medium import com.duckduckgo.remote.messaging.api.Content.Placeholder import com.duckduckgo.remote.messaging.api.Content.PromoSingleAction import com.duckduckgo.remote.messaging.api.Content.Small +import com.duckduckgo.remote.messaging.api.JsonListItem import com.duckduckgo.remote.messaging.api.JsonMessageAction import com.duckduckgo.remote.messaging.api.MessageActionMapperPlugin import com.duckduckgo.remote.messaging.api.RemoteMessage import com.duckduckgo.remote.messaging.impl.models.* import com.duckduckgo.remote.messaging.impl.models.JsonMessageType.BIG_SINGLE_ACTION import com.duckduckgo.remote.messaging.impl.models.JsonMessageType.BIG_TWO_ACTION +import com.duckduckgo.remote.messaging.impl.models.JsonMessageType.CARDS_LIST import com.duckduckgo.remote.messaging.impl.models.JsonMessageType.MEDIUM import com.duckduckgo.remote.messaging.impl.models.JsonMessageType.PROMO_SINGLE_ACTION import com.duckduckgo.remote.messaging.impl.models.JsonMessageType.SMALL @@ -85,6 +90,17 @@ private val promoSingleActionMapper: (JsonContent, Set) -> Content = { jsonContent, actionMappers -> + CardsList( + titleText = jsonContent.titleText.failIfEmpty(), + descriptionText = jsonContent.descriptionText.failIfEmpty(), + placeholder = jsonContent.placeholder.asPlaceholder(), + primaryActionText = jsonContent.primaryActionText.failIfEmpty(), + primaryAction = jsonContent.primaryAction!!.toAction(actionMappers), + listItems = jsonContent.listItems.toListItems(actionMappers), + ) +} + // plugin point? private val messageMappers = mapOf( Pair(SMALL.jsonValue, smallMapper), @@ -92,6 +108,7 @@ private val messageMappers = mapOf( Pair(BIG_SINGLE_ACTION.jsonValue, bigMessageSingleActionMapper), Pair(BIG_TWO_ACTION.jsonValue, bigMessageTwoActionMapper), Pair(PROMO_SINGLE_ACTION.jsonValue, promoSingleActionMapper), + Pair(CARDS_LIST.jsonValue, cardsListMapper), ) fun List.mapToRemoteMessage( @@ -112,7 +129,7 @@ private fun JsonRemoteMessage.map( ) remoteMessage.localizeMessage(this.translations, locale) }.onFailure { - logcat(ERROR) { "RMF: error $it" } + logcat(ERROR) { "RMF: error parsing message id=${this.id}: ${it.message}\n${it.stackTraceToString()}" } }.getOrNull() } @@ -140,14 +157,28 @@ private fun JsonMessageAction.toAction(actionMappers: Set?.toListItems(actionMappers: Set): List { + return this?.map { jsonItem -> + CardItem( + id = jsonItem.id.failIfEmpty(), + type = CardItemType.from(jsonItem.type), + titleText = jsonItem.titleText.failIfEmpty(), + descriptionText = jsonItem.descriptionText.failIfEmpty(), + placeholder = jsonItem.placeholder.asPlaceholder(), + primaryAction = jsonItem.primaryAction?.toAction(actionMappers) + ?: throw IllegalStateException("CardItem primaryAction cannot be null"), + ) + } ?: emptyList() +} + private fun Content.localize(translations: JsonContentTranslations): Content { return when (this) { is BigSingleAction -> this.copy( @@ -174,5 +205,10 @@ private fun Content.localize(translations: JsonContentTranslations): Content { descriptionText = translations.descriptionText.takeUnless { it.isEmpty() } ?: this.descriptionText, actionText = translations.actionText.takeUnless { it.isEmpty() } ?: this.actionText, ) + is CardsList -> this.copy( + titleText = translations.titleText.takeUnless { it.isEmpty() } ?: this.titleText, + descriptionText = translations.descriptionText.takeUnless { it.isEmpty() } ?: this.descriptionText, + primaryActionText = translations.primaryActionText.takeUnless { it.isEmpty() } ?: this.primaryActionText, + ) } } diff --git a/remote-messaging/remote-messaging-impl/src/main/java/com/duckduckgo/remote/messaging/impl/mappers/RemoteMessageMapper.kt b/remote-messaging/remote-messaging-impl/src/main/java/com/duckduckgo/remote/messaging/impl/mappers/RemoteMessageMapper.kt index b4e87eadb01c..4dbac828438c 100644 --- a/remote-messaging/remote-messaging-impl/src/main/java/com/duckduckgo/remote/messaging/impl/mappers/RemoteMessageMapper.kt +++ b/remote-messaging/remote-messaging-impl/src/main/java/com/duckduckgo/remote/messaging/impl/mappers/RemoteMessageMapper.kt @@ -21,6 +21,7 @@ import com.duckduckgo.common.ui.view.MessageCta.MessageType import com.duckduckgo.mobile.android.R import com.duckduckgo.remote.messaging.api.Content.BigSingleAction import com.duckduckgo.remote.messaging.api.Content.BigTwoActions +import com.duckduckgo.remote.messaging.api.Content.CardsList import com.duckduckgo.remote.messaging.api.Content.Medium import com.duckduckgo.remote.messaging.api.Content.Placeholder import com.duckduckgo.remote.messaging.api.Content.Placeholder.ANNOUNCE @@ -29,8 +30,11 @@ import com.duckduckgo.remote.messaging.api.Content.Placeholder.CRITICAL_UPDATE import com.duckduckgo.remote.messaging.api.Content.Placeholder.DDG_ANNOUNCE import com.duckduckgo.remote.messaging.api.Content.Placeholder.DUCK_AI import com.duckduckgo.remote.messaging.api.Content.Placeholder.DUCK_AI_OLD +import com.duckduckgo.remote.messaging.api.Content.Placeholder.IMAGE_AI +import com.duckduckgo.remote.messaging.api.Content.Placeholder.KEY_IMPORT import com.duckduckgo.remote.messaging.api.Content.Placeholder.MAC_AND_WINDOWS import com.duckduckgo.remote.messaging.api.Content.Placeholder.PRIVACY_SHIELD +import com.duckduckgo.remote.messaging.api.Content.Placeholder.RADAR import com.duckduckgo.remote.messaging.api.Content.Placeholder.VISUAL_DESIGN_UPDATE import com.duckduckgo.remote.messaging.api.Content.PromoSingleAction import com.duckduckgo.remote.messaging.api.Content.Small @@ -71,6 +75,12 @@ fun RemoteMessage.asMessage(isLightModeEnabled: Boolean): Message { promoAction = content.actionText, messageType = MessageType.REMOTE_PROMO_MESSAGE, ) + is CardsList -> Message( + title = content.titleText, + subtitle = content.descriptionText, + action = content.primaryActionText, + messageType = MessageType.REMOTE_WHATS_NEW_MESSAGE, + ) } } @@ -89,5 +99,8 @@ private fun Placeholder.drawable(isLightModeEnabled: Boolean): Int { } else { R.drawable.ic_visual_design_update_artwork_dark } + IMAGE_AI -> R.drawable.ic_image_ai + RADAR -> R.drawable.ic_radar + KEY_IMPORT -> R.drawable.ic_key_import } } diff --git a/remote-messaging/remote-messaging-impl/src/main/java/com/duckduckgo/remote/messaging/impl/models/JsonRemoteMessagingConfig.kt b/remote-messaging/remote-messaging-impl/src/main/java/com/duckduckgo/remote/messaging/impl/models/JsonRemoteMessagingConfig.kt index bdf9706179da..bf25a685114b 100644 --- a/remote-messaging/remote-messaging-impl/src/main/java/com/duckduckgo/remote/messaging/impl/models/JsonRemoteMessagingConfig.kt +++ b/remote-messaging/remote-messaging-impl/src/main/java/com/duckduckgo/remote/messaging/impl/models/JsonRemoteMessagingConfig.kt @@ -16,6 +16,7 @@ package com.duckduckgo.remote.messaging.impl.models +import com.duckduckgo.remote.messaging.api.JsonListItem import com.duckduckgo.remote.messaging.api.JsonMatchingAttribute import com.duckduckgo.remote.messaging.api.JsonMessageAction @@ -44,6 +45,7 @@ data class JsonContent( val secondaryAction: JsonMessageAction? = null, val actionText: String = "", val action: JsonMessageAction? = null, + val listItems: List? = null, ) data class JsonContentTranslations( @@ -72,4 +74,5 @@ sealed class JsonMessageType(val jsonValue: String) { data object BIG_SINGLE_ACTION : JsonMessageType("big_single_action") data object BIG_TWO_ACTION : JsonMessageType("big_two_action") data object PROMO_SINGLE_ACTION : JsonMessageType("promo_single_action") + data object CARDS_LIST : JsonMessageType("cards_list") } diff --git a/remote-messaging/remote-messaging-impl/src/main/java/com/duckduckgo/remote/messaging/impl/newtab/RemoteMessageView.kt b/remote-messaging/remote-messaging-impl/src/main/java/com/duckduckgo/remote/messaging/impl/newtab/RemoteMessageView.kt index dbd188260f16..af011b399792 100644 --- a/remote-messaging/remote-messaging-impl/src/main/java/com/duckduckgo/remote/messaging/impl/newtab/RemoteMessageView.kt +++ b/remote-messaging/remote-messaging-impl/src/main/java/com/duckduckgo/remote/messaging/impl/newtab/RemoteMessageView.kt @@ -140,6 +140,7 @@ class RemoteMessageView @JvmOverloads constructor( is LaunchScreen -> launchScreen(command.screen, command.payload) is SharePromoLinkRMF -> launchSharePromoRMFPageChooser(command.url, command.shareTitle) is SubmitUrl -> submitUrl(command.url) + is Command.SubmitUrlInContext -> submitUrlInContext(command.url) } } @@ -221,6 +222,10 @@ class RemoteMessageView @JvmOverloads constructor( private fun submitUrl(url: String) { context.startActivity(browserNav.openInCurrentTab(context, url)) } + + private fun submitUrlInContext(url: String) { + // TODO: ANA open a webview activity here + } } @ContributesActivePlugin( diff --git a/remote-messaging/remote-messaging-impl/src/main/java/com/duckduckgo/remote/messaging/impl/newtab/RemoteMessageViewModel.kt b/remote-messaging/remote-messaging-impl/src/main/java/com/duckduckgo/remote/messaging/impl/newtab/RemoteMessageViewModel.kt index cb2427bc5061..52fbcb1fd1cb 100644 --- a/remote-messaging/remote-messaging-impl/src/main/java/com/duckduckgo/remote/messaging/impl/newtab/RemoteMessageViewModel.kt +++ b/remote-messaging/remote-messaging-impl/src/main/java/com/duckduckgo/remote/messaging/impl/newtab/RemoteMessageViewModel.kt @@ -34,6 +34,7 @@ import com.duckduckgo.remote.messaging.api.Action.PlayStore import com.duckduckgo.remote.messaging.api.Action.Share import com.duckduckgo.remote.messaging.api.Action.Survey import com.duckduckgo.remote.messaging.api.Action.Url +import com.duckduckgo.remote.messaging.api.Action.UrlInContext import com.duckduckgo.remote.messaging.api.RemoteMessage import com.duckduckgo.remote.messaging.api.RemoteMessageModel import com.duckduckgo.survey.api.SurveyParameterManager @@ -68,6 +69,7 @@ class RemoteMessageViewModel @Inject constructor( data object DismissMessage : Command() data class LaunchPlayStore(val appPackage: String) : Command() data class SubmitUrl(val url: String) : Command() + data class SubmitUrlInContext(val url: String) : Command() data object LaunchDefaultBrowser : Command() data object LaunchAppTPOnboarding : Command() data class SharePromoLinkRMF( @@ -162,6 +164,7 @@ class RemoteMessageViewModel @Inject constructor( is Dismiss -> Command.DismissMessage is PlayStore -> Command.LaunchPlayStore(this.value) is Url -> Command.SubmitUrl(this.value) + is UrlInContext -> Command.SubmitUrlInContext(this.value) is DefaultBrowser -> Command.LaunchDefaultBrowser is AppTpOnboarding -> Command.LaunchAppTPOnboarding is Share -> Command.SharePromoLinkRMF(this.value, this.title) diff --git a/remote-messaging/remote-messaging-impl/src/test/java/com/duckduckgo/remote/messaging/fixtures/FakeActionPlugins.kt b/remote-messaging/remote-messaging-impl/src/test/java/com/duckduckgo/remote/messaging/fixtures/FakeActionPlugins.kt index 06e7d490b9da..fa3e8624c294 100644 --- a/remote-messaging/remote-messaging-impl/src/test/java/com/duckduckgo/remote/messaging/fixtures/FakeActionPlugins.kt +++ b/remote-messaging/remote-messaging-impl/src/test/java/com/duckduckgo/remote/messaging/fixtures/FakeActionPlugins.kt @@ -21,9 +21,11 @@ import com.duckduckgo.remote.messaging.impl.mappers.DismissActionMapper import com.duckduckgo.remote.messaging.impl.mappers.PlayStoreActionMapper import com.duckduckgo.remote.messaging.impl.mappers.ShareActionMapper import com.duckduckgo.remote.messaging.impl.mappers.UrlActionMapper +import com.duckduckgo.remote.messaging.impl.mappers.UrlInContextActionMapper val messageActionPlugins = listOf( UrlActionMapper(), + UrlInContextActionMapper(), DismissActionMapper(), PlayStoreActionMapper(), DefaultBrowserActionMapper(), From 5b860c5400532a4bd2b48ae673321f527fd3d1fc Mon Sep 17 00:00:00 2001 From: Ana Capatina Date: Wed, 19 Nov 2025 14:10:13 +0000 Subject: [PATCH 2/7] Added the latest assets for ic_image_ai.xml, ic_key_import.xml, and ic_radar.xml. --- .../src/main/res/drawable/ic_image_ai.xml | 172 +++++++++--------- .../src/main/res/drawable/ic_key_import.xml | 94 +++++----- .../src/main/res/drawable/ic_radar.xml | 109 +++++------ 3 files changed, 177 insertions(+), 198 deletions(-) diff --git a/android-design-system/design-system/src/main/res/drawable/ic_image_ai.xml b/android-design-system/design-system/src/main/res/drawable/ic_image_ai.xml index 4626b1c6bb93..86fd6786412d 100644 --- a/android-design-system/design-system/src/main/res/drawable/ic_image_ai.xml +++ b/android-design-system/design-system/src/main/res/drawable/ic_image_ai.xml @@ -16,95 +16,85 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - + android:width="96dp" + android:height="96dp" + android:viewportWidth="96" + android:viewportHeight="96"> + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/android-design-system/design-system/src/main/res/drawable/ic_key_import.xml b/android-design-system/design-system/src/main/res/drawable/ic_key_import.xml index 52b84fc1b38d..25a32ba9b7f7 100644 --- a/android-design-system/design-system/src/main/res/drawable/ic_key_import.xml +++ b/android-design-system/design-system/src/main/res/drawable/ic_key_import.xml @@ -1,38 +1,54 @@ + + + android:width="96dp" + android:height="96dp" + android:viewportWidth="96" + android:viewportHeight="96"> @@ -40,41 +56,13 @@ + android:pathData="M92.501,59c0.298,0 0.595,0.12 0.823,0.354 0.454,0.468 0.454,1.23 0,1.698l-2.333,2.4a1.145,1.145 0,0 1,-1.65 0,1.227 1.227,0 0,1 0,-1.698l2.333,-2.4c0.227,-0.234 0.524,-0.354 0.822,-0.354zM91.335,69.798h3.499c0.641,0 1.166,0.54 1.166,1.2s-0.525,1.2 -1.166,1.2h-3.499c-0.641,0 -1.166,-0.54 -1.166,-1.2s0.525,-1.2 1.166,-1.2m-1.982,8.754c0.227,-0.234 0.525,-0.354 0.822,-0.354h0.006c0.297,0 0.595,0.12 0.822,0.354l2.332,2.4c0.455,0.467 0.455,1.23 0,1.697a1.145,1.145 0,0 1,-1.65 0l-2.332,-2.4a1.227,1.227 0,0 1,0 -1.697" + android:fillColor="#ccc"/> + android:pathData="M87,71c0,-8.837 -7.163,-16 -16,-16s-16,7.163 -16,16 7.163,16 16,16 16,-7.163 16,-16" + android:fillColor="#557ff3"/> - - - - - - - - - diff --git a/android-design-system/design-system/src/main/res/drawable/ic_radar.xml b/android-design-system/design-system/src/main/res/drawable/ic_radar.xml index f6d9557703b0..7a48614cf8b9 100644 --- a/android-design-system/design-system/src/main/res/drawable/ic_radar.xml +++ b/android-design-system/design-system/src/main/res/drawable/ic_radar.xml @@ -1,85 +1,86 @@ + + + android:width="96dp" + android:height="96dp" + android:viewportWidth="96" + android:viewportHeight="96"> + android:pathData="M35,20c-8.284,0 -15,6.716 -15,15v26c0,8.284 6.716,15 15,15h16v-5c0,-11.046 8.954,-20 20,-20h5V35c0,-8.284 -6.716,-15 -15,-15z" + android:fillColor="#63c853"/> + android:pathData="M34.75,16C24.257,16 16,24.507 16,35v26c0,10.493 8.257,19 18.75,19H47a4,4 0,0 0,4 -4v-5c0,-5.201 1.986,-9.938 5.24,-13.495L55.75,53h6.523A19.9,19.9 0,0 1,71 51h4.75a4,4 0,0 0,4 -4V35c0,-10.493 -8.507,-19 -19,-19z" + android:fillColor="#999"/> + android:pathData="M30.667,20C24.776,20 20,24.776 20,30.667v30.666C20,67.224 24.776,72 30.667,72H51v-1c0,-11.046 8.954,-20 20,-20h1V30.667C72,24.776 67.224,20 61.333,20z" + android:fillColor="#e5e5e5"/> + android:pathData="M30.667,16C22.567,16 16,22.567 16,30.667v30.666C16,69.433 22.567,76 30.667,76H51v-4H30.667l-0.549,-0.014c-5.454,-0.276 -9.828,-4.65 -10.104,-10.104L20,61.333V30.667c0,-5.707 4.482,-10.368 10.118,-10.653l0.549,-0.014h30.666l0.549,0.014C67.518,20.299 72,24.96 72,30.667V51h4V30.667C76,22.567 69.433,16 61.333,16z" + android:fillColor="#ccc"/> + android:pathData="M46,23c5.365,0 10.3,1.837 14.213,4.916A22.9,22.9 0,0 0,49 25c-12.703,0 -23,10.297 -23,23 0,7.337 3.436,13.872 8.786,18.083C27.755,62.148 23,54.63 23,46c0,-12.703 10.297,-23 23,-23" + android:fillColor="#1a690d"/> + android:pathData="M44,33.5a1.5,1.5 0,1 1,-3 0,1.5 1.5,0 0,1 3,0" + android:fillColor="#d3ffcc"/> + android:pathData="M34,44.5a1.5,1.5 0,1 1,-3 0,1.5 1.5,0 0,1 3,0m6,4a1.5,1.5 0,1 1,-3 0,1.5 1.5,0 0,1 3,0m14,6a1.5,1.5 0,1 1,-3 0,1.5 1.5,0 0,1 3,0" + android:fillColor="#96e38a"/> + android:pathData="M33.293,34.293a1,1 0,0 1,1.338 -0.068l0.076,0.068 0.793,0.793 0.793,-0.793 0.076,-0.068a1,1 0,0 1,1.406 1.406l-0.068,0.076 -0.793,0.793 0.793,0.793 0.068,0.076a1,1 0,0 1,-1.406 1.407l-0.076,-0.069 -0.793,-0.793 -0.793,0.793 -0.076,0.069a1,1 0,0 1,-1.407 -1.407l0.069,-0.076 0.793,-0.793 -0.793,-0.793 -0.069,-0.076a1,1 0,0 1,0.069 -1.338" + android:fillColor="#d3ffcc"/> + android:pathData="M59.293,47.293a1,1 0,0 1,1.338 -0.068l0.076,0.068 0.793,0.793 0.793,-0.793 0.076,-0.068a1,1 0,0 1,1.406 1.406l-0.068,0.076 -0.793,0.793 0.793,0.793 0.068,0.076a1,1 0,0 1,-1.406 1.407l-0.076,-0.069 -0.793,-0.793 -0.793,0.793 -0.076,0.069a1,1 0,0 1,-1.407 -1.407l0.069,-0.076 0.793,-0.793 -0.793,-0.793 -0.069,-0.076a1,1 0,0 1,0.069 -1.338m-22,9a1,1 0,0 1,1.338 -0.068l0.076,0.068 0.793,0.793 0.793,-0.793 0.076,-0.068a1,1 0,0 1,1.406 1.406l-0.068,0.076 -0.793,0.793 0.793,0.793 0.068,0.076a1,1 0,0 1,-1.406 1.407l-0.076,-0.069 -0.793,-0.793 -0.793,0.793 -0.076,0.069a1,1 0,0 1,-1.407 -1.407l0.069,-0.076 0.793,-0.793 -0.793,-0.793 -0.069,-0.076a1,1 0,0 1,0.069 -1.338" + android:fillColor="#96e38a"/> + android:pathData="M71,87c8.837,0 16,-7.163 16,-16s-7.163,-16 -16,-16 -16,7.163 -16,16 7.163,16 16,16" + android:fillColor="#21c000"/> + android:pathData="M79.885,68.31c0.52,-0.52 0.52,-1.365 0,-1.885l-1.859,-1.86a1.333,1.333 0,0 0,-1.885 0l-6.538,6.54c-0.52,0.52 -1.365,0.52 -1.885,0l-1.86,-1.86a1.333,1.333 0,0 0,-1.884 0l-1.859,1.86c-0.52,0.52 -0.52,1.364 0,1.884l5.603,5.603c0.52,0.52 1.364,0.52 1.885,0z" + android:fillColor="#fff"/> - - - - - + android:pathData="M92.501,59c0.298,0 0.595,0.12 0.823,0.354 0.454,0.468 0.454,1.23 0,1.698l-2.333,2.4a1.145,1.145 0,0 1,-1.65 0,1.227 1.227,0 0,1 0,-1.698l2.333,-2.4c0.227,-0.234 0.524,-0.354 0.822,-0.354zM91.335,69.798h3.499c0.641,0 1.166,0.54 1.166,1.2s-0.525,1.2 -1.166,1.2h-3.499c-0.641,0 -1.166,-0.54 -1.166,-1.2s0.525,-1.2 1.166,-1.2m-1.982,8.754c0.227,-0.234 0.525,-0.354 0.822,-0.354h0.006c0.297,0 0.595,0.12 0.822,0.354l2.332,2.4c0.455,0.467 0.455,1.23 0,1.697a1.145,1.145 0,0 1,-1.65 0l-2.332,-2.4a1.227,1.227 0,0 1,0 -1.697" + android:fillColor="#ccc"/> From 8dc3cebff064958079e5d517b64b35cc23192cd5 Mon Sep 17 00:00:00 2001 From: Ana Capatina Date: Wed, 19 Nov 2025 14:15:31 +0000 Subject: [PATCH 3/7] Fixed spotless. --- .../src/main/java/com/duckduckgo/common/ui/view/MessageCta.kt | 4 +--- .../com/duckduckgo/app/browser/newtab/NewTabLegacyPageView.kt | 1 - .../remote/messaging/impl/newtab/RemoteMessageView.kt | 1 - 3 files changed, 1 insertion(+), 5 deletions(-) diff --git a/android-design-system/design-system/src/main/java/com/duckduckgo/common/ui/view/MessageCta.kt b/android-design-system/design-system/src/main/java/com/duckduckgo/common/ui/view/MessageCta.kt index a7361b11cd3b..438186778a0f 100644 --- a/android-design-system/design-system/src/main/java/com/duckduckgo/common/ui/view/MessageCta.kt +++ b/android-design-system/design-system/src/main/java/com/duckduckgo/common/ui/view/MessageCta.kt @@ -74,9 +74,7 @@ class MessageCta : FrameLayout { when (message.messageType) { REMOTE_MESSAGE -> setRemoteMessage(message) REMOTE_PROMO_MESSAGE -> setPromoMessage(message) - REMOTE_WHATS_NEW_MESSAGE -> { - // TODO: ANA set what's new message - } + REMOTE_WHATS_NEW_MESSAGE -> { } } } diff --git a/app/src/main/java/com/duckduckgo/app/browser/newtab/NewTabLegacyPageView.kt b/app/src/main/java/com/duckduckgo/app/browser/newtab/NewTabLegacyPageView.kt index 8c5a2fd13728..6d53b2852ff0 100644 --- a/app/src/main/java/com/duckduckgo/app/browser/newtab/NewTabLegacyPageView.kt +++ b/app/src/main/java/com/duckduckgo/app/browser/newtab/NewTabLegacyPageView.kt @@ -220,7 +220,6 @@ class NewTabLegacyPageView @JvmOverloads constructor( } private fun submitUrlInContext(url: String) { - // TODO: ANA open a webview activity here } private fun showRemoteMessage( diff --git a/remote-messaging/remote-messaging-impl/src/main/java/com/duckduckgo/remote/messaging/impl/newtab/RemoteMessageView.kt b/remote-messaging/remote-messaging-impl/src/main/java/com/duckduckgo/remote/messaging/impl/newtab/RemoteMessageView.kt index af011b399792..ccba7ca20d51 100644 --- a/remote-messaging/remote-messaging-impl/src/main/java/com/duckduckgo/remote/messaging/impl/newtab/RemoteMessageView.kt +++ b/remote-messaging/remote-messaging-impl/src/main/java/com/duckduckgo/remote/messaging/impl/newtab/RemoteMessageView.kt @@ -224,7 +224,6 @@ class RemoteMessageView @JvmOverloads constructor( } private fun submitUrlInContext(url: String) { - // TODO: ANA open a webview activity here } } From edde666d3edb744921c160d2af5266ae541d09b1 Mon Sep 17 00:00:00 2001 From: Ana Capatina Date: Wed, 19 Nov 2025 14:35:43 +0000 Subject: [PATCH 4/7] Added tests. --- .../messaging/fixtures/JsonRemoteMessageOM.kt | 35 ++ .../messaging/fixtures/RemoteMessageOM.kt | 51 +++ .../impl/CardsListMessageMapperTest.kt | 328 ++++++++++++++++++ .../impl/JsonRemoteMessageMapperTest.kt | 20 ++ 4 files changed, 434 insertions(+) create mode 100644 remote-messaging/remote-messaging-impl/src/test/java/com/duckduckgo/remote/messaging/impl/CardsListMessageMapperTest.kt diff --git a/remote-messaging/remote-messaging-impl/src/test/java/com/duckduckgo/remote/messaging/fixtures/JsonRemoteMessageOM.kt b/remote-messaging/remote-messaging-impl/src/test/java/com/duckduckgo/remote/messaging/fixtures/JsonRemoteMessageOM.kt index f704c01373e6..babd9bc54585 100644 --- a/remote-messaging/remote-messaging-impl/src/test/java/com/duckduckgo/remote/messaging/fixtures/JsonRemoteMessageOM.kt +++ b/remote-messaging/remote-messaging-impl/src/test/java/com/duckduckgo/remote/messaging/fixtures/JsonRemoteMessageOM.kt @@ -16,6 +16,7 @@ package com.duckduckgo.remote.messaging.fixtures +import com.duckduckgo.remote.messaging.api.JsonListItem import com.duckduckgo.remote.messaging.api.JsonMessageAction import com.duckduckgo.remote.messaging.impl.models.JsonContent import com.duckduckgo.remote.messaging.impl.models.JsonContentTranslations @@ -103,6 +104,40 @@ object JsonRemoteMessageOM { action = action, ) + fun cardsListJsonContent( + titleText: String = "title", + descriptionText: String = "description", + placeholder: String = "Announce", + primaryActionText: String = "Action", + primaryAction: JsonMessageAction = jsonMessageAction(), + listItems: List = listOf( + JsonListItem( + id = "item1", + type = "two_line_list_item", + titleText = "Item Title 1", + descriptionText = "Item Description 1", + placeholder = "ImageAI", + primaryAction = jsonMessageAction(), + ), + JsonListItem( + id = "item2", + type = "two_line_list_item", + titleText = "Item Title 2", + descriptionText = "Item Description 2", + placeholder = "Radar", + primaryAction = jsonMessageAction(), + ), + ), + ) = JsonContent( + messageType = "cards_list", + titleText = titleText, + descriptionText = descriptionText, + placeholder = placeholder, + primaryActionText = primaryActionText, + primaryAction = primaryAction, + listItems = listItems, + ) + fun emptyJsonContent(messageType: String = "") = JsonContent(messageType = messageType) fun aJsonMessage( diff --git a/remote-messaging/remote-messaging-impl/src/test/java/com/duckduckgo/remote/messaging/fixtures/RemoteMessageOM.kt b/remote-messaging/remote-messaging-impl/src/test/java/com/duckduckgo/remote/messaging/fixtures/RemoteMessageOM.kt index cde470a37ab4..047dbb13cc20 100644 --- a/remote-messaging/remote-messaging-impl/src/test/java/com/duckduckgo/remote/messaging/fixtures/RemoteMessageOM.kt +++ b/remote-messaging/remote-messaging-impl/src/test/java/com/duckduckgo/remote/messaging/fixtures/RemoteMessageOM.kt @@ -17,10 +17,14 @@ package com.duckduckgo.remote.messaging.fixtures import com.duckduckgo.remote.messaging.api.Action +import com.duckduckgo.remote.messaging.api.CardItem +import com.duckduckgo.remote.messaging.api.CardItemType import com.duckduckgo.remote.messaging.api.Content import com.duckduckgo.remote.messaging.api.Content.Placeholder import com.duckduckgo.remote.messaging.api.Content.Placeholder.ANNOUNCE +import com.duckduckgo.remote.messaging.api.Content.Placeholder.IMAGE_AI import com.duckduckgo.remote.messaging.api.Content.Placeholder.MAC_AND_WINDOWS +import com.duckduckgo.remote.messaging.api.Content.Placeholder.RADAR import com.duckduckgo.remote.messaging.api.RemoteMessage @Suppress("MemberVisibilityCanBePrivate") @@ -93,6 +97,39 @@ object RemoteMessageOM { action = action, ) + fun cardsListContent( + titleText: String = "title", + descriptionText: String = "description", + placeholder: Placeholder = ANNOUNCE, + primaryActionText: String = "Action", + primaryAction: Action = urlAction(), + listItems: List = listOf( + CardItem( + id = "item1", + type = CardItemType.TWO_LINE_LIST_ITEM, + titleText = "Item Title 1", + descriptionText = "Item Description 1", + placeholder = IMAGE_AI, + primaryAction = urlAction(), + ), + CardItem( + id = "item2", + type = CardItemType.TWO_LINE_LIST_ITEM, + titleText = "Item Title 2", + descriptionText = "Item Description 2", + placeholder = RADAR, + primaryAction = urlAction(), + ), + ), + ) = Content.CardsList( + titleText = titleText, + descriptionText = descriptionText, + placeholder = placeholder, + primaryActionText = primaryActionText, + primaryAction = primaryAction, + listItems = listItems, + ) + fun aSmallMessage( id: String = "id", content: Content = smallContent(), @@ -162,4 +199,18 @@ object RemoteMessageOM { matchingRules = matchingRules, ) } + + fun aCardsListMessage( + id: String = "id", + content: Content = cardsListContent(), + exclusionRules: List = emptyList(), + matchingRules: List = emptyList(), + ): RemoteMessage { + return RemoteMessage( + id = id, + content = content, + exclusionRules = exclusionRules, + matchingRules = matchingRules, + ) + } } diff --git a/remote-messaging/remote-messaging-impl/src/test/java/com/duckduckgo/remote/messaging/impl/CardsListMessageMapperTest.kt b/remote-messaging/remote-messaging-impl/src/test/java/com/duckduckgo/remote/messaging/impl/CardsListMessageMapperTest.kt new file mode 100644 index 000000000000..e567254053a6 --- /dev/null +++ b/remote-messaging/remote-messaging-impl/src/test/java/com/duckduckgo/remote/messaging/impl/CardsListMessageMapperTest.kt @@ -0,0 +1,328 @@ +/* + * Copyright (c) 2025 DuckDuckGo + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.duckduckgo.remote.messaging.impl + +import com.duckduckgo.remote.messaging.api.Action +import com.duckduckgo.remote.messaging.api.CardItemType +import com.duckduckgo.remote.messaging.api.Content +import com.duckduckgo.remote.messaging.api.JsonListItem +import com.duckduckgo.remote.messaging.api.JsonMessageAction +import com.duckduckgo.remote.messaging.fixtures.JsonRemoteMessageOM.aJsonMessage +import com.duckduckgo.remote.messaging.fixtures.JsonRemoteMessageOM.cardsListJsonContent +import com.duckduckgo.remote.messaging.fixtures.messageActionPlugins +import com.duckduckgo.remote.messaging.impl.mappers.mapToRemoteMessage +import com.duckduckgo.remote.messaging.impl.models.JsonContent +import org.junit.Assert.assertEquals +import org.junit.Assert.assertNotNull +import org.junit.Assert.assertTrue +import org.junit.Test +import java.util.* + +class CardsListMessageMapperTest { + + @Test + fun whenCardsListMessageWithValidDataThenReturnMessage() { + val jsonMessages = listOf( + aJsonMessage(id = "cards1", content = cardsListJsonContent()), + ) + + val remoteMessages = jsonMessages.mapToRemoteMessage(Locale.US, messageActionPlugins) + + assertEquals(1, remoteMessages.size) + val message = remoteMessages.first() + assertEquals("cards1", message.id) + assertTrue(message.content is Content.CardsList) + + val content = message.content as Content.CardsList + assertEquals("title", content.titleText) + assertEquals("description", content.descriptionText) + assertEquals("Action", content.primaryActionText) + assertEquals(2, content.listItems.size) + } + + @Test + fun whenCardsListMessageWithCustomListItemsThenMapCorrectly() { + val customListItems = listOf( + JsonListItem( + id = "feature1", + type = "two_line_list_item", + titleText = "Feature One", + descriptionText = "Description for feature one", + placeholder = "ImageAI", + primaryAction = JsonMessageAction(type = "url", value = "https://example.com/feature1", additionalParameters = null), + ), + JsonListItem( + id = "feature2", + type = "two_line_list_item", + titleText = "Feature Two", + descriptionText = "Description for feature two", + placeholder = "Radar", + primaryAction = JsonMessageAction(type = "url", value = "https://example.com/feature2", additionalParameters = null), + ), + JsonListItem( + id = "feature3", + type = "two_line_list_item", + titleText = "Feature Three", + descriptionText = "Description for feature three", + placeholder = "KeyImport", + primaryAction = JsonMessageAction(type = "url", value = "https://example.com/feature3", additionalParameters = null), + ), + ) + + val jsonMessages = listOf( + aJsonMessage( + id = "cards2", + content = cardsListJsonContent(listItems = customListItems), + ), + ) + + val remoteMessages = jsonMessages.mapToRemoteMessage(Locale.US, messageActionPlugins) + + assertEquals(1, remoteMessages.size) + val content = remoteMessages.first().content as Content.CardsList + assertEquals(3, content.listItems.size) + + val item1 = content.listItems[0] + assertEquals("feature1", item1.id) + assertEquals(CardItemType.TWO_LINE_LIST_ITEM, item1.type) + assertEquals("Feature One", item1.titleText) + assertEquals("Description for feature one", item1.descriptionText) + assertEquals(Content.Placeholder.IMAGE_AI, item1.placeholder) + assertTrue(item1.primaryAction is Action.Url) + assertEquals("https://example.com/feature1", (item1.primaryAction as Action.Url).value) + + val item2 = content.listItems[1] + assertEquals("feature2", item2.id) + assertEquals("Feature Two", item2.titleText) + assertEquals(Content.Placeholder.RADAR, item2.placeholder) + + val item3 = content.listItems[2] + assertEquals("feature3", item3.id) + assertEquals("Feature Three", item3.titleText) + assertEquals(Content.Placeholder.KEY_IMPORT, item3.placeholder) + } + + @Test + fun whenCardsListMessageWithEmptyListItemsThenReturnEmptyList() { + val jsonMessages = listOf( + aJsonMessage( + id = "cards3", + content = cardsListJsonContent(listItems = emptyList()), + ), + ) + + val remoteMessages = jsonMessages.mapToRemoteMessage(Locale.US, messageActionPlugins) + + assertEquals(1, remoteMessages.size) + val content = remoteMessages.first().content as Content.CardsList + assertEquals(0, content.listItems.size) + } + + @Test + fun whenCardsListMessageWithNullListItemsThenReturnEmptyList() { + val jsonContent = JsonContent( + messageType = "cards_list", + titleText = "title", + descriptionText = "description", + placeholder = "Announce", + primaryActionText = "Action", + primaryAction = JsonMessageAction(type = "url", value = "https://example.com", additionalParameters = null), + listItems = null, + ) + + val jsonMessages = listOf( + aJsonMessage(id = "cards4", content = jsonContent), + ) + + val remoteMessages = jsonMessages.mapToRemoteMessage(Locale.US, messageActionPlugins) + + assertEquals(1, remoteMessages.size) + val content = remoteMessages.first().content as Content.CardsList + assertNotNull(content.listItems) + assertEquals(0, content.listItems.size) + } + + @Test + fun whenCardsListMessageWithMissingRequiredFieldsThenMessageIsFiltered() { + // Missing title + val jsonContentMissingTitle = JsonContent( + messageType = "cards_list", + titleText = "", + descriptionText = "description", + placeholder = "Announce", + primaryActionText = "Action", + primaryAction = JsonMessageAction(type = "url", value = "https://example.com", additionalParameters = null), + listItems = emptyList(), + ) + + val jsonMessages = listOf( + aJsonMessage(id = "cards5", content = jsonContentMissingTitle), + ) + + val remoteMessages = jsonMessages.mapToRemoteMessage(Locale.US, messageActionPlugins) + + // Message should be filtered out due to empty required field + assertEquals(0, remoteMessages.size) + } + + @Test + fun whenCardsListMessageWithListItemMissingRequiredFieldsThenMessageIsFiltered() { + val invalidListItems = listOf( + JsonListItem( + id = "", // Empty ID should cause failure + type = "two_line_list_item", + titleText = "Feature", + descriptionText = "Description", + placeholder = "ImageAI", + primaryAction = JsonMessageAction(type = "url", value = "https://example.com", additionalParameters = null), + ), + ) + + val jsonMessages = listOf( + aJsonMessage( + id = "cards6", + content = cardsListJsonContent(listItems = invalidListItems), + ), + ) + + val remoteMessages = jsonMessages.mapToRemoteMessage(Locale.US, messageActionPlugins) + + // Message should be filtered out due to invalid list item + assertEquals(0, remoteMessages.size) + } + + @Test + fun whenCardsListMessageWithListItemMissingActionThenMessageIsFiltered() { + val listItemsWithNullAction = listOf( + JsonListItem( + id = "item1", + type = "two_line_list_item", + titleText = "Feature", + descriptionText = "Description", + placeholder = "ImageAI", + primaryAction = null, // Null action should cause failure + ), + ) + + val jsonMessages = listOf( + aJsonMessage( + id = "cards7", + content = cardsListJsonContent(listItems = listItemsWithNullAction), + ), + ) + + val remoteMessages = jsonMessages.mapToRemoteMessage(Locale.US, messageActionPlugins) + + // Message should be filtered out due to null action in list item + assertEquals(0, remoteMessages.size) + } + + @Test + fun whenCardsListMessageWithDifferentPlaceholdersThenMapCorrectly() { + val listItemsWithDifferentPlaceholders = listOf( + JsonListItem( + id = "item1", + type = "two_line_list_item", + titleText = "AI Feature", + descriptionText = "AI Description", + placeholder = "DuckAi", + primaryAction = JsonMessageAction(type = "url", value = "https://example.com", additionalParameters = null), + ), + JsonListItem( + id = "item2", + type = "two_line_list_item", + titleText = "Privacy Feature", + descriptionText = "Privacy Description", + placeholder = "PrivacyShield", + primaryAction = JsonMessageAction(type = "url", value = "https://example.com", additionalParameters = null), + ), + ) + + val jsonMessages = listOf( + aJsonMessage( + id = "cards8", + content = cardsListJsonContent( + placeholder = "DDGAnnounce", + listItems = listItemsWithDifferentPlaceholders, + ), + ), + ) + + val remoteMessages = jsonMessages.mapToRemoteMessage(Locale.US, messageActionPlugins) + + assertEquals(1, remoteMessages.size) + val content = remoteMessages.first().content as Content.CardsList + assertEquals(Content.Placeholder.DDG_ANNOUNCE, content.placeholder) + assertEquals(Content.Placeholder.DUCK_AI, content.listItems[0].placeholder) + assertEquals(Content.Placeholder.PRIVACY_SHIELD, content.listItems[1].placeholder) + } + + @Test + fun whenCardsListMessageWithDifferentActionTypesThenMapCorrectly() { + val listItemsWithDifferentActions = listOf( + JsonListItem( + id = "item1", + type = "two_line_list_item", + titleText = "Web Feature", + descriptionText = "Opens URL", + placeholder = "ImageAI", + primaryAction = JsonMessageAction(type = "url", value = "https://example.com", additionalParameters = null), + ), + JsonListItem( + id = "item2", + type = "two_line_list_item", + titleText = "Dismiss Feature", + descriptionText = "Just dismisses", + placeholder = "Radar", + primaryAction = JsonMessageAction(type = "dismiss", value = "", additionalParameters = null), + ), + JsonListItem( + id = "item3", + type = "two_line_list_item", + titleText = "Share Feature", + descriptionText = "Share content", + placeholder = "KeyImport", + primaryAction = JsonMessageAction( + type = "share", + value = "Share this!", + additionalParameters = mapOf("title" to "Share Title"), + ), + ), + ) + + val jsonMessages = listOf( + aJsonMessage( + id = "cards9", + content = cardsListJsonContent(listItems = listItemsWithDifferentActions), + ), + ) + + val remoteMessages = jsonMessages.mapToRemoteMessage(Locale.US, messageActionPlugins) + + assertEquals(1, remoteMessages.size) + val content = remoteMessages.first().content as Content.CardsList + assertEquals(3, content.listItems.size) + + assertTrue(content.listItems[0].primaryAction is Action.Url) + assertTrue(content.listItems[1].primaryAction is Action.Dismiss) + assertTrue(content.listItems[2].primaryAction is Action.Share) + + val shareAction = content.listItems[2].primaryAction as Action.Share + assertEquals("Share this!", shareAction.value) + assertEquals("Share Title", shareAction.title) + } +} diff --git a/remote-messaging/remote-messaging-impl/src/test/java/com/duckduckgo/remote/messaging/impl/JsonRemoteMessageMapperTest.kt b/remote-messaging/remote-messaging-impl/src/test/java/com/duckduckgo/remote/messaging/impl/JsonRemoteMessageMapperTest.kt index d341ba6c6c56..557eed989e6b 100644 --- a/remote-messaging/remote-messaging-impl/src/test/java/com/duckduckgo/remote/messaging/impl/JsonRemoteMessageMapperTest.kt +++ b/remote-messaging/remote-messaging-impl/src/test/java/com/duckduckgo/remote/messaging/impl/JsonRemoteMessageMapperTest.kt @@ -20,17 +20,20 @@ import com.duckduckgo.remote.messaging.api.RemoteMessage import com.duckduckgo.remote.messaging.fixtures.JsonRemoteMessageOM.aJsonMessage import com.duckduckgo.remote.messaging.fixtures.JsonRemoteMessageOM.bigSingleActionJsonContent import com.duckduckgo.remote.messaging.fixtures.JsonRemoteMessageOM.bigTwoActionJsonContent +import com.duckduckgo.remote.messaging.fixtures.JsonRemoteMessageOM.cardsListJsonContent import com.duckduckgo.remote.messaging.fixtures.JsonRemoteMessageOM.emptyJsonContent import com.duckduckgo.remote.messaging.fixtures.JsonRemoteMessageOM.mediumJsonContent import com.duckduckgo.remote.messaging.fixtures.JsonRemoteMessageOM.promoSingleActionJsonContent import com.duckduckgo.remote.messaging.fixtures.JsonRemoteMessageOM.smallJsonContent import com.duckduckgo.remote.messaging.fixtures.RemoteMessageOM.aBigSingleActionMessage import com.duckduckgo.remote.messaging.fixtures.RemoteMessageOM.aBigTwoActionsMessage +import com.duckduckgo.remote.messaging.fixtures.RemoteMessageOM.aCardsListMessage import com.duckduckgo.remote.messaging.fixtures.RemoteMessageOM.aMediumMessage import com.duckduckgo.remote.messaging.fixtures.RemoteMessageOM.aPromoSingleActionMessage import com.duckduckgo.remote.messaging.fixtures.RemoteMessageOM.aSmallMessage import com.duckduckgo.remote.messaging.fixtures.RemoteMessageOM.bigSingleActionContent import com.duckduckgo.remote.messaging.fixtures.RemoteMessageOM.bigTwoActionsContent +import com.duckduckgo.remote.messaging.fixtures.RemoteMessageOM.cardsListContent import com.duckduckgo.remote.messaging.fixtures.RemoteMessageOM.mediumContent import com.duckduckgo.remote.messaging.fixtures.RemoteMessageOM.promoSingleActionContent import com.duckduckgo.remote.messaging.fixtures.RemoteMessageOM.smallContent @@ -65,6 +68,7 @@ class JsonRemoteMessageMapperTest(private val testCase: TestCase) { aJsonMessage(id = "id3", content = bigSingleActionJsonContent()), aJsonMessage(id = "id4", content = bigTwoActionJsonContent()), aJsonMessage(id = "id5", content = promoSingleActionJsonContent()), + aJsonMessage(id = "id6", content = cardsListJsonContent()), ), listOf( aSmallMessage(id = "id1"), @@ -72,6 +76,7 @@ class JsonRemoteMessageMapperTest(private val testCase: TestCase) { aBigSingleActionMessage(id = "id3"), aBigTwoActionsMessage(id = "id4"), aPromoSingleActionMessage(id = "id5"), + aCardsListMessage(id = "id6"), ), ), TestCase( @@ -82,6 +87,7 @@ class JsonRemoteMessageMapperTest(private val testCase: TestCase) { aJsonMessage(id = "id4", content = bigSingleActionJsonContent()), aJsonMessage(id = "id5", content = bigTwoActionJsonContent()), aJsonMessage(id = "id6", content = promoSingleActionJsonContent()), + aJsonMessage(id = "id7", content = cardsListJsonContent()), ), listOf( aSmallMessage(id = "id2"), @@ -89,6 +95,7 @@ class JsonRemoteMessageMapperTest(private val testCase: TestCase) { aBigSingleActionMessage(id = "id4"), aBigTwoActionsMessage(id = "id5"), aPromoSingleActionMessage(id = "id6"), + aCardsListMessage(id = "id7"), ), ), TestCase( @@ -134,6 +141,11 @@ class JsonRemoteMessageMapperTest(private val testCase: TestCase) { content = promoSingleActionJsonContent(), translations = mapOf("fr" to frenchTranslations()), ), + aJsonMessage( + id = "id6", + content = cardsListJsonContent(), + translations = mapOf("fr" to frenchTranslations()), + ), ), listOf( aSmallMessage( @@ -172,6 +184,14 @@ class JsonRemoteMessageMapperTest(private val testCase: TestCase) { actionText = frenchTranslations().actionText, ), ), + aCardsListMessage( + id = "id6", + cardsListContent( + titleText = frenchTranslations().titleText, + descriptionText = frenchTranslations().descriptionText, + primaryActionText = frenchTranslations().primaryActionText, + ), + ), ), ), ) From 42badecb879943298548505b8093e20341bf5e71 Mon Sep 17 00:00:00 2001 From: Ana Capatina Date: Thu, 20 Nov 2025 17:57:56 +0000 Subject: [PATCH 5/7] Renamed REMOTE_WHATS_NEW_MESSAGE to REMOTE_LIST_MESSAGE. --- .../main/java/com/duckduckgo/common/ui/view/MessageCta.kt | 7 +++---- .../app/browser/remotemessage/RemoteMessageMapper.kt | 2 +- .../remote/messaging/impl/mappers/RemoteMessageMapper.kt | 2 +- 3 files changed, 5 insertions(+), 6 deletions(-) diff --git a/android-design-system/design-system/src/main/java/com/duckduckgo/common/ui/view/MessageCta.kt b/android-design-system/design-system/src/main/java/com/duckduckgo/common/ui/view/MessageCta.kt index 438186778a0f..253f81a9a1d0 100644 --- a/android-design-system/design-system/src/main/java/com/duckduckgo/common/ui/view/MessageCta.kt +++ b/android-design-system/design-system/src/main/java/com/duckduckgo/common/ui/view/MessageCta.kt @@ -28,7 +28,7 @@ import androidx.core.view.isVisible import com.airbnb.lottie.LottieAnimationView import com.duckduckgo.common.ui.view.MessageCta.MessageType.REMOTE_MESSAGE import com.duckduckgo.common.ui.view.MessageCta.MessageType.REMOTE_PROMO_MESSAGE -import com.duckduckgo.common.ui.view.MessageCta.MessageType.REMOTE_WHATS_NEW_MESSAGE +import com.duckduckgo.common.ui.view.MessageCta.MessageType.REMOTE_LIST_MESSAGE import com.duckduckgo.common.ui.viewbinding.viewBinding import com.duckduckgo.mobile.android.databinding.ViewMessageCtaBinding @@ -74,7 +74,7 @@ class MessageCta : FrameLayout { when (message.messageType) { REMOTE_MESSAGE -> setRemoteMessage(message) REMOTE_PROMO_MESSAGE -> setPromoMessage(message) - REMOTE_WHATS_NEW_MESSAGE -> { } + REMOTE_LIST_MESSAGE -> { } } } @@ -201,7 +201,6 @@ class MessageCta : FrameLayout { enum class MessageType { REMOTE_MESSAGE, REMOTE_PROMO_MESSAGE, - - REMOTE_WHATS_NEW_MESSAGE, + REMOTE_LIST_MESSAGE, } } diff --git a/app/src/main/java/com/duckduckgo/app/browser/remotemessage/RemoteMessageMapper.kt b/app/src/main/java/com/duckduckgo/app/browser/remotemessage/RemoteMessageMapper.kt index 8cc972c5cdd2..75495babd14a 100644 --- a/app/src/main/java/com/duckduckgo/app/browser/remotemessage/RemoteMessageMapper.kt +++ b/app/src/main/java/com/duckduckgo/app/browser/remotemessage/RemoteMessageMapper.kt @@ -79,7 +79,7 @@ fun RemoteMessage.asMessage(isLightModeEnabled: Boolean): Message { title = content.titleText, subtitle = content.descriptionText, action = content.primaryActionText, - messageType = MessageType.REMOTE_WHATS_NEW_MESSAGE, + messageType = MessageType.REMOTE_LIST_MESSAGE, ) } } diff --git a/remote-messaging/remote-messaging-impl/src/main/java/com/duckduckgo/remote/messaging/impl/mappers/RemoteMessageMapper.kt b/remote-messaging/remote-messaging-impl/src/main/java/com/duckduckgo/remote/messaging/impl/mappers/RemoteMessageMapper.kt index 4dbac828438c..f410488cbb20 100644 --- a/remote-messaging/remote-messaging-impl/src/main/java/com/duckduckgo/remote/messaging/impl/mappers/RemoteMessageMapper.kt +++ b/remote-messaging/remote-messaging-impl/src/main/java/com/duckduckgo/remote/messaging/impl/mappers/RemoteMessageMapper.kt @@ -79,7 +79,7 @@ fun RemoteMessage.asMessage(isLightModeEnabled: Boolean): Message { title = content.titleText, subtitle = content.descriptionText, action = content.primaryActionText, - messageType = MessageType.REMOTE_WHATS_NEW_MESSAGE, + messageType = MessageType.REMOTE_LIST_MESSAGE, ) } } From c6561c3322e9803f8fc092a318365101fcdae51e Mon Sep 17 00:00:00 2001 From: Ana Capatina Date: Thu, 20 Nov 2025 18:14:27 +0000 Subject: [PATCH 6/7] Simplify CardItemType enum and mapper. --- .../main/java/com/duckduckgo/common/ui/view/MessageCta.kt | 2 +- .../com/duckduckgo/remote/messaging/api/RemoteMessage.kt | 7 ------- .../messaging/impl/mappers/JsonRemoteMessageMapper.kt | 6 +++++- 3 files changed, 6 insertions(+), 9 deletions(-) diff --git a/android-design-system/design-system/src/main/java/com/duckduckgo/common/ui/view/MessageCta.kt b/android-design-system/design-system/src/main/java/com/duckduckgo/common/ui/view/MessageCta.kt index 253f81a9a1d0..dc44f9374988 100644 --- a/android-design-system/design-system/src/main/java/com/duckduckgo/common/ui/view/MessageCta.kt +++ b/android-design-system/design-system/src/main/java/com/duckduckgo/common/ui/view/MessageCta.kt @@ -26,9 +26,9 @@ import androidx.appcompat.content.res.AppCompatResources import androidx.core.text.HtmlCompat import androidx.core.view.isVisible import com.airbnb.lottie.LottieAnimationView +import com.duckduckgo.common.ui.view.MessageCta.MessageType.REMOTE_LIST_MESSAGE import com.duckduckgo.common.ui.view.MessageCta.MessageType.REMOTE_MESSAGE import com.duckduckgo.common.ui.view.MessageCta.MessageType.REMOTE_PROMO_MESSAGE -import com.duckduckgo.common.ui.view.MessageCta.MessageType.REMOTE_LIST_MESSAGE import com.duckduckgo.common.ui.viewbinding.viewBinding import com.duckduckgo.mobile.android.databinding.ViewMessageCtaBinding diff --git a/remote-messaging/remote-messaging-api/src/main/java/com/duckduckgo/remote/messaging/api/RemoteMessage.kt b/remote-messaging/remote-messaging-api/src/main/java/com/duckduckgo/remote/messaging/api/RemoteMessage.kt index eb58e0f75bf2..450cbf088d3d 100644 --- a/remote-messaging/remote-messaging-api/src/main/java/com/duckduckgo/remote/messaging/api/RemoteMessage.kt +++ b/remote-messaging/remote-messaging-api/src/main/java/com/duckduckgo/remote/messaging/api/RemoteMessage.kt @@ -155,11 +155,4 @@ data class CardItem( enum class CardItemType(val jsonValue: String) { TWO_LINE_LIST_ITEM("two_line_list_item"), - ; - - companion object { - fun from(jsonValue: String): CardItemType { - return CardItemType.values().first { it.jsonValue == jsonValue } - } - } } diff --git a/remote-messaging/remote-messaging-impl/src/main/java/com/duckduckgo/remote/messaging/impl/mappers/JsonRemoteMessageMapper.kt b/remote-messaging/remote-messaging-impl/src/main/java/com/duckduckgo/remote/messaging/impl/mappers/JsonRemoteMessageMapper.kt index 2e2d81564c5a..9c48208be1de 100644 --- a/remote-messaging/remote-messaging-impl/src/main/java/com/duckduckgo/remote/messaging/impl/mappers/JsonRemoteMessageMapper.kt +++ b/remote-messaging/remote-messaging-impl/src/main/java/com/duckduckgo/remote/messaging/impl/mappers/JsonRemoteMessageMapper.kt @@ -172,11 +172,15 @@ private fun String.failIfEmpty() = this.ifEmpty { throw IllegalStateException("E private fun String.asPlaceholder(): Placeholder = Placeholder.from(this) +private fun String.toCardItemType(): CardItemType { + return CardItemType.entries.first { it.jsonValue == this } +} + private fun List?.toListItems(actionMappers: Set): List { return this?.map { jsonItem -> CardItem( id = jsonItem.id.failIfEmpty(), - type = CardItemType.from(jsonItem.type), + type = jsonItem.type.toCardItemType(), titleText = jsonItem.titleText.failIfEmpty(), descriptionText = jsonItem.descriptionText.failIfEmpty(), placeholder = jsonItem.placeholder.asPlaceholder(), From c34f2d7893b43e310e4a2c611441236faa450827 Mon Sep 17 00:00:00 2001 From: Ana Capatina Date: Thu, 20 Nov 2025 19:09:02 +0000 Subject: [PATCH 7/7] Moved JsonListItem data class to the JsonRemoteMessagingConfig file. --- .../remote/messaging/api/MessageActionMapperPlugin.kt | 9 --------- .../messaging/impl/mappers/JsonRemoteMessageMapper.kt | 1 - .../messaging/impl/models/JsonRemoteMessagingConfig.kt | 10 +++++++++- .../remote/messaging/fixtures/JsonRemoteMessageOM.kt | 2 +- .../messaging/impl/CardsListMessageMapperTest.kt | 2 +- 5 files changed, 11 insertions(+), 13 deletions(-) diff --git a/remote-messaging/remote-messaging-api/src/main/java/com/duckduckgo/remote/messaging/api/MessageActionMapperPlugin.kt b/remote-messaging/remote-messaging-api/src/main/java/com/duckduckgo/remote/messaging/api/MessageActionMapperPlugin.kt index 3254c879a7a6..461738daf3d4 100644 --- a/remote-messaging/remote-messaging-api/src/main/java/com/duckduckgo/remote/messaging/api/MessageActionMapperPlugin.kt +++ b/remote-messaging/remote-messaging-api/src/main/java/com/duckduckgo/remote/messaging/api/MessageActionMapperPlugin.kt @@ -38,12 +38,3 @@ sealed class JsonActionType(val jsonValue: String) { data object NAVIGATION : JsonActionType("navigation") data object SURVEY : JsonActionType("survey") } - -data class JsonListItem( - val id: String, - val type: String, - val titleText: String, - val descriptionText: String, - val placeholder: String = "", - val primaryAction: JsonMessageAction?, -) diff --git a/remote-messaging/remote-messaging-impl/src/main/java/com/duckduckgo/remote/messaging/impl/mappers/JsonRemoteMessageMapper.kt b/remote-messaging/remote-messaging-impl/src/main/java/com/duckduckgo/remote/messaging/impl/mappers/JsonRemoteMessageMapper.kt index 9c48208be1de..79cd90fedcf9 100644 --- a/remote-messaging/remote-messaging-impl/src/main/java/com/duckduckgo/remote/messaging/impl/mappers/JsonRemoteMessageMapper.kt +++ b/remote-messaging/remote-messaging-impl/src/main/java/com/duckduckgo/remote/messaging/impl/mappers/JsonRemoteMessageMapper.kt @@ -27,7 +27,6 @@ import com.duckduckgo.remote.messaging.api.Content.Medium import com.duckduckgo.remote.messaging.api.Content.Placeholder import com.duckduckgo.remote.messaging.api.Content.PromoSingleAction import com.duckduckgo.remote.messaging.api.Content.Small -import com.duckduckgo.remote.messaging.api.JsonListItem import com.duckduckgo.remote.messaging.api.JsonMessageAction import com.duckduckgo.remote.messaging.api.MessageActionMapperPlugin import com.duckduckgo.remote.messaging.api.RemoteMessage diff --git a/remote-messaging/remote-messaging-impl/src/main/java/com/duckduckgo/remote/messaging/impl/models/JsonRemoteMessagingConfig.kt b/remote-messaging/remote-messaging-impl/src/main/java/com/duckduckgo/remote/messaging/impl/models/JsonRemoteMessagingConfig.kt index 01b6e30e6f91..aa56900fc609 100644 --- a/remote-messaging/remote-messaging-impl/src/main/java/com/duckduckgo/remote/messaging/impl/models/JsonRemoteMessagingConfig.kt +++ b/remote-messaging/remote-messaging-impl/src/main/java/com/duckduckgo/remote/messaging/impl/models/JsonRemoteMessagingConfig.kt @@ -16,7 +16,6 @@ package com.duckduckgo.remote.messaging.impl.models -import com.duckduckgo.remote.messaging.api.JsonListItem import com.duckduckgo.remote.messaging.api.JsonMatchingAttribute import com.duckduckgo.remote.messaging.api.JsonMessageAction @@ -68,6 +67,15 @@ data class JsonTargetPercentile( val before: Float?, ) +data class JsonListItem( + val id: String, + val type: String, + val titleText: String, + val descriptionText: String, + val placeholder: String = "", + val primaryAction: JsonMessageAction?, +) + @Suppress("ktlint:standard:class-naming") sealed class JsonMessageType(val jsonValue: String) { data object SMALL : JsonMessageType("small") diff --git a/remote-messaging/remote-messaging-impl/src/test/java/com/duckduckgo/remote/messaging/fixtures/JsonRemoteMessageOM.kt b/remote-messaging/remote-messaging-impl/src/test/java/com/duckduckgo/remote/messaging/fixtures/JsonRemoteMessageOM.kt index 1d231afc1626..7548cab08493 100644 --- a/remote-messaging/remote-messaging-impl/src/test/java/com/duckduckgo/remote/messaging/fixtures/JsonRemoteMessageOM.kt +++ b/remote-messaging/remote-messaging-impl/src/test/java/com/duckduckgo/remote/messaging/fixtures/JsonRemoteMessageOM.kt @@ -16,10 +16,10 @@ package com.duckduckgo.remote.messaging.fixtures -import com.duckduckgo.remote.messaging.api.JsonListItem import com.duckduckgo.remote.messaging.api.JsonMessageAction import com.duckduckgo.remote.messaging.impl.models.JsonContent import com.duckduckgo.remote.messaging.impl.models.JsonContentTranslations +import com.duckduckgo.remote.messaging.impl.models.JsonListItem import com.duckduckgo.remote.messaging.impl.models.JsonMatchingRule import com.duckduckgo.remote.messaging.impl.models.JsonRemoteMessage import com.duckduckgo.remote.messaging.impl.models.JsonRemoteMessagingConfig diff --git a/remote-messaging/remote-messaging-impl/src/test/java/com/duckduckgo/remote/messaging/impl/CardsListMessageMapperTest.kt b/remote-messaging/remote-messaging-impl/src/test/java/com/duckduckgo/remote/messaging/impl/CardsListMessageMapperTest.kt index e567254053a6..a1ace46ff4e2 100644 --- a/remote-messaging/remote-messaging-impl/src/test/java/com/duckduckgo/remote/messaging/impl/CardsListMessageMapperTest.kt +++ b/remote-messaging/remote-messaging-impl/src/test/java/com/duckduckgo/remote/messaging/impl/CardsListMessageMapperTest.kt @@ -19,13 +19,13 @@ package com.duckduckgo.remote.messaging.impl import com.duckduckgo.remote.messaging.api.Action import com.duckduckgo.remote.messaging.api.CardItemType import com.duckduckgo.remote.messaging.api.Content -import com.duckduckgo.remote.messaging.api.JsonListItem import com.duckduckgo.remote.messaging.api.JsonMessageAction import com.duckduckgo.remote.messaging.fixtures.JsonRemoteMessageOM.aJsonMessage import com.duckduckgo.remote.messaging.fixtures.JsonRemoteMessageOM.cardsListJsonContent import com.duckduckgo.remote.messaging.fixtures.messageActionPlugins import com.duckduckgo.remote.messaging.impl.mappers.mapToRemoteMessage import com.duckduckgo.remote.messaging.impl.models.JsonContent +import com.duckduckgo.remote.messaging.impl.models.JsonListItem import org.junit.Assert.assertEquals import org.junit.Assert.assertNotNull import org.junit.Assert.assertTrue