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..86fd6786412d --- /dev/null +++ b/android-design-system/design-system/src/main/res/drawable/ic_image_ai.xml @@ -0,0 +1,100 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 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..25a32ba9b7f7 --- /dev/null +++ b/android-design-system/design-system/src/main/res/drawable/ic_key_import.xml @@ -0,0 +1,68 @@ + + + + + + + + + + + + + + + + + + + + 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..7a48614cf8b9 --- /dev/null +++ b/android-design-system/design-system/src/main/res/drawable/ic_radar.xml @@ -0,0 +1,86 @@ + + + + + + + + + + + + + + + + + + + + + + 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..1ddfda366b8d 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 @@ -222,10 +222,11 @@ class NewTabLegacyPageView @JvmOverloads constructor( newMessage: Boolean, ) { val parentVisible = (this.parent as? View)?.isVisible ?: false + val msg = message.asMessage(isLightModeEnabled = appTheme.isLightModeEnabled()) val shouldRender = parentVisible && (newMessage || binding.messageCta.isGone) - if (shouldRender) { - binding.messageCta.setMessage(message.asMessage(isLightModeEnabled = appTheme.isLightModeEnabled())) + if (msg != null && shouldRender) { + binding.messageCta.setMessage(msg) binding.messageCta.onCloseButtonClicked { viewModel.onMessageCloseButtonClicked() } 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..8add42cb2250 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 @@ -32,6 +32,7 @@ import com.duckduckgo.common.utils.playstore.PlayStoreUtils import com.duckduckgo.mobile.android.app.tracking.AppTrackingProtection import com.duckduckgo.remote.messaging.api.RemoteMessage import com.duckduckgo.remote.messaging.api.RemoteMessageModel +import com.duckduckgo.remote.messaging.api.Surface import com.duckduckgo.savedsites.api.SavedSitesRepository import com.duckduckgo.savedsites.api.models.SavedSite.Favorite import com.duckduckgo.sync.api.engine.SyncEngine @@ -47,6 +48,7 @@ import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.flowOn import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.receiveAsFlow import kotlinx.coroutines.flow.update @@ -123,7 +125,12 @@ class NewTabLegacyPageViewModel @AssistedInject constructor( viewModelScope.launch(dispatchers.io()) { savedSitesRepository.getFavorites() - .combine(remoteMessagingModel.getActiveMessages()) { favorites, activeMessage -> + .combine( + remoteMessagingModel.getActiveMessages() + .map { message -> + if (message?.surfaces?.contains(Surface.NEW_TAB_PAGE) == true) message else null + }, + ) { favorites, activeMessage -> if (favorites.isNotEmpty()) { syncEngine.triggerSync(FEATURE_READ) } 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..1a8931bd6ee6 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.SubmitUrl(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..85c85a4d4d09 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 @@ -29,14 +29,17 @@ 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 import com.duckduckgo.remote.messaging.api.RemoteMessage -fun RemoteMessage.asMessage(isLightModeEnabled: Boolean): Message { +fun RemoteMessage.asMessage(isLightModeEnabled: Boolean): Message? { return when (val content = this.content) { is Small -> Message( title = content.titleText, @@ -71,6 +74,7 @@ fun RemoteMessage.asMessage(isLightModeEnabled: Boolean): Message { promoAction = content.actionText, messageType = MessageType.REMOTE_PROMO_MESSAGE, ) + else -> null } } @@ -89,5 +93,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/app/src/test/java/com/duckduckgo/app/browser/newtab/NewTabLegacyPageViewModelTest.kt b/app/src/test/java/com/duckduckgo/app/browser/newtab/NewTabLegacyPageViewModelTest.kt index 6233ba277d59..bfff5b293171 100644 --- a/app/src/test/java/com/duckduckgo/app/browser/newtab/NewTabLegacyPageViewModelTest.kt +++ b/app/src/test/java/com/duckduckgo/app/browser/newtab/NewTabLegacyPageViewModelTest.kt @@ -34,6 +34,7 @@ import com.duckduckgo.remote.messaging.api.Action import com.duckduckgo.remote.messaging.api.Content import com.duckduckgo.remote.messaging.api.RemoteMessage import com.duckduckgo.remote.messaging.api.RemoteMessageModel +import com.duckduckgo.remote.messaging.api.Surface import com.duckduckgo.savedsites.api.SavedSitesRepository import com.duckduckgo.savedsites.api.models.SavedSite import com.duckduckgo.sync.api.engine.SyncEngine @@ -101,7 +102,7 @@ class NewTabLegacyPageViewModelTest { @Test fun whenViewModelIsInitializedThenViewStateShouldEmitInitialState() = runTest { - val remoteMessage = RemoteMessage("id1", Content.Small("", ""), emptyList(), emptyList(), emptyList()) + val remoteMessage = RemoteMessage("id1", Content.Small("", ""), emptyList(), emptyList(), listOf(Surface.NEW_TAB_PAGE)) whenever(mockRemoteMessageModel.getActiveMessages()).thenReturn(flowOf(remoteMessage)) whenever(mockDismissedCtaDao.exists(DAX_END)).thenReturn(false) @@ -119,7 +120,7 @@ class NewTabLegacyPageViewModelTest { @Test fun whenRemoteMessageAvailableAndOnboardingNotCompleteThenMessageNotShown() = runTest { - val remoteMessage = RemoteMessage("id1", Content.Small("", ""), emptyList(), emptyList(), emptyList()) + val remoteMessage = RemoteMessage("id1", Content.Small("", ""), emptyList(), emptyList(), listOf(Surface.NEW_TAB_PAGE)) whenever(mockRemoteMessageModel.getActiveMessages()).thenReturn(flowOf(remoteMessage)) whenever(mockDismissedCtaDao.exists(DAX_END)).thenReturn(false) @@ -137,7 +138,7 @@ class NewTabLegacyPageViewModelTest { @Test fun whenRemoteMessageAvailableAndOnboardingCompleteThenMessageShown() = runTest { - val remoteMessage = RemoteMessage("id1", Content.Small("", ""), emptyList(), emptyList(), emptyList()) + val remoteMessage = RemoteMessage("id1", Content.Small("", ""), emptyList(), emptyList(), listOf(Surface.NEW_TAB_PAGE)) whenever(mockRemoteMessageModel.getActiveMessages()).thenReturn(flowOf(remoteMessage)) whenever(mockDismissedCtaDao.exists(DAX_END)).thenReturn(true) @@ -155,7 +156,7 @@ class NewTabLegacyPageViewModelTest { @Test fun whenRemoteMessageShownThenFirePixelAndMarkAsShown() = runTest { - val remoteMessage = RemoteMessage("id1", Content.Small("", ""), emptyList(), emptyList(), emptyList()) + val remoteMessage = RemoteMessage("id1", Content.Small("", ""), emptyList(), emptyList(), listOf(Surface.NEW_TAB_PAGE)) whenever(mockRemoteMessageModel.getActiveMessages()).thenReturn(flowOf(remoteMessage)) testee.onStart(mockLifecycleOwner) @@ -167,7 +168,7 @@ class NewTabLegacyPageViewModelTest { @Test fun whenRemoteMessageCloseButtonClickedThenFirePixelAndDismiss() = runTest { - val remoteMessage = RemoteMessage("id1", Content.Small("", ""), emptyList(), emptyList(), emptyList()) + val remoteMessage = RemoteMessage("id1", Content.Small("", ""), emptyList(), emptyList(), listOf(Surface.NEW_TAB_PAGE)) whenever(mockRemoteMessageModel.getActiveMessages()).thenReturn(flowOf(remoteMessage)) testee.onStart(mockLifecycleOwner) @@ -179,7 +180,7 @@ class NewTabLegacyPageViewModelTest { @Test fun whenRemoteMessagePrimaryButtonClickedThenFirePixelAndDismiss() = runTest { - val remoteMessage = RemoteMessage("id1", Content.Small("", ""), emptyList(), emptyList(), emptyList()) + val remoteMessage = RemoteMessage("id1", Content.Small("", ""), emptyList(), emptyList(), listOf(Surface.NEW_TAB_PAGE)) whenever(mockRemoteMessageModel.getActiveMessages()).thenReturn(flowOf(remoteMessage)) val action = Action.Dismiss @@ -199,7 +200,7 @@ class NewTabLegacyPageViewModelTest { @Test fun whenRemoteMessageSecondaryButtonClickedThenFirePixelAndDismiss() = runTest { - val remoteMessage = RemoteMessage("id1", Content.Small("", ""), emptyList(), emptyList(), emptyList()) + val remoteMessage = RemoteMessage("id1", Content.Small("", ""), emptyList(), emptyList(), listOf(Surface.NEW_TAB_PAGE)) whenever(mockRemoteMessageModel.getActiveMessages()).thenReturn(flowOf(remoteMessage)) val action = Action.Dismiss @@ -219,7 +220,7 @@ class NewTabLegacyPageViewModelTest { @Test fun whenRemoteMessageActionButtonClickedThenFirePixelAndDismiss() = runTest { - val remoteMessage = RemoteMessage("id1", Content.Small("", ""), emptyList(), emptyList(), emptyList()) + val remoteMessage = RemoteMessage("id1", Content.Small("", ""), emptyList(), emptyList(), listOf(Surface.NEW_TAB_PAGE)) whenever(mockRemoteMessageModel.getActiveMessages()).thenReturn(flowOf(remoteMessage)) val action = Action.Dismiss @@ -252,7 +253,7 @@ class NewTabLegacyPageViewModelTest { @Test fun whenRemoteMessageAvailableAndLowPriorityMessageAvailableThenLowPriorityMessageIsNull() = runTest { - val remoteMessage = RemoteMessage("id1", Content.Small("", ""), emptyList(), emptyList(), emptyList()) + val remoteMessage = RemoteMessage("id1", Content.Small("", ""), emptyList(), emptyList(), listOf(Surface.NEW_TAB_PAGE)) val lowPriorityMessage = LowPriorityMessage.DefaultBrowserMessage( message = MessageCta.Message( topIllustration = R.drawable.ic_device_mobile_default, @@ -372,7 +373,7 @@ class NewTabLegacyPageViewModelTest { @Test fun `when onboarding complete and RMF available, then hide logo`() = runTest { - val remoteMessage = RemoteMessage("id1", Content.Small("", ""), emptyList(), emptyList(), emptyList()) + val remoteMessage = RemoteMessage("id1", Content.Small("", ""), emptyList(), emptyList(), listOf(Surface.NEW_TAB_PAGE)) whenever(mockRemoteMessageModel.getActiveMessages()).thenReturn(flowOf(remoteMessage)) whenever(mockDismissedCtaDao.exists(DAX_END)).thenReturn(true) @@ -476,4 +477,51 @@ class NewTabLegacyPageViewModelTest { } } } + + @Test + fun `when remote message available with MODAL surface then show logo, not the message`() = runTest { + val remoteMessage = RemoteMessage("id1", Content.Small("", ""), emptyList(), emptyList(), listOf(Surface.MODAL)) + whenever(mockRemoteMessageModel.getActiveMessages()).thenReturn(flowOf(remoteMessage)) + whenever(mockDismissedCtaDao.exists(DAX_END)).thenReturn(true) + + testee.onStart(mockLifecycleOwner) + + testee.viewState.test { + expectMostRecentItem().also { + assertTrue(it.shouldShowLogo) + assertNull(it.message) + } + } + } + + @Test + fun `when remote message available with NEW_TAB_PAGE surface then show the message, not the logo`() = runTest { + val remoteMessage = RemoteMessage("id1", Content.Small("", ""), emptyList(), emptyList(), listOf(Surface.NEW_TAB_PAGE)) + whenever(mockRemoteMessageModel.getActiveMessages()).thenReturn(flowOf(remoteMessage)) + whenever(mockDismissedCtaDao.exists(DAX_END)).thenReturn(true) + + testee.onStart(mockLifecycleOwner) + + testee.viewState.test { + expectMostRecentItem().also { + assertFalse(it.shouldShowLogo) + assertEquals(remoteMessage, it.message) + } + } + } + + @Test + fun `when no remote message available then show the logo`() = runTest { + whenever(mockRemoteMessageModel.getActiveMessages()).thenReturn(flowOf(null)) + whenever(mockDismissedCtaDao.exists(DAX_END)).thenReturn(true) + + testee.onStart(mockLifecycleOwner) + + testee.viewState.test { + expectMostRecentItem().also { + assertTrue(it.shouldShowLogo) + assertNull(it.message) + } + } + } } 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..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 @@ -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") 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 f5a5e784e56d..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 @@ -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 @@ -73,12 +74,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) { @@ -91,6 +102,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 { @@ -103,6 +117,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) @@ -128,3 +143,16 @@ 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"), +} diff --git a/remote-messaging/remote-messaging-impl/src/main/java/com/duckduckgo/remote/messaging/impl/AppRemoteMessagingRepository.kt b/remote-messaging/remote-messaging-impl/src/main/java/com/duckduckgo/remote/messaging/impl/AppRemoteMessagingRepository.kt index 95691965121e..e3a2b56708a2 100644 --- a/remote-messaging/remote-messaging-impl/src/main/java/com/duckduckgo/remote/messaging/impl/AppRemoteMessagingRepository.kt +++ b/remote-messaging/remote-messaging-impl/src/main/java/com/duckduckgo/remote/messaging/impl/AppRemoteMessagingRepository.kt @@ -19,6 +19,7 @@ package com.duckduckgo.remote.messaging.impl import com.duckduckgo.common.utils.DispatcherProvider import com.duckduckgo.remote.messaging.api.RemoteMessage import com.duckduckgo.remote.messaging.api.RemoteMessagingRepository +import com.duckduckgo.remote.messaging.api.Surface import com.duckduckgo.remote.messaging.impl.mappers.MessageMapper import com.duckduckgo.remote.messaging.store.RemoteMessageEntity import com.duckduckgo.remote.messaging.store.RemoteMessageEntity.Status @@ -69,7 +70,7 @@ class AppRemoteMessagingRepository( content = remoteMessage.content, emptyList(), emptyList(), - emptyList(), + listOf(Surface.NEW_TAB_PAGE), ) return remoteMessage } @@ -84,7 +85,7 @@ class AppRemoteMessagingRepository( content = message.content, emptyList(), emptyList(), - emptyList(), + listOf(Surface.NEW_TAB_PAGE), ) } } 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..bbf5b965aa22 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,20 @@ class UrlActionMapper @Inject constructor() : MessageActionMapperPlugin { } } +// TODO ANA: Uncomment the below annotation once we are able to use this action (i.e is fully implemented). +// @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 08a08bdb95b3..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 @@ -17,9 +17,12 @@ 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 @@ -31,6 +34,7 @@ import com.duckduckgo.remote.messaging.api.Surface 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 @@ -86,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), @@ -93,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( @@ -114,7 +130,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() } @@ -147,14 +163,32 @@ private fun JsonMessageAction.toAction(actionMappers: Set?.toListItems(actionMappers: Set): List { + return this?.map { jsonItem -> + CardItem( + id = jsonItem.id.failIfEmpty(), + type = jsonItem.type.toCardItemType(), + 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( @@ -181,5 +215,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..c639824b8fd0 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 @@ -29,14 +29,17 @@ 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 import com.duckduckgo.remote.messaging.api.RemoteMessage -fun RemoteMessage.asMessage(isLightModeEnabled: Boolean): Message { +fun RemoteMessage.asMessage(isLightModeEnabled: Boolean): Message? { return when (val content = this.content) { is Small -> Message( title = content.titleText, @@ -71,6 +74,7 @@ fun RemoteMessage.asMessage(isLightModeEnabled: Boolean): Message { promoAction = content.actionText, messageType = MessageType.REMOTE_PROMO_MESSAGE, ) + else -> null } } @@ -89,5 +93,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/mappers/RemoteMessagingConfigJsonMapper.kt b/remote-messaging/remote-messaging-impl/src/main/java/com/duckduckgo/remote/messaging/impl/mappers/RemoteMessagingConfigJsonMapper.kt index 80d97b93069c..935e102842ee 100644 --- a/remote-messaging/remote-messaging-impl/src/main/java/com/duckduckgo/remote/messaging/impl/mappers/RemoteMessagingConfigJsonMapper.kt +++ b/remote-messaging/remote-messaging-impl/src/main/java/com/duckduckgo/remote/messaging/impl/mappers/RemoteMessagingConfigJsonMapper.kt @@ -19,6 +19,7 @@ package com.duckduckgo.remote.messaging.impl.mappers import com.duckduckgo.appbuildconfig.api.AppBuildConfig import com.duckduckgo.remote.messaging.api.JsonToMatchingAttributeMapper import com.duckduckgo.remote.messaging.api.MessageActionMapperPlugin +import com.duckduckgo.remote.messaging.api.Surface import com.duckduckgo.remote.messaging.impl.models.JsonRemoteMessagingConfig import com.duckduckgo.remote.messaging.impl.models.RemoteConfig import logcat.LogPriority.INFO @@ -31,6 +32,10 @@ class RemoteMessagingConfigJsonMapper( ) { fun map(jsonRemoteMessagingConfig: JsonRemoteMessagingConfig): RemoteConfig { val messages = jsonRemoteMessagingConfig.messages.mapToRemoteMessage(appBuildConfig.deviceLocale, actionMappers) + // TODO ANA: Remove this mapping once the feature is fully implemented. + .mapNotNull { message -> + if (message.surfaces.isEmpty() || message.surfaces.contains(Surface.NEW_TAB_PAGE)) message else null + } logcat(INFO) { "RMF: messages parsed $messages" } val rules = jsonRemoteMessagingConfig.rules.mapToMatchingRules(matchingAttributeMappers) return RemoteConfig( 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 689445046a35..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 @@ -45,6 +45,7 @@ data class JsonContent( val secondaryAction: JsonMessageAction? = null, val actionText: String = "", val action: JsonMessageAction? = null, + val listItems: List? = null, ) data class JsonContentTranslations( @@ -66,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") @@ -73,4 +83,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..b168b08990d9 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 -> submitUrl(command.url) } } @@ -147,12 +148,13 @@ class RemoteMessageView @JvmOverloads constructor( message: RemoteMessage, newMessage: Boolean, ) { + val msg = message.asMessage(isLightModeEnabled = appTheme.isLightModeEnabled()) val shouldRender = newMessage || binding.root.visibility == View.GONE - if (shouldRender) { + if (msg != null && shouldRender) { binding.messageCta.show() viewModel.onMessageShown() - binding.messageCta.setMessage(message.asMessage(isLightModeEnabled = appTheme.isLightModeEnabled())) + binding.messageCta.setMessage(msg) binding.messageCta.onCloseButtonClicked { viewModel.onMessageCloseButtonClicked() } 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..21da4b03fe7d 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,8 +34,10 @@ 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.remote.messaging.api.Surface import com.duckduckgo.survey.api.SurveyParameterManager import kotlinx.coroutines.channels.BufferOverflow import kotlinx.coroutines.channels.Channel @@ -44,6 +46,7 @@ import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.flowOn import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.receiveAsFlow import kotlinx.coroutines.launch @@ -68,6 +71,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( @@ -94,6 +98,9 @@ class RemoteMessageViewModel @Inject constructor( viewModelScope.launch(dispatchers.io()) { remoteMessagingModel.getActiveMessages() + .map { message -> + if (message?.surfaces?.contains(Surface.NEW_TAB_PAGE) == true) message else null + } .flowOn(dispatchers.io()) .onEach { message -> withContext(dispatchers.main()) { @@ -162,6 +169,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(), 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 0026ca328041..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 @@ -19,6 +19,7 @@ package com.duckduckgo.remote.messaging.fixtures 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 @@ -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 cf21c1b03c38..755fee195857 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 import com.duckduckgo.remote.messaging.api.Surface @@ -94,6 +98,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(), @@ -173,4 +210,20 @@ object RemoteMessageOM { surfaces = surfaces, ) } + + fun aCardsListMessage( + id: String = "id", + content: Content = cardsListContent(), + exclusionRules: List = emptyList(), + matchingRules: List = emptyList(), + surfaces: List = emptyList(), + ): RemoteMessage { + return RemoteMessage( + id = id, + content = content, + exclusionRules = exclusionRules, + matchingRules = matchingRules, + surfaces = surfaces, + ) + } } diff --git a/remote-messaging/remote-messaging-impl/src/test/java/com/duckduckgo/remote/messaging/impl/AppRemoteMessagingRepositoryTest.kt b/remote-messaging/remote-messaging-impl/src/test/java/com/duckduckgo/remote/messaging/impl/AppRemoteMessagingRepositoryTest.kt index 771cdf2e037c..f99d0c1705c5 100644 --- a/remote-messaging/remote-messaging-impl/src/test/java/com/duckduckgo/remote/messaging/impl/AppRemoteMessagingRepositoryTest.kt +++ b/remote-messaging/remote-messaging-impl/src/test/java/com/duckduckgo/remote/messaging/impl/AppRemoteMessagingRepositoryTest.kt @@ -31,6 +31,7 @@ import com.duckduckgo.remote.messaging.api.Content.Placeholder.MAC_AND_WINDOWS import com.duckduckgo.remote.messaging.api.Content.PromoSingleAction import com.duckduckgo.remote.messaging.api.Content.Small import com.duckduckgo.remote.messaging.api.RemoteMessage +import com.duckduckgo.remote.messaging.api.Surface import com.duckduckgo.remote.messaging.fixtures.getMessageMapper import com.duckduckgo.remote.messaging.store.RemoteMessageEntity.Status import com.duckduckgo.remote.messaging.store.RemoteMessagingConfigRepository @@ -86,7 +87,7 @@ class AppRemoteMessagingRepositoryTest { ), matchingRules = emptyList(), exclusionRules = emptyList(), - surfaces = emptyList(), + surfaces = listOf(Surface.NEW_TAB_PAGE), ), ) @@ -103,7 +104,7 @@ class AppRemoteMessagingRepositoryTest { ), matchingRules = emptyList(), exclusionRules = emptyList(), - surfaces = emptyList(), + surfaces = listOf(Surface.NEW_TAB_PAGE), ), message, ) @@ -122,7 +123,7 @@ class AppRemoteMessagingRepositoryTest { ), matchingRules = emptyList(), exclusionRules = emptyList(), - surfaces = emptyList(), + surfaces = listOf(Surface.NEW_TAB_PAGE), ), ) @@ -138,7 +139,7 @@ class AppRemoteMessagingRepositoryTest { ), matchingRules = emptyList(), exclusionRules = emptyList(), - surfaces = emptyList(), + surfaces = listOf(Surface.NEW_TAB_PAGE), ), message, ) @@ -160,7 +161,7 @@ class AppRemoteMessagingRepositoryTest { ), matchingRules = emptyList(), exclusionRules = emptyList(), - surfaces = emptyList(), + surfaces = listOf(Surface.NEW_TAB_PAGE), ), ) @@ -179,7 +180,7 @@ class AppRemoteMessagingRepositoryTest { ), matchingRules = emptyList(), exclusionRules = emptyList(), - surfaces = emptyList(), + surfaces = listOf(Surface.NEW_TAB_PAGE), ), message, ) @@ -203,7 +204,7 @@ class AppRemoteMessagingRepositoryTest { ), matchingRules = emptyList(), exclusionRules = emptyList(), - surfaces = emptyList(), + surfaces = listOf(Surface.NEW_TAB_PAGE), ), ) @@ -224,7 +225,7 @@ class AppRemoteMessagingRepositoryTest { ), matchingRules = emptyList(), exclusionRules = emptyList(), - surfaces = emptyList(), + surfaces = listOf(Surface.NEW_TAB_PAGE), ), message, ) @@ -246,7 +247,7 @@ class AppRemoteMessagingRepositoryTest { ), matchingRules = emptyList(), exclusionRules = emptyList(), - surfaces = emptyList(), + surfaces = listOf(Surface.NEW_TAB_PAGE), ), ) @@ -265,7 +266,7 @@ class AppRemoteMessagingRepositoryTest { ), matchingRules = emptyList(), exclusionRules = emptyList(), - surfaces = emptyList(), + surfaces = listOf(Surface.NEW_TAB_PAGE), ), message, ) @@ -436,7 +437,7 @@ class AppRemoteMessagingRepositoryTest { ), matchingRules = emptyList(), exclusionRules = emptyList(), - surfaces = emptyList(), + surfaces = listOf(Surface.NEW_TAB_PAGE), ) } } 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..a1ace46ff4e2 --- /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.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 +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 9eaae30cbdd2..5978868215ca 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 @@ -21,17 +21,20 @@ import com.duckduckgo.remote.messaging.api.Surface.NEW_TAB_PAGE 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 @@ -66,6 +69,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", surfaces = listOf(NEW_TAB_PAGE)), @@ -73,6 +77,7 @@ class JsonRemoteMessageMapperTest(private val testCase: TestCase) { aBigSingleActionMessage(id = "id3", surfaces = listOf(NEW_TAB_PAGE)), aBigTwoActionsMessage(id = "id4", surfaces = listOf(NEW_TAB_PAGE)), aPromoSingleActionMessage(id = "id5", surfaces = listOf(NEW_TAB_PAGE)), + aCardsListMessage(id = "id6", surfaces = listOf(NEW_TAB_PAGE)), ), ), TestCase( @@ -83,6 +88,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", surfaces = listOf(NEW_TAB_PAGE)), @@ -90,6 +96,7 @@ class JsonRemoteMessageMapperTest(private val testCase: TestCase) { aBigSingleActionMessage(id = "id4", surfaces = listOf(NEW_TAB_PAGE)), aBigTwoActionsMessage(id = "id5", surfaces = listOf(NEW_TAB_PAGE)), aPromoSingleActionMessage(id = "id6", surfaces = listOf(NEW_TAB_PAGE)), + aCardsListMessage(id = "id7", surfaces = listOf(NEW_TAB_PAGE)), ), ), TestCase( @@ -135,6 +142,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( @@ -178,6 +190,15 @@ class JsonRemoteMessageMapperTest(private val testCase: TestCase) { ), surfaces = listOf(NEW_TAB_PAGE), ), + aCardsListMessage( + id = "id6", + cardsListContent( + titleText = frenchTranslations().titleText, + descriptionText = frenchTranslations().descriptionText, + primaryActionText = frenchTranslations().primaryActionText, + ), + surfaces = listOf(NEW_TAB_PAGE), + ), ), ), ) diff --git a/remote-messaging/remote-messaging-impl/src/test/java/com/duckduckgo/remote/messaging/newtab/RemoteMessageViewModelTest.kt b/remote-messaging/remote-messaging-impl/src/test/java/com/duckduckgo/remote/messaging/newtab/RemoteMessageViewModelTest.kt index 01523d702ee6..490bcf0d59da 100644 --- a/remote-messaging/remote-messaging-impl/src/test/java/com/duckduckgo/remote/messaging/newtab/RemoteMessageViewModelTest.kt +++ b/remote-messaging/remote-messaging-impl/src/test/java/com/duckduckgo/remote/messaging/newtab/RemoteMessageViewModelTest.kt @@ -25,6 +25,7 @@ import com.duckduckgo.remote.messaging.api.Action.Survey import com.duckduckgo.remote.messaging.api.Content import com.duckduckgo.remote.messaging.api.RemoteMessage import com.duckduckgo.remote.messaging.api.RemoteMessageModel +import com.duckduckgo.remote.messaging.api.Surface import com.duckduckgo.remote.messaging.impl.newtab.RemoteMessageViewModel import com.duckduckgo.remote.messaging.impl.newtab.RemoteMessageViewModel.Command.SubmitUrl import com.duckduckgo.survey.api.SurveyParameterManager @@ -72,6 +73,18 @@ class RemoteMessageViewModelTest { } } + @Test + fun whenViewModelInitialisedWithMessageAndModalSurfaceThenViewStateEmitInitStateWithNoMessageToShow() = runTest { + val remoteMessage = RemoteMessage("id1", Content.Small("", ""), emptyList(), emptyList(), listOf(Surface.MODAL)) + whenever(remoteMessageModel.getActiveMessages()).thenReturn(flowOf(remoteMessage)) + testee.onStart(mockLifecycleOwner) + testee.viewState.test { + expectMostRecentItem().also { + assertFalse(it.newMessage) + } + } + } + @Test fun whenViewModelInitialisedThenViewStateEmitInitState() = runTest { val remoteMessage = whenRemoteMessageAvailable() @@ -87,7 +100,7 @@ class RemoteMessageViewModelTest { @Test fun whenMessageShownThenRemoteMessagingModelUpdated() = runTest { - val remoteMessage = RemoteMessage("id1", Content.Small("", ""), emptyList(), emptyList(), emptyList()) + val remoteMessage = RemoteMessage("id1", Content.Small("", ""), emptyList(), emptyList(), listOf(Surface.NEW_TAB_PAGE)) whenever(remoteMessageModel.getActiveMessages()).thenReturn(flowOf(remoteMessage)) testee.onStart(mockLifecycleOwner) @@ -105,7 +118,7 @@ class RemoteMessageViewModelTest { @Test fun whenMessagePrimaryButtonCLickedThenRemoteMessagingModelDismissed() = runTest { - val remoteMessage = RemoteMessage("id1", Content.Small("", ""), emptyList(), emptyList(), emptyList()) + val remoteMessage = RemoteMessage("id1", Content.Small("", ""), emptyList(), emptyList(), listOf(Surface.NEW_TAB_PAGE)) whenever(remoteMessageModel.getActiveMessages()).thenReturn(flowOf(remoteMessage)) whenever(remoteMessageModel.onPrimaryActionClicked(remoteMessage)).thenReturn(DefaultBrowser) testee.onStart(mockLifecycleOwner) @@ -120,7 +133,7 @@ class RemoteMessageViewModelTest { @Test fun whenMessageSecondaryButtonCLickedThenRemoteMessagingModelDismissed() = runTest { - val remoteMessage = RemoteMessage("id1", Content.Small("", ""), emptyList(), emptyList(), emptyList()) + val remoteMessage = RemoteMessage("id1", Content.Small("", ""), emptyList(), emptyList(), listOf(Surface.NEW_TAB_PAGE)) whenever(remoteMessageModel.getActiveMessages()).thenReturn(flowOf(remoteMessage)) whenever(remoteMessageModel.onSecondaryActionClicked(remoteMessage)).thenReturn(DefaultBrowser) testee.onStart(mockLifecycleOwner) @@ -135,7 +148,7 @@ class RemoteMessageViewModelTest { @Test fun whenMessageActionCLickedThenRemoteMessagingModelDismissed() = runTest { - val remoteMessage = RemoteMessage("id1", Content.Small("", ""), emptyList(), emptyList(), emptyList()) + val remoteMessage = RemoteMessage("id1", Content.Small("", ""), emptyList(), emptyList(), listOf(Surface.NEW_TAB_PAGE)) whenever(remoteMessageModel.getActiveMessages()).thenReturn(flowOf(remoteMessage)) whenever(remoteMessageModel.onActionClicked(remoteMessage)).thenReturn(DefaultBrowser) testee.onStart(mockLifecycleOwner) @@ -150,7 +163,7 @@ class RemoteMessageViewModelTest { @Test fun whenMessageActionClickedIsSurveyThenSubmitUrlCommandSent() = runTest { - val remoteMessage = RemoteMessage("id1", Content.Small("", ""), emptyList(), emptyList(), emptyList()) + val remoteMessage = RemoteMessage("id1", Content.Small("", ""), emptyList(), emptyList(), listOf(Surface.NEW_TAB_PAGE)) whenever(surveyParameterManager.buildSurveyUrl("https://example.com", listOf("atb"))).thenReturn("https://example.com?atb") whenever(remoteMessageModel.getActiveMessages()).thenReturn(flowOf(remoteMessage)) whenever(remoteMessageModel.onPrimaryActionClicked(remoteMessage)).thenReturn( @@ -175,7 +188,7 @@ class RemoteMessageViewModelTest { } private fun whenRemoteMessageAvailable(): RemoteMessage { - val remoteMessage = RemoteMessage("id1", Content.Small("", ""), emptyList(), emptyList(), emptyList()) + val remoteMessage = RemoteMessage("id1", Content.Small("", ""), emptyList(), emptyList(), listOf(Surface.NEW_TAB_PAGE)) whenever(remoteMessageModel.getActiveMessages()).thenReturn(flowOf(remoteMessage)) testee.onStart(mockLifecycleOwner) return remoteMessage