diff --git a/app/build.gradle.kts b/app/build.gradle.kts
index 33ba9e4946..838f2eac7c 100644
--- a/app/build.gradle.kts
+++ b/app/build.gradle.kts
@@ -332,6 +332,8 @@ dependencies {
implementation(projects.features.yieldSupply.impl)
implementation(projects.features.approval.api)
implementation(projects.features.approval.impl)
+ implementation(projects.features.forYou.api)
+ implementation(projects.features.forYou.impl)
/** AndroidX libraries */
implementation(deps.androidx.core.ktx)
diff --git a/core/res/src/main/res/values-de/strings.xml b/core/res/src/main/res/values-de/strings.xml
index f4e0b41e2d..525e778b71 100644
--- a/core/res/src/main/res/values-de/strings.xml
+++ b/core/res/src/main/res/values-de/strings.xml
@@ -1550,6 +1550,7 @@
Staking aktiviert
Zurzeit sind keine Validierer verfügbar. Bitte versuche es später noch einmal.
Staking nicht verfügbar
+ Staking ist in Ihrer Region nicht verfügbar.
Das Netzwerk erhebt eine Token-Genehmigungsgebühr, um zu überprüfen, ob Du die Verwendung Deines Tokens für das Staking genehmigst.
Indem Du die Staking-Funktionalität nutzt, stimmst Du den %1$s und %2$s des Anbieters zu.
Gesperrt
diff --git a/core/res/src/main/res/values-fr/strings.xml b/core/res/src/main/res/values-fr/strings.xml
index ec381bcad1..c1d9e544ef 100644
--- a/core/res/src/main/res/values-fr/strings.xml
+++ b/core/res/src/main/res/values-fr/strings.xml
@@ -1421,6 +1421,7 @@
Staking activé
Aucun validateur disponible pour le moment. Veuillez réessayer plus tard.
Staking indisponible
+ Le staking est indisponible dans votre région
Le réseau facturera des frais d’approbation de jeton pour vérifier que vous autorisez l’utilisation de votre jeton pour le jalonnement.
En utilisant la fonctionnalité de staking, vous acceptez les %1$s et %2$s du fournisseur
Bloqué
diff --git a/core/res/src/main/res/values-ja/strings.xml b/core/res/src/main/res/values-ja/strings.xml
index b851b50576..1a9a334e93 100644
--- a/core/res/src/main/res/values-ja/strings.xml
+++ b/core/res/src/main/res/values-ja/strings.xml
@@ -1520,6 +1520,7 @@
ステーキングが有効です
現在、利用可能なバリデーターは見つかりません。しばらくしてからもう一度お試しください。
ステーキングは利用できません
+ お住まいの地域ではステーキングをご利用いただけません
ネットワークは、ステーキングのためにトークンの使用を承認していることを確認するために、トークン承認手数料を請求します。
ステーキング機能を使用すると、プロバイダーの%1$sと%2$sに同意したことになります
ロック中
diff --git a/core/res/src/main/res/values-zh-rCN/strings.xml b/core/res/src/main/res/values-zh-rCN/strings.xml
index d74f43edfc..e1ea9ae35f 100644
--- a/core/res/src/main/res/values-zh-rCN/strings.xml
+++ b/core/res/src/main/res/values-zh-rCN/strings.xml
@@ -1512,6 +1512,7 @@
已启用质押
目前没有可用的验证节点。请稍后再试。
质押功能不可用
+ 您所在的地区暂不支持质押功能
网络将收取代币批准费,以验证您是否授权使用您的代币进行质押。
使用质押功能即表示您同意提供商的 %1$s 和 %2$s
已锁定
diff --git a/core/res/src/main/res/values/strings.xml b/core/res/src/main/res/values/strings.xml
index c0d7a0dfb5..a0ebd7f24c 100644
--- a/core/res/src/main/res/values/strings.xml
+++ b/core/res/src/main/res/values/strings.xml
@@ -709,6 +709,8 @@
Tangem feedback
Can\'t send a transaction
Coin description error
+ Review portfolio and explore earn opportunities
+ For You
Update now
Update the app to its latest version to ensure proper functionality
Update needed
diff --git a/features/feed/impl/build.gradle.kts b/features/feed/impl/build.gradle.kts
index e2d888a68b..48926c4ffd 100644
--- a/features/feed/impl/build.gradle.kts
+++ b/features/feed/impl/build.gradle.kts
@@ -25,6 +25,7 @@ dependencies {
api(projects.features.wallet.api)
api(projects.features.account.api)
api(projects.features.commonFeatures.api)
+ api(projects.features.forYou.api)
implementation(projects.features.promoBanners.api)
/* Data */
diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/DefaultFeedEntryComponent.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/DefaultFeedEntryComponent.kt
index 9df57ca897..bd3a3e0d26 100644
--- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/DefaultFeedEntryComponent.kt
+++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/DefaultFeedEntryComponent.kt
@@ -142,6 +142,10 @@ internal class DefaultFeedEntryComponent @AssistedInject constructor(
override fun openSearch(source: String) {
stackNavigation.bringToFront(FeedEntryChildFactory.Child.Search(source))
}
+
+ override fun openForYou() {
+ stackNavigation.bringToFront(FeedEntryChildFactory.Child.ForYou)
+ }
}
private val stack: Value> =
diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/FeedEntryChildFactory.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/FeedEntryChildFactory.kt
index b5814e9491..607cb93355 100644
--- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/FeedEntryChildFactory.kt
+++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/FeedEntryChildFactory.kt
@@ -20,6 +20,7 @@ import com.tangem.features.feed.components.news.details.DefaultNewsDetailsCompon
import com.tangem.features.feed.components.news.list.DefaultNewsListComponent
import com.tangem.features.feed.components.search.DefaultSearchComponent
import com.tangem.features.feed.model.market.list.state.SortByTypeUM
+import com.tangem.features.foryou.ForYouComponent
import com.tangem.features.promobanners.api.PromoBannersBlockComponent
import kotlinx.serialization.Serializable
import javax.inject.Inject
@@ -33,6 +34,7 @@ internal class FeedEntryChildFactory @Inject constructor(
private val manageFundsComponentFactory: ManageFundsComponent.Factory,
private val promoBannersBlockComponentFactory: PromoBannersBlockComponent.Factory,
private val designFeatureToggles: DesignFeatureToggles,
+ private val forYouComponentFactory: ForYouComponent.Factory,
) {
@Serializable
@@ -66,6 +68,10 @@ internal class FeedEntryChildFactory @Inject constructor(
@Serializable
@Immutable
data class Search(val source: String) : Child
+
+ @Serializable
+ @Immutable
+ data object ForYou : Child
}
@Suppress("LongMethod")
@@ -147,6 +153,10 @@ internal class FeedEntryChildFactory @Inject constructor(
onSeeAllMarketsClick = { feedEntryClickIntents.onMarketOpenClick(SortByTypeUM.Rating) },
),
)
+ Child.ForYou -> forYouComponentFactory.create(
+ context = appComponentContext,
+ params = Unit,
+ )
}
}
}
\ No newline at end of file
diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/feed/FeedComponentModel.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/feed/FeedComponentModel.kt
index a0cb8e55db..515c813659 100644
--- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/feed/FeedComponentModel.kt
+++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/feed/FeedComponentModel.kt
@@ -14,7 +14,12 @@ import com.tangem.core.decompose.di.ModelScoped
import com.tangem.core.decompose.model.Model
import com.tangem.core.decompose.model.ParamsContainer
import com.tangem.core.ui.DesignFeatureToggles
+import com.tangem.core.ui.ds.image.TangemIconUM
+import com.tangem.core.ui.ds.message.TangemMessageEffect
+import com.tangem.core.ui.ds.message.TangemMessageUM
import com.tangem.core.ui.extensions.resourceReference
+import com.tangem.core.ui.res.generated.icons.Icons
+import com.tangem.core.ui.res.generated.icons.ic_heart_28
import com.tangem.core.ui.utils.DateTimeFormatters
import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase
import com.tangem.domain.appcurrency.model.AppCurrency
@@ -37,6 +42,7 @@ import com.tangem.features.feed.model.feed.state.transformers.*
import com.tangem.features.feed.model.market.list.state.SortByTypeUM
import com.tangem.features.feed.ui.earn.state.EarnListUM
import com.tangem.features.feed.ui.feed.state.*
+import com.tangem.features.foryou.ForYouFeatureToggles
import com.tangem.utils.Provider
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import kotlinx.collections.immutable.persistentListOf
@@ -61,6 +67,7 @@ internal class FeedComponentModel @Inject constructor(
private val appRouter: AppRouter,
private val designFeatureToggles: DesignFeatureToggles,
private val addToPortfolioManagerFactory: AddToPortfolioManager.Factory,
+ private val forYouFeatureToggles: ForYouFeatureToggles,
getTopFiveMarketTokenUseCase: GetTopFiveMarketTokenUseCase,
getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase,
paramsContainer: ParamsContainer,
@@ -273,6 +280,20 @@ internal class FeedComponentModel @Inject constructor(
),
globalState = GlobalFeedState.Loading,
earnListUM = EarnListUM.Loading,
+ forYouBannerUM = if (forYouFeatureToggles.isForYouEnabled) {
+ ForYouBannerUM.Content(
+ TangemMessageUM(
+ id = ForYouBannerUM.Content::class.java.simpleName,
+ title = resourceReference(R.string.for_you_title),
+ subtitle = resourceReference(R.string.for_you_description),
+ iconUM = TangemIconUM.Icon(Icons.ic_heart_28), // TODO ForYou update icon,
+ messageEffect = TangemMessageEffect.Magic,
+ onClick = params.feedClickIntents::openForYou,
+ ),
+ )
+ } else {
+ ForYouBannerUM.Empty
+ },
)
}
diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/feed/FeedModelClickIntents.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/feed/FeedModelClickIntents.kt
index 7c81362bf3..766575f273 100644
--- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/feed/FeedModelClickIntents.kt
+++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/feed/FeedModelClickIntents.kt
@@ -31,4 +31,6 @@ internal interface FeedModelClickIntents {
fun onOpenEarnPage()
fun openSearch(source: String)
+
+ fun openForYou()
}
\ No newline at end of file
diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/feed/state/FeedStateController.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/feed/state/FeedStateController.kt
index 73fe72d6b2..6e7f856229 100644
--- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/feed/state/FeedStateController.kt
+++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/feed/state/FeedStateController.kt
@@ -65,6 +65,7 @@ internal class FeedStateController @Inject constructor() {
),
globalState = GlobalFeedState.Loading,
earnListUM = EarnListUM.Loading,
+ forYouBannerUM = ForYouBannerUM.Empty,
)
}
}
\ No newline at end of file
diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/feed/FeedList.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/feed/FeedList.kt
index 22fb987794..64176e2ffa 100644
--- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/feed/FeedList.kt
+++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/feed/FeedList.kt
@@ -16,6 +16,7 @@ import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import com.tangem.core.ui.components.SpacerH
import com.tangem.core.ui.decompose.ComposableContentComponent
+import com.tangem.core.ui.ds.message.TangemMessage
import com.tangem.core.ui.res.LocalMainBottomSheetColor
import com.tangem.core.ui.res.LocalRedesignEnabled
import com.tangem.core.ui.res.TangemThemePreview
@@ -27,6 +28,7 @@ import com.tangem.features.feed.ui.feed.components.*
import com.tangem.features.feed.ui.feed.preview.FeedListPreviewDataProvider.createFeedPreviewState
import com.tangem.features.feed.ui.feed.state.FeedListSearchBar
import com.tangem.features.feed.ui.feed.state.FeedListUM
+import com.tangem.features.feed.ui.feed.state.ForYouBannerUM
import com.tangem.features.feed.ui.feed.state.GlobalFeedState
@Composable
@@ -105,6 +107,17 @@ private fun FeedListContent(
SpacerH(contentPadding.calculateTopPadding())
}
DateBlock(state.currentDate)
+
+ SpacerH(16.dp)
+
+ if (state.forYouBannerUM is ForYouBannerUM.Content && LocalRedesignEnabled.current) {
+ // TODO ForYou replace with message banner
+ TangemMessage(
+ messageUM = state.forYouBannerUM.banner,
+ modifier = Modifier.padding(horizontal = 16.dp),
+ )
+ }
+
SpacerH(32.dp)
MarketBlock(
diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/feed/preview/FeedListPreviewDataProvider.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/feed/preview/FeedListPreviewDataProvider.kt
index ed2ebcdccb..4648ee5d8b 100644
--- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/feed/preview/FeedListPreviewDataProvider.kt
+++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/feed/preview/FeedListPreviewDataProvider.kt
@@ -7,9 +7,15 @@ import com.tangem.core.ui.components.currency.icon.CurrencyIconState
import com.tangem.core.ui.components.label.entity.LabelLeadingContentUM
import com.tangem.core.ui.components.label.entity.LabelUM
import com.tangem.core.ui.components.marketprice.PriceChangeType
+import com.tangem.core.ui.ds.image.TangemIconUM
+import com.tangem.core.ui.ds.message.TangemMessageEffect
+import com.tangem.core.ui.ds.message.TangemMessageUM
import com.tangem.core.ui.extensions.TextReference
+import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.extensions.stringReference
import com.tangem.core.ui.res.TangemColorPalette
+import com.tangem.core.ui.res.generated.icons.Icons
+import com.tangem.core.ui.res.generated.icons.ic_heart_28
import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.domain.models.earn.EarnType
import com.tangem.features.feed.model.market.list.state.SortByTypeUM
@@ -56,6 +62,7 @@ internal object FeedListPreviewDataProvider {
earnListUM = EarnListUM.Content(
items = createEarnListItemsUM(),
),
+ forYouBannerUM = createForYouItem(),
)
}
@@ -285,4 +292,17 @@ internal object FeedListPreviewDataProvider {
)
}.toPersistentList()
}
+
+ private fun createForYouItem(): ForYouBannerUM {
+ return ForYouBannerUM.Content(
+ TangemMessageUM(
+ id = ForYouBannerUM.Content::class.java.simpleName,
+ title = resourceReference(R.string.for_you_title),
+ subtitle = resourceReference(R.string.for_you_description),
+ iconUM = TangemIconUM.Icon(Icons.ic_heart_28), // TODO ForYou update icon,
+ messageEffect = TangemMessageEffect.Magic,
+ onClick = {},
+ ),
+ )
+ }
}
\ No newline at end of file
diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/feed/state/FeedListUM.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/feed/state/FeedListUM.kt
index 8ba119a2d1..bd522d2562 100644
--- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/feed/state/FeedListUM.kt
+++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/feed/state/FeedListUM.kt
@@ -2,10 +2,11 @@ package com.tangem.features.feed.ui.feed.state
import androidx.compose.runtime.Immutable
import com.tangem.common.ui.markets.models.MarketsListItemUM
-import com.tangem.features.feed.ui.feed.components.articles.ArticleConfigUM
+import com.tangem.core.ui.ds.message.TangemMessageUM
import com.tangem.core.ui.extensions.TextReference
import com.tangem.features.feed.model.market.list.state.SortByTypeUM
import com.tangem.features.feed.ui.earn.state.EarnListUM
+import com.tangem.features.feed.ui.feed.components.articles.ArticleConfigUM
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.ImmutableMap
import kotlinx.collections.immutable.persistentListOf
@@ -19,6 +20,7 @@ internal data class FeedListUM(
val marketChartConfig: MarketChartConfig,
val globalState: GlobalFeedState = GlobalFeedState.Content,
val earnListUM: EarnListUM,
+ val forYouBannerUM: ForYouBannerUM,
)
internal data class FeedListCallbacks(
@@ -80,6 +82,16 @@ internal data class SortChartConfigUM(
val isSelected: Boolean,
)
+@Immutable
+internal sealed interface ForYouBannerUM {
+
+ data class Content(
+ val banner: TangemMessageUM,
+ ) : ForYouBannerUM
+
+ data object Empty : ForYouBannerUM
+}
+
@Immutable
internal sealed interface GlobalFeedState {
data object Loading : GlobalFeedState
diff --git a/features/feed/impl/src/test/kotlin/com/tangem/features/feed/model/feed/FeedComponentModelTest.kt b/features/feed/impl/src/test/kotlin/com/tangem/features/feed/model/feed/FeedComponentModelTest.kt
new file mode 100644
index 0000000000..d00fdc6b7d
--- /dev/null
+++ b/features/feed/impl/src/test/kotlin/com/tangem/features/feed/model/feed/FeedComponentModelTest.kt
@@ -0,0 +1,166 @@
+package com.tangem.features.feed.model.feed
+
+import android.text.format.DateFormat
+import arrow.core.Either
+import com.google.common.truth.Truth.assertThat
+import com.tangem.common.routing.AppRouter
+import com.tangem.core.analytics.api.AnalyticsEventHandler
+import com.tangem.core.decompose.model.MutableParamsContainer
+import com.tangem.core.ui.DesignFeatureToggles
+import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase
+import com.tangem.domain.appcurrency.model.AppCurrency
+import com.tangem.domain.earn.usecase.FetchTopEarnTokensUseCase
+import com.tangem.domain.earn.usecase.GetTopEarnTokensUseCase
+import com.tangem.domain.markets.GetTopFiveMarketTokenUseCase
+import com.tangem.domain.news.usecase.FetchTrendingNewsUseCase
+import com.tangem.domain.news.usecase.ManageTrendingNewsUseCase
+import com.tangem.features.commonfeatures.api.addtoportfolio.AddToPortfolioManager
+import com.tangem.features.feed.components.feed.DefaultFeedComponent.FeedParams
+import com.tangem.features.feed.model.feed.state.FeedStateController
+import com.tangem.features.feed.ui.feed.state.ForYouBannerUM
+import com.tangem.features.foryou.ForYouFeatureToggles
+import com.tangem.utils.coroutines.TestingCoroutineDispatcherProvider
+import io.mockk.every
+import io.mockk.mockk
+import io.mockk.mockkStatic
+import io.mockk.unmockkStatic
+import io.mockk.verify
+import kotlinx.coroutines.flow.emptyFlow
+import kotlinx.coroutines.flow.flowOf
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.test.StandardTestDispatcher
+import kotlinx.coroutines.test.TestScope
+import kotlinx.coroutines.test.advanceUntilIdle
+import kotlinx.coroutines.test.runTest
+import org.junit.jupiter.api.AfterEach
+import org.junit.jupiter.api.BeforeEach
+import org.junit.jupiter.api.Nested
+import org.junit.jupiter.api.Test
+import org.junit.jupiter.api.TestInstance
+
+@OptIn(ExperimentalCoroutinesApi::class)
+@TestInstance(TestInstance.Lifecycle.PER_CLASS)
+internal class FeedComponentModelTest {
+
+ // --- shared mocks ---
+ private val fetchTrendingNewsUseCase: FetchTrendingNewsUseCase = mockk(relaxed = true)
+ private val manageTrendingNewsUseCase: ManageTrendingNewsUseCase = mockk()
+ private val analyticsEventHandler: AnalyticsEventHandler = mockk(relaxed = true)
+ private val fetchTopEarnTokensUseCase: FetchTopEarnTokensUseCase = mockk(relaxed = true)
+ private val getTopEarnTokensUseCase: GetTopEarnTokensUseCase = mockk()
+ private val appRouter: AppRouter = mockk(relaxed = true)
+ private val designFeatureToggles: DesignFeatureToggles = mockk()
+ private val addToPortfolioManagerFactory: AddToPortfolioManager.Factory = mockk(relaxed = true)
+ private val getTopFiveMarketTokenUseCase: GetTopFiveMarketTokenUseCase = mockk(relaxed = true)
+ private val getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase = mockk()
+ private val feedClickIntents: FeedModelClickIntents = mockk(relaxed = true)
+
+ @BeforeEach
+ fun setUpDateFormatMock() {
+ // DateTimeFormatters.dateDMMM is a lazy val that calls android.text.format.DateFormat
+ // .getBestDateTimePattern — an Android stub not available in JVM unit tests.
+ // Mirror the pattern used in TxHistoryInfoToTxHistoryDetailsUMConverterTest.
+ mockkStatic(DateFormat::class)
+ every { DateFormat.getBestDateTimePattern(any(), any()) } answers { secondArg() }
+ }
+
+ @AfterEach
+ fun tearDownDateFormatMock() {
+ unmockkStatic(DateFormat::class)
+ }
+
+ /**
+ * Builds a [FeedComponentModel] wired into the given [TestScope], using a real
+ * [FeedStateController] so we can read the initialised state directly.
+ *
+ * All deps unrelated to [ForYouFeatureToggles] are relaxed or stubbed with empty flows so
+ * the model's background coroutines don't throw.
+ */
+ private fun TestScope.createModel(forYouFeatureToggles: ForYouFeatureToggles): FeedComponentModel {
+ val testDispatcher = StandardTestDispatcher(testScheduler)
+ val dispatchers = TestingCoroutineDispatcherProvider(
+ main = testDispatcher,
+ mainImmediate = testDispatcher,
+ io = testDispatcher,
+ default = testDispatcher,
+ single = testDispatcher,
+ )
+
+ every { getSelectedAppCurrencyUseCase() } returns flowOf(Either.Right(AppCurrency.Default))
+ every { manageTrendingNewsUseCase.observeTrendingNews() } returns emptyFlow()
+ every { getTopEarnTokensUseCase() } returns emptyFlow()
+ every { designFeatureToggles.isRedesignEnabled } returns false
+
+ val paramsContainer = MutableParamsContainer(FeedParams(feedClickIntents = feedClickIntents))
+
+ return FeedComponentModel(
+ dispatchers = dispatchers,
+ fetchTrendingNewsUseCase = fetchTrendingNewsUseCase,
+ manageTrendingNewsUseCase = manageTrendingNewsUseCase,
+ analyticsEventHandler = analyticsEventHandler,
+ stateController = FeedStateController(),
+ fetchTopEarnTokensUseCase = fetchTopEarnTokensUseCase,
+ getTopEarnTokensUseCase = getTopEarnTokensUseCase,
+ appRouter = appRouter,
+ designFeatureToggles = designFeatureToggles,
+ addToPortfolioManagerFactory = addToPortfolioManagerFactory,
+ forYouFeatureToggles = forYouFeatureToggles,
+ getTopFiveMarketTokenUseCase = getTopFiveMarketTokenUseCase,
+ getSelectedAppCurrencyUseCase = getSelectedAppCurrencyUseCase,
+ paramsContainer = paramsContainer,
+ )
+ }
+
+ @Nested
+ @TestInstance(TestInstance.Lifecycle.PER_CLASS)
+ inner class `initialState forYouBannerUM` {
+
+ @Test
+ fun `GIVEN isForYouEnabled is true WHEN model initialises THEN forYouBannerUM is Content`() = runTest {
+ // Arrange
+ val toggles = mockk { every { isForYouEnabled } returns true }
+
+ // Act
+ val model = createModel(forYouFeatureToggles = toggles)
+ advanceUntilIdle()
+
+ // Assert
+ assertThat(model.state.value.forYouBannerUM).isInstanceOf(ForYouBannerUM.Content::class.java)
+
+ model.onDestroy()
+ }
+
+ @Test
+ fun `GIVEN isForYouEnabled is false WHEN model initialises THEN forYouBannerUM is Empty`() = runTest {
+ // Arrange
+ val toggles = mockk { every { isForYouEnabled } returns false }
+
+ // Act
+ val model = createModel(forYouFeatureToggles = toggles)
+ advanceUntilIdle()
+
+ // Assert
+ assertThat(model.state.value.forYouBannerUM).isEqualTo(ForYouBannerUM.Empty)
+
+ model.onDestroy()
+ }
+
+ @Test
+ fun `GIVEN isForYouEnabled is true WHEN Content banner clicked THEN openForYou invoked`() = runTest {
+ // Arrange
+ val toggles = mockk { every { isForYouEnabled } returns true }
+
+ // Act
+ val model = createModel(forYouFeatureToggles = toggles)
+ advanceUntilIdle()
+ val banner = model.state.value.forYouBannerUM
+ (banner as? ForYouBannerUM.Content)?.banner?.onClick?.invoke()
+
+ // Assert – onClick must be wired to feedClickIntents::openForYou, not just any lambda
+ assertThat(banner).isInstanceOf(ForYouBannerUM.Content::class.java)
+ verify(exactly = 1) { feedClickIntents.openForYou() }
+
+ model.onDestroy()
+ }
+ }
+}
\ No newline at end of file
diff --git a/features/for-you/impl/build.gradle.kts b/features/for-you/impl/build.gradle.kts
index 86fa456438..93ffbfa390 100644
--- a/features/for-you/impl/build.gradle.kts
+++ b/features/for-you/impl/build.gradle.kts
@@ -23,6 +23,7 @@ dependencies {
implementation(deps.compose.ui)
implementation(deps.compose.foundation)
implementation(deps.lifecycle.compose)
+ implementation(deps.compose.material3)
/** DI */
implementation(deps.hilt.android)
diff --git a/features/for-you/impl/src/main/java/com/tangem/features/foryou/impl/DefaultForYouComponent.kt b/features/for-you/impl/src/main/java/com/tangem/features/foryou/impl/DefaultForYouComponent.kt
index 6962ef2efd..737af3b002 100644
--- a/features/for-you/impl/src/main/java/com/tangem/features/foryou/impl/DefaultForYouComponent.kt
+++ b/features/for-you/impl/src/main/java/com/tangem/features/foryou/impl/DefaultForYouComponent.kt
@@ -1,11 +1,27 @@
package com.tangem.features.foryou.impl
import androidx.compose.foundation.layout.PaddingValues
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.shape.CircleShape
+import androidx.compose.material3.Icon
+import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.State
import androidx.compose.ui.Modifier
+import androidx.compose.ui.draw.clip
+import androidx.compose.ui.graphics.vector.ImageVector
+import androidx.compose.ui.res.vectorResource
+import androidx.compose.ui.unit.dp
import com.tangem.core.decompose.context.AppComponentContext
+import com.tangem.core.ui.R
import com.tangem.core.ui.components.bottomsheets.state.BottomSheetState
+import com.tangem.core.ui.components.haze.hazeEffectTangem
+import com.tangem.core.ui.ds.topbar.TangemTopBar
+import com.tangem.core.ui.ds.topbar.TangemTopBarType
+import com.tangem.core.ui.extensions.clickableSingle
+import com.tangem.core.ui.extensions.resourceReference
+import com.tangem.core.ui.res.TangemTheme
import com.tangem.features.foryou.ForYouComponent
import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
@@ -18,7 +34,26 @@ internal class DefaultForYouComponent @AssistedInject constructor(
@Composable
override fun Title(bottomSheetState: State) {
- TODO("Not yet implemented")
+ TangemTopBar(
+ title = resourceReference(R.string.for_you_title),
+ type = TangemTopBarType.BottomSheet,
+ startContent = {
+ Icon(
+ imageVector = ImageVector.vectorResource(id = R.drawable.ic_arrow_back_28),
+ contentDescription = null,
+ tint = TangemTheme.colors3.icon.primary,
+ modifier = Modifier
+ .size(44.dp)
+ .clip(CircleShape)
+ .hazeEffectTangem { blurRadius = 8.dp }
+ .clickableSingle(
+ onClick = router::pop,
+ enabled = bottomSheetState.value == BottomSheetState.EXPANDED,
+ )
+ .padding(8.dp),
+ )
+ },
+ )
}
@Composable
@@ -27,7 +62,7 @@ internal class DefaultForYouComponent @AssistedInject constructor(
contentPadding: PaddingValues,
modifier: Modifier,
) {
- TODO("Not yet implemented")
+ Text("FOR YOU")
}
@AssistedFactory