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