Updated on 2026-08-14
This commit is contained in:
parent
48137d6199
commit
aae00c8dd5
18 changed files with 308 additions and 3 deletions
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -1550,6 +1550,7 @@
|
|||
<string name="staking_enabled">Staking aktiviert</string>
|
||||
<string name="staking_error_no_validators_message">Zurzeit sind keine Validierer verfügbar. Bitte versuche es später noch einmal.</string>
|
||||
<string name="staking_error_no_validators_title">Staking nicht verfügbar</string>
|
||||
<string name="staking_error_unavailable_region">Staking ist in Ihrer Region nicht verfügbar.</string>
|
||||
<string name="staking_give_permission_fee_footer">Das Netzwerk erhebt eine Token-Genehmigungsgebühr, um zu überprüfen, ob Du die Verwendung Deines Tokens für das Staking genehmigst.</string>
|
||||
<string name="staking_legal">Indem Du die Staking-Funktionalität nutzt, stimmst Du den %1$s und %2$s des Anbieters zu.</string>
|
||||
<string name="staking_locked">Gesperrt</string>
|
||||
|
|
|
|||
|
|
@ -1421,6 +1421,7 @@
|
|||
<string name="staking_enabled">Staking activé</string>
|
||||
<string name="staking_error_no_validators_message">Aucun validateur disponible pour le moment. Veuillez réessayer plus tard.</string>
|
||||
<string name="staking_error_no_validators_title">Staking indisponible</string>
|
||||
<string name="staking_error_unavailable_region">Le staking est indisponible dans votre région</string>
|
||||
<string name="staking_give_permission_fee_footer">Le réseau facturera des frais d’approbation de jeton pour vérifier que vous autorisez l’utilisation de votre jeton pour le jalonnement.</string>
|
||||
<string name="staking_legal">En utilisant la fonctionnalité de staking, vous acceptez les %1$s et %2$s du fournisseur</string>
|
||||
<string name="staking_locked">Bloqué</string>
|
||||
|
|
|
|||
|
|
@ -1520,6 +1520,7 @@
|
|||
<string name="staking_enabled">ステーキングが有効です</string>
|
||||
<string name="staking_error_no_validators_message">現在、利用可能なバリデーターは見つかりません。しばらくしてからもう一度お試しください。</string>
|
||||
<string name="staking_error_no_validators_title">ステーキングは利用できません</string>
|
||||
<string name="staking_error_unavailable_region">お住まいの地域ではステーキングをご利用いただけません</string>
|
||||
<string name="staking_give_permission_fee_footer">ネットワークは、ステーキングのためにトークンの使用を承認していることを確認するために、トークン承認手数料を請求します。</string>
|
||||
<string name="staking_legal">ステーキング機能を使用すると、プロバイダーの%1$sと%2$sに同意したことになります</string>
|
||||
<string name="staking_locked">ロック中</string>
|
||||
|
|
|
|||
|
|
@ -1512,6 +1512,7 @@
|
|||
<string name="staking_enabled">已启用质押</string>
|
||||
<string name="staking_error_no_validators_message">目前没有可用的验证节点。请稍后再试。</string>
|
||||
<string name="staking_error_no_validators_title">质押功能不可用</string>
|
||||
<string name="staking_error_unavailable_region">您所在的地区暂不支持质押功能</string>
|
||||
<string name="staking_give_permission_fee_footer">网络将收取代币批准费,以验证您是否授权使用您的代币进行质押。</string>
|
||||
<string name="staking_legal">使用质押功能即表示您同意提供商的 %1$s 和 %2$s</string>
|
||||
<string name="staking_locked">已锁定</string>
|
||||
|
|
|
|||
|
|
@ -115,10 +115,17 @@
|
|||
<string name="address_book_enter_address">Enter address</string>
|
||||
<string name="address_book_invalid_address_error">Invalid address</string>
|
||||
<string name="address_book_keep_editing">Keep editing</string>
|
||||
<string name="address_book_max_networks_alert_description">You can not create more than 20 addresses. Delete one to add new.</string>
|
||||
<string name="address_book_max_networks_alert_title">Can\'t add new address </string>
|
||||
<string name="address_book_name_empty_error">Contact name is required</string>
|
||||
<string name="address_book_name_invalid_chars_error">Contact name contains invalid characters</string>
|
||||
<string name="address_book_name_max_chars_error">Contact name must not exceed 50 characters</string>
|
||||
<string name="address_book_name_taken_error">That name is already taken on this wallet</string>
|
||||
<string name="address_book_new_contact">New contact</string>
|
||||
<string name="address_book_no_contacts">No contacts yet</string>
|
||||
<string name="address_book_no_contacts_description">Contacts added will appear here</string>
|
||||
<string name="address_book_remove_address">Remove address</string>
|
||||
<string name="address_book_save_to_wallet_title">Save to Wallet</string>
|
||||
<string name="address_book_save_wallet_to_description">This contact will be linked to this wallet’s address book.</string>
|
||||
<string name="address_book_select_network">Select network</string>
|
||||
<string name="address_book_title">Address book</string>
|
||||
|
|
@ -352,6 +359,7 @@
|
|||
<string name="common_get_token">Get token</string>
|
||||
<string name="common_go_to_provider">Go to provider</string>
|
||||
<string name="common_go_to_token">Go to token</string>
|
||||
<string name="common_go_to_verification">Go to verification</string>
|
||||
<string name="common_got_it">Got it</string>
|
||||
<string name="common_hide">Hide</string>
|
||||
<string name="common_hold_to">Hold to %s</string>
|
||||
|
|
@ -701,6 +709,8 @@
|
|||
<string name="feedback_subject_support_tangem">Tangem feedback</string>
|
||||
<string name="feedback_subject_tx_failed">Can\'t send a transaction</string>
|
||||
<string name="feedback_token_description_error">Coin description error</string>
|
||||
<string name="for_you_description">Review portfolio and explore earn opportunities</string>
|
||||
<string name="for_you_title">For You</string>
|
||||
<string name="force_update_action">Update now</string>
|
||||
<string name="force_update_banner_message">Update the app to its latest version to ensure proper functionality</string>
|
||||
<string name="force_update_banner_title">Update needed</string>
|
||||
|
|
@ -732,6 +742,8 @@
|
|||
<string name="hardware_wallet_key_feature_title">Key Generation</string>
|
||||
<string name="hardware_wallet_security_feature_description">All cryptographic operations happen inside the secure chip, certified against cloning and physical tampering.</string>
|
||||
<string name="hardware_wallet_security_feature_title">Hardware-Level Security</string>
|
||||
<string name="high_fee_warning_description">Network activity is high. You can continue now or try again later when fees may be lower.</string>
|
||||
<string name="high_fee_warning_title">Network fee is higher than usual</string>
|
||||
<string name="home_button_add_existing_wallet">Add Existing Wallet</string>
|
||||
<string name="home_button_create_new_wallet">Create New Wallet</string>
|
||||
<string name="home_button_order">Order Tangem</string>
|
||||
|
|
@ -1402,6 +1414,7 @@
|
|||
<string name="send_extras_hint_memo">Memo</string>
|
||||
<string name="send_fee_unreachable_error_text">Check your network connection</string>
|
||||
<string name="send_fee_unreachable_error_title">Network fee info unreachable</string>
|
||||
<string name="send_from_account_in_wallet_name">from %1$s in %2$s</string>
|
||||
<string name="send_from_title">You send</string>
|
||||
<string name="send_from_wallet_name">From %s</string>
|
||||
<string name="send_gas_limit">Gas limit</string>
|
||||
|
|
|
|||
|
|
@ -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 */
|
||||
|
|
|
|||
|
|
@ -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<ChildStack<FeedEntryChildFactory.Child, ComposableModularBottomSheetContentComponent>> =
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -31,4 +31,6 @@ internal interface FeedModelClickIntents {
|
|||
fun onOpenEarnPage()
|
||||
|
||||
fun openSearch(source: String)
|
||||
|
||||
fun openForYou()
|
||||
}
|
||||
|
|
@ -65,6 +65,7 @@ internal class FeedStateController @Inject constructor() {
|
|||
),
|
||||
globalState = GlobalFeedState.Loading,
|
||||
earnListUM = EarnListUM.Loading,
|
||||
forYouBannerUM = ForYouBannerUM.Empty,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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 = {},
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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<ForYouFeatureToggles> { 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<ForYouFeatureToggles> { 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<ForYouFeatureToggles> { 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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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<BottomSheetState>) {
|
||||
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
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue