Updated on 2026-08-14
This commit is contained in:
commit
d894698a9b
25 changed files with 597 additions and 24 deletions
|
|
@ -177,6 +177,10 @@ internal class FeedEntryChildFactory @Inject constructor(
|
||||||
token = TokenSummaryComponent.Token.Portfolio(currency),
|
token = TokenSummaryComponent.Token.Portfolio(currency),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun onAllEarnTokensClick() {
|
||||||
|
feedEntryClickIntents.onOpenEarnPage()
|
||||||
|
}
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,7 @@ interface ForYouComponent : ComposableModularBottomSheetContentComponent {
|
||||||
|
|
||||||
interface ForYouModelCallbacks {
|
interface ForYouModelCallbacks {
|
||||||
fun onTokenClick(userWalletId: UserWalletId, currency: CryptoCurrency)
|
fun onTokenClick(userWalletId: UserWalletId, currency: CryptoCurrency)
|
||||||
|
fun onAllEarnTokensClick()
|
||||||
}
|
}
|
||||||
|
|
||||||
interface Factory : ComponentFactory<Params, ForYouComponent>
|
interface Factory : ComponentFactory<Params, ForYouComponent>
|
||||||
|
|
|
||||||
|
|
@ -12,12 +12,18 @@ import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.draw.clip
|
import androidx.compose.ui.draw.clip
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||||
|
import com.arkivanov.decompose.ComponentContext
|
||||||
|
import com.arkivanov.decompose.extensions.compose.subscribeAsState
|
||||||
|
import com.arkivanov.decompose.router.slot.childSlot
|
||||||
|
import com.arkivanov.decompose.router.slot.dismiss
|
||||||
import com.tangem.core.decompose.context.AppComponentContext
|
import com.tangem.core.decompose.context.AppComponentContext
|
||||||
import com.tangem.core.decompose.context.child
|
import com.tangem.core.decompose.context.child
|
||||||
|
import com.tangem.core.decompose.context.childByContext
|
||||||
import com.tangem.core.decompose.model.getOrCreateModel
|
import com.tangem.core.decompose.model.getOrCreateModel
|
||||||
import com.tangem.core.ui.R
|
import com.tangem.core.ui.R
|
||||||
import com.tangem.core.ui.components.bottomsheets.state.BottomSheetState
|
import com.tangem.core.ui.components.bottomsheets.state.BottomSheetState
|
||||||
import com.tangem.core.ui.components.haze.hazeEffectTangem
|
import com.tangem.core.ui.components.haze.hazeEffectTangem
|
||||||
|
import com.tangem.core.ui.decompose.ComposableBottomSheetComponent
|
||||||
import com.tangem.core.ui.ds.topbar.TangemTopBar
|
import com.tangem.core.ui.ds.topbar.TangemTopBar
|
||||||
import com.tangem.core.ui.ds.topbar.TangemTopBarType
|
import com.tangem.core.ui.ds.topbar.TangemTopBarType
|
||||||
import com.tangem.core.ui.extensions.clickableSingle
|
import com.tangem.core.ui.extensions.clickableSingle
|
||||||
|
|
@ -25,7 +31,10 @@ import com.tangem.core.ui.extensions.resourceReference
|
||||||
import com.tangem.core.ui.res.TangemTheme
|
import com.tangem.core.ui.res.TangemTheme
|
||||||
import com.tangem.core.ui.res.generated.icons.Icons
|
import com.tangem.core.ui.res.generated.icons.Icons
|
||||||
import com.tangem.core.ui.res.generated.icons.ic_chevron_left_20
|
import com.tangem.core.ui.res.generated.icons.ic_chevron_left_20
|
||||||
|
import com.tangem.features.commonfeatures.api.addtoportfolio.AddToPortfolioComponent
|
||||||
|
import com.tangem.features.commonfeatures.api.managefunds.ManageFundsComponent
|
||||||
import com.tangem.features.foryou.ForYouComponent
|
import com.tangem.features.foryou.ForYouComponent
|
||||||
|
import com.tangem.features.foryou.impl.entity.ForYouBottomSheetConfig
|
||||||
import com.tangem.features.foryou.impl.model.ForYouModel
|
import com.tangem.features.foryou.impl.model.ForYouModel
|
||||||
import com.tangem.features.foryou.impl.ui.ForYouContent
|
import com.tangem.features.foryou.impl.ui.ForYouContent
|
||||||
import com.tangem.features.promobanners.api.PromoBannersBlockComponent
|
import com.tangem.features.promobanners.api.PromoBannersBlockComponent
|
||||||
|
|
@ -37,6 +46,8 @@ internal class DefaultForYouComponent @AssistedInject constructor(
|
||||||
@Assisted context: AppComponentContext,
|
@Assisted context: AppComponentContext,
|
||||||
@Assisted params: ForYouComponent.Params,
|
@Assisted params: ForYouComponent.Params,
|
||||||
private val promoBannersBlockComponentFactory: PromoBannersBlockComponent.Factory,
|
private val promoBannersBlockComponentFactory: PromoBannersBlockComponent.Factory,
|
||||||
|
private val addToPortfolioComponentFactory: AddToPortfolioComponent.Factory,
|
||||||
|
private val manageFundsComponentFactory: ManageFundsComponent.Factory,
|
||||||
) : AppComponentContext by context, ForYouComponent {
|
) : AppComponentContext by context, ForYouComponent {
|
||||||
|
|
||||||
private val model: ForYouModel = getOrCreateModel(params = params)
|
private val model: ForYouModel = getOrCreateModel(params = params)
|
||||||
|
|
@ -51,6 +62,18 @@ internal class DefaultForYouComponent @AssistedInject constructor(
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private val bottomSheetSlot = childSlot(
|
||||||
|
source = model.bottomSheetNavigation,
|
||||||
|
serializer = null,
|
||||||
|
handleBackButton = false,
|
||||||
|
childFactory = { config, componentContext ->
|
||||||
|
when (config) {
|
||||||
|
ForYouBottomSheetConfig.AddToPortfolio -> portfolioSelectorChild(componentContext)
|
||||||
|
is ForYouBottomSheetConfig.ManageFunds -> manageFundsChild(config, componentContext)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
override fun Title(bottomSheetState: State<BottomSheetState>) {
|
override fun Title(bottomSheetState: State<BottomSheetState>) {
|
||||||
TangemTopBar(
|
TangemTopBar(
|
||||||
|
|
@ -82,6 +105,7 @@ internal class DefaultForYouComponent @AssistedInject constructor(
|
||||||
modifier: Modifier,
|
modifier: Modifier,
|
||||||
) {
|
) {
|
||||||
val uiState by model.uiState.collectAsStateWithLifecycle()
|
val uiState by model.uiState.collectAsStateWithLifecycle()
|
||||||
|
val bottomSheet by bottomSheetSlot.subscribeAsState()
|
||||||
|
|
||||||
ForYouContent(
|
ForYouContent(
|
||||||
forYouUM = uiState,
|
forYouUM = uiState,
|
||||||
|
|
@ -90,8 +114,31 @@ internal class DefaultForYouComponent @AssistedInject constructor(
|
||||||
contentPadding = contentPadding,
|
contentPadding = contentPadding,
|
||||||
modifier = modifier,
|
modifier = modifier,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
bottomSheet.child?.instance?.BottomSheet()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun portfolioSelectorChild(componentContext: ComponentContext): ComposableBottomSheetComponent =
|
||||||
|
addToPortfolioComponentFactory.create(
|
||||||
|
context = childByContext(componentContext),
|
||||||
|
params = AddToPortfolioComponent.Params(
|
||||||
|
addToPortfolioManager = checkNotNull(model.addToPortfolioManager) {
|
||||||
|
"addToPortfolioManager must be set before activating AddToPortfolio slot"
|
||||||
|
},
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
private fun manageFundsChild(
|
||||||
|
config: ForYouBottomSheetConfig.ManageFunds,
|
||||||
|
componentContext: ComponentContext,
|
||||||
|
): ComposableBottomSheetComponent = manageFundsComponentFactory.create(
|
||||||
|
context = childByContext(componentContext),
|
||||||
|
params = ManageFundsComponent.Params(
|
||||||
|
launchMode = ManageFundsComponent.LaunchMode.FilteredByRawId(config.rawCurrencyId),
|
||||||
|
onDismiss = { model.bottomSheetNavigation.dismiss() },
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
@AssistedFactory
|
@AssistedFactory
|
||||||
interface Factory : ForYouComponent.Factory {
|
interface Factory : ForYouComponent.Factory {
|
||||||
override fun create(context: AppComponentContext, params: ForYouComponent.Params): DefaultForYouComponent
|
override fun create(context: AppComponentContext, params: ForYouComponent.Params): DefaultForYouComponent
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,12 @@
|
||||||
|
package com.tangem.features.foryou.impl.entity
|
||||||
|
|
||||||
|
import com.tangem.domain.models.currency.CryptoCurrency
|
||||||
|
|
||||||
|
internal sealed interface ForYouBottomSheetConfig {
|
||||||
|
|
||||||
|
data object AddToPortfolio : ForYouBottomSheetConfig
|
||||||
|
|
||||||
|
data class ManageFunds(
|
||||||
|
val rawCurrencyId: CryptoCurrency.RawID,
|
||||||
|
) : ForYouBottomSheetConfig
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,31 @@
|
||||||
|
package com.tangem.features.foryou.impl.entity
|
||||||
|
|
||||||
|
import com.tangem.domain.staking.model.StakingIntegrationID
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The earn product behind an earn-opportunities row. Resolved once per token when the row is built
|
||||||
|
* and carried through the click callback, so the model knows which earn screen to open
|
||||||
|
* ([com.tangem.common.routing.AppRoute.YieldSupplyEntry] or [com.tangem.common.routing.AppRoute.Staking])
|
||||||
|
* without re-resolving availability.
|
||||||
|
*/
|
||||||
|
internal sealed interface ForYouEarnOpportunitiesType {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Yield supply (yield module) opportunity.
|
||||||
|
*
|
||||||
|
* @property apy rate in percent as received from the backend (e.g. "5.5" = 5.5%), passed as-is
|
||||||
|
* to the yield-supply entry route
|
||||||
|
*/
|
||||||
|
data class YieldSupply(
|
||||||
|
val apy: String,
|
||||||
|
) : ForYouEarnOpportunitiesType
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Staking opportunity.
|
||||||
|
*
|
||||||
|
* @property integrationID staking integration to open the staking screen with
|
||||||
|
*/
|
||||||
|
data class Staking(
|
||||||
|
val integrationID: StakingIntegrationID,
|
||||||
|
) : ForYouEarnOpportunitiesType
|
||||||
|
}
|
||||||
|
|
@ -62,6 +62,7 @@ internal sealed interface EarnOpportunitiesUM {
|
||||||
@param:StringRes val subtitleRes: Int,
|
@param:StringRes val subtitleRes: Int,
|
||||||
val potentialReward: TextReference?,
|
val potentialReward: TextReference?,
|
||||||
val potentialRewardType: TextReference?,
|
val potentialRewardType: TextReference?,
|
||||||
|
val onAllEarnTokensClick: () -> Unit,
|
||||||
) : EarnOpportunitiesUM
|
) : EarnOpportunitiesUM
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,12 @@ import androidx.compose.runtime.Stable
|
||||||
import arrow.core.getOrElse
|
import arrow.core.getOrElse
|
||||||
import arrow.core.left
|
import arrow.core.left
|
||||||
import arrow.core.right
|
import arrow.core.right
|
||||||
|
import com.arkivanov.decompose.router.slot.SlotNavigation
|
||||||
|
import com.arkivanov.decompose.router.slot.activate
|
||||||
|
import com.arkivanov.decompose.router.slot.dismiss
|
||||||
|
import com.tangem.common.routing.AppRoute
|
||||||
|
import com.tangem.common.routing.AppRouter
|
||||||
|
import com.tangem.core.analytics.models.AnalyticsParam
|
||||||
import com.tangem.core.decompose.di.ModelScoped
|
import com.tangem.core.decompose.di.ModelScoped
|
||||||
import com.tangem.core.decompose.model.Model
|
import com.tangem.core.decompose.model.Model
|
||||||
import com.tangem.core.decompose.model.ParamsContainer
|
import com.tangem.core.decompose.model.ParamsContainer
|
||||||
|
|
@ -24,6 +30,7 @@ import com.tangem.domain.models.earn.EarnTopToken
|
||||||
import com.tangem.domain.models.wallet.UserWalletId
|
import com.tangem.domain.models.wallet.UserWalletId
|
||||||
import com.tangem.domain.staking.usecase.StakingAvailabilityListUseCase
|
import com.tangem.domain.staking.usecase.StakingAvailabilityListUseCase
|
||||||
import com.tangem.domain.yield.supply.usecase.YieldSupplyApyFlowUseCase
|
import com.tangem.domain.yield.supply.usecase.YieldSupplyApyFlowUseCase
|
||||||
|
import com.tangem.features.commonfeatures.api.addtoportfolio.AddToPortfolioManager
|
||||||
import com.tangem.features.foryou.ForYouComponent
|
import com.tangem.features.foryou.ForYouComponent
|
||||||
import com.tangem.features.foryou.impl.components.state.MarketChartUM
|
import com.tangem.features.foryou.impl.components.state.MarketChartUM
|
||||||
import com.tangem.features.foryou.impl.entity.*
|
import com.tangem.features.foryou.impl.entity.*
|
||||||
|
|
@ -38,7 +45,11 @@ import com.tangem.utils.coroutines.combine6
|
||||||
import com.tangem.utils.transformer.update
|
import com.tangem.utils.transformer.update
|
||||||
import kotlinx.collections.immutable.persistentListOf
|
import kotlinx.collections.immutable.persistentListOf
|
||||||
import kotlinx.collections.immutable.toPersistentList
|
import kotlinx.collections.immutable.toPersistentList
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.SupervisorJob
|
||||||
|
import kotlinx.coroutines.cancel
|
||||||
import kotlinx.coroutines.flow.*
|
import kotlinx.coroutines.flow.*
|
||||||
|
import kotlinx.coroutines.job
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
@Stable
|
@Stable
|
||||||
|
|
@ -49,12 +60,14 @@ internal class ForYouModel @Inject constructor(
|
||||||
userWalletsListRepository: UserWalletsListRepository,
|
userWalletsListRepository: UserWalletsListRepository,
|
||||||
multiAccountStatusListSupplier: MultiAccountStatusListSupplier,
|
multiAccountStatusListSupplier: MultiAccountStatusListSupplier,
|
||||||
yieldSupplyApyFlowUseCase: YieldSupplyApyFlowUseCase,
|
yieldSupplyApyFlowUseCase: YieldSupplyApyFlowUseCase,
|
||||||
|
private val router: AppRouter,
|
||||||
override val dispatchers: CoroutineDispatcherProvider,
|
override val dispatchers: CoroutineDispatcherProvider,
|
||||||
private val getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase,
|
private val getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase,
|
||||||
private val getEarnTokensBatchFlowUseCase: GetEarnTokensBatchFlowUseCase,
|
private val getEarnTokensBatchFlowUseCase: GetEarnTokensBatchFlowUseCase,
|
||||||
private val stakingAvailabilityListUseCase: StakingAvailabilityListUseCase,
|
private val stakingAvailabilityListUseCase: StakingAvailabilityListUseCase,
|
||||||
private val isAccountsModeEnabledUseCase: IsAccountsModeEnabledUseCase,
|
private val isAccountsModeEnabledUseCase: IsAccountsModeEnabledUseCase,
|
||||||
private val earnErrorResolver: EarnErrorResolver,
|
private val earnErrorResolver: EarnErrorResolver,
|
||||||
|
private val addToPortfolioManagerFactory: AddToPortfolioManager.Factory,
|
||||||
) : Model() {
|
) : Model() {
|
||||||
|
|
||||||
private val params = paramsContainer.require<ForYouComponent.Params>()
|
private val params = paramsContainer.require<ForYouComponent.Params>()
|
||||||
|
|
@ -63,6 +76,13 @@ internal class ForYouModel @Inject constructor(
|
||||||
private val expandedEarnOpportunitiesAssetIds = MutableStateFlow<Set<String>>(value = emptySet())
|
private val expandedEarnOpportunitiesAssetIds = MutableStateFlow<Set<String>>(value = emptySet())
|
||||||
private val selectedAppCurrencyFlow: StateFlow<AppCurrency> = createSelectedAppCurrencyFlow()
|
private val selectedAppCurrencyFlow: StateFlow<AppCurrency> = createSelectedAppCurrencyFlow()
|
||||||
|
|
||||||
|
val bottomSheetNavigation: SlotNavigation<ForYouBottomSheetConfig> = SlotNavigation()
|
||||||
|
|
||||||
|
var addToPortfolioManager: AddToPortfolioManager? = null
|
||||||
|
private set
|
||||||
|
|
||||||
|
private var addToPortfolioManagerScope: CoroutineScope? = null
|
||||||
|
|
||||||
val uiState: StateFlow<ForYouUM>
|
val uiState: StateFlow<ForYouUM>
|
||||||
field = MutableStateFlow<ForYouUM>(
|
field = MutableStateFlow<ForYouUM>(
|
||||||
ForYouUM(
|
ForYouUM(
|
||||||
|
|
@ -144,6 +164,8 @@ internal class ForYouModel @Inject constructor(
|
||||||
topEarnTokens = topEarnTokens,
|
topEarnTokens = topEarnTokens,
|
||||||
expandedAssetIds = expandedEarnOpportunities,
|
expandedAssetIds = expandedEarnOpportunities,
|
||||||
expandClick = ::onExpandEarnOpportunitiesClick,
|
expandClick = ::onExpandEarnOpportunitiesClick,
|
||||||
|
onTokenClick = ::onEarnOpportunitiesTokenClick,
|
||||||
|
onAllEarnTokensClick = params.callbacks::onAllEarnTokensClick,
|
||||||
).convert(accountStatusList)
|
).convert(accountStatusList)
|
||||||
|
|
||||||
uiState.update(
|
uiState.update(
|
||||||
|
|
@ -207,6 +229,46 @@ internal class ForYouModel @Inject constructor(
|
||||||
params.callbacks.onTokenClick(walletId, currency)
|
params.callbacks.onTokenClick(walletId, currency)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun onEarnOpportunitiesTokenClick(
|
||||||
|
selectedWalletId: UserWalletId?,
|
||||||
|
currency: CryptoCurrency,
|
||||||
|
type: ForYouEarnOpportunitiesType,
|
||||||
|
) {
|
||||||
|
when {
|
||||||
|
selectedWalletId != null -> openEarnScreen(
|
||||||
|
userWalletId = selectedWalletId,
|
||||||
|
currency = currency,
|
||||||
|
type = type,
|
||||||
|
)
|
||||||
|
else -> {
|
||||||
|
// TODO For you make logic if not added add token, otherwise manage funds
|
||||||
|
// val token = RawMarketToken(
|
||||||
|
// id = currency.id.rawCurrencyId ?: return,
|
||||||
|
// name = currency.name,
|
||||||
|
// symbol = currency.symbol,
|
||||||
|
// )
|
||||||
|
// val network = TokenMarketInfo.Network(
|
||||||
|
// networkId = currency.network.rawId,
|
||||||
|
// isExchangeable = false,
|
||||||
|
// contractAddress = (currency as? CryptoCurrency.Token)?.contractAddress,
|
||||||
|
// decimalCount = currency.decimals,
|
||||||
|
// )
|
||||||
|
// val manager = createAddToPortfolioManager().apply {
|
||||||
|
// setTokenParams(token)
|
||||||
|
// setTokenNetworks(listOf(network))
|
||||||
|
// }
|
||||||
|
// addToPortfolioManager = manager
|
||||||
|
// Drop the slot through null so the same-source repeat click still recreates the child.
|
||||||
|
bottomSheetNavigation.dismiss()
|
||||||
|
bottomSheetNavigation.activate(
|
||||||
|
ForYouBottomSheetConfig.ManageFunds(
|
||||||
|
currency.id.rawCurrencyId ?: return,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private fun onExpandPortfolioReviewClick(assetId: String) {
|
private fun onExpandPortfolioReviewClick(assetId: String) {
|
||||||
expandedPortfolioReviewAssetIds.update { ids ->
|
expandedPortfolioReviewAssetIds.update { ids ->
|
||||||
if (assetId in ids) ids - assetId else ids + assetId
|
if (assetId in ids) ids - assetId else ids + assetId
|
||||||
|
|
@ -228,4 +290,77 @@ internal class ForYouModel @Inject constructor(
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO For you make logic if not added add token, otherwise manage funds
|
||||||
|
@Suppress("UnusedPrivateMember")
|
||||||
|
private fun createAddToPortfolioManager(): AddToPortfolioManager {
|
||||||
|
addToPortfolioManagerScope?.cancel()
|
||||||
|
val managerScope = CoroutineScope(
|
||||||
|
modelScope.coroutineContext + SupervisorJob(modelScope.coroutineContext.job),
|
||||||
|
)
|
||||||
|
addToPortfolioManagerScope = managerScope
|
||||||
|
|
||||||
|
val manager = addToPortfolioManagerFactory.create(
|
||||||
|
scope = managerScope,
|
||||||
|
settings = AddToPortfolioManager.Settings.Earn,
|
||||||
|
analyticsParams = AddToPortfolioManager.AnalyticsParams(
|
||||||
|
source = AnalyticsParam.ScreensSources.Markets.value,
|
||||||
|
),
|
||||||
|
).apply {
|
||||||
|
updateLaunchMode(AddToPortfolioManager.LaunchMode.ViaUserPortfolio)
|
||||||
|
}
|
||||||
|
|
||||||
|
manager.onDismiss.receiveAsFlow()
|
||||||
|
.onEach { bottomSheetNavigation.dismiss() }
|
||||||
|
.launchIn(managerScope)
|
||||||
|
manager.onSuccessAdded.receiveAsFlow()
|
||||||
|
.onEach { bottomSheetNavigation.dismiss() }
|
||||||
|
.onEach { result ->
|
||||||
|
router.push(
|
||||||
|
AppRoute.CurrencyDetails(
|
||||||
|
userWalletId = result.wallet.walletId,
|
||||||
|
currency = result.addedCurrency.currency,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
.launchIn(managerScope)
|
||||||
|
manager.onAddedTokenClick.receiveAsFlow()
|
||||||
|
.onEach { bottomSheetNavigation.dismiss() }
|
||||||
|
.onEach { result ->
|
||||||
|
router.push(
|
||||||
|
AppRoute.CurrencyDetails(
|
||||||
|
userWalletId = result.wallet.walletId,
|
||||||
|
currency = result.addedCurrency.currency,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
.launchIn(managerScope)
|
||||||
|
|
||||||
|
return manager
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun openEarnScreen(
|
||||||
|
userWalletId: UserWalletId,
|
||||||
|
currency: CryptoCurrency,
|
||||||
|
type: ForYouEarnOpportunitiesType,
|
||||||
|
) {
|
||||||
|
router.push(
|
||||||
|
when (type) {
|
||||||
|
is ForYouEarnOpportunitiesType.Staking -> {
|
||||||
|
AppRoute.Staking(
|
||||||
|
userWalletId = userWalletId,
|
||||||
|
cryptoCurrency = currency,
|
||||||
|
integrationId = type.integrationID,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
is ForYouEarnOpportunitiesType.YieldSupply -> {
|
||||||
|
AppRoute.YieldSupplyEntry(
|
||||||
|
userWalletId = userWalletId,
|
||||||
|
cryptoCurrency = currency,
|
||||||
|
apy = type.apy,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -8,6 +8,7 @@ import com.tangem.core.ui.extensions.stringReference
|
||||||
import com.tangem.domain.models.account.Account
|
import com.tangem.domain.models.account.Account
|
||||||
import com.tangem.domain.models.currency.CryptoCurrency
|
import com.tangem.domain.models.currency.CryptoCurrency
|
||||||
import com.tangem.domain.models.currency.CryptoCurrencyStatus
|
import com.tangem.domain.models.currency.CryptoCurrencyStatus
|
||||||
|
import com.tangem.features.foryou.impl.entity.ForYouEarnOpportunitiesType
|
||||||
import com.tangem.utils.extensions.isZero
|
import com.tangem.utils.extensions.isZero
|
||||||
import java.math.BigDecimal
|
import java.math.BigDecimal
|
||||||
import java.math.RoundingMode
|
import java.math.RoundingMode
|
||||||
|
|
@ -62,11 +63,14 @@ internal fun forYouPlaceholderBadge(): TangemBadgeUM = TangemBadgeUM(
|
||||||
* @property isActive whether the user already earns on the token (active yield supply or stake)
|
* @property isActive whether the user already earns on the token (active yield supply or stake)
|
||||||
* @property apy rate as a fraction (0.05 = 5%)
|
* @property apy rate as a fraction (0.05 = 5%)
|
||||||
* @property potentialRewards projected yearly reward in fiat (`fiatAmount * apy`), `null` when unknown
|
* @property potentialRewards projected yearly reward in fiat (`fiatAmount * apy`), `null` when unknown
|
||||||
|
* @property type which earn product the rate belongs to; passed to the click callback so the model
|
||||||
|
* can open the matching earn screen
|
||||||
*/
|
*/
|
||||||
internal data class EarnApyInfo(
|
internal data class EarnApyInfo(
|
||||||
val isActive: Boolean,
|
val isActive: Boolean,
|
||||||
val apy: BigDecimal?,
|
val apy: BigDecimal?,
|
||||||
val potentialRewards: BigDecimal?,
|
val potentialRewards: BigDecimal?,
|
||||||
|
val type: ForYouEarnOpportunitiesType,
|
||||||
)
|
)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
||||||
|
|
@ -8,12 +8,15 @@ import com.tangem.domain.models.currency.CryptoCurrencyStatus
|
||||||
import com.tangem.domain.models.currency.yieldSupplyKey
|
import com.tangem.domain.models.currency.yieldSupplyKey
|
||||||
import com.tangem.domain.models.earn.EarnTopToken
|
import com.tangem.domain.models.earn.EarnTopToken
|
||||||
import com.tangem.domain.models.staking.StakingBalance
|
import com.tangem.domain.models.staking.StakingBalance
|
||||||
|
import com.tangem.domain.models.wallet.UserWalletId
|
||||||
import com.tangem.domain.staking.model.StakingAvailability
|
import com.tangem.domain.staking.model.StakingAvailability
|
||||||
|
import com.tangem.domain.staking.model.StakingIntegrationID
|
||||||
import com.tangem.domain.staking.model.StakingOption
|
import com.tangem.domain.staking.model.StakingOption
|
||||||
import com.tangem.domain.staking.model.common.RewardInfo
|
import com.tangem.domain.staking.model.common.RewardInfo
|
||||||
import com.tangem.domain.staking.model.common.RewardType
|
import com.tangem.domain.staking.model.common.RewardType
|
||||||
import com.tangem.domain.staking.model.optionOrNull
|
import com.tangem.domain.staking.model.optionOrNull
|
||||||
import com.tangem.features.foryou.impl.entity.EarnOpportunitiesUM
|
import com.tangem.features.foryou.impl.entity.EarnOpportunitiesUM
|
||||||
|
import com.tangem.features.foryou.impl.entity.ForYouEarnOpportunitiesType
|
||||||
import com.tangem.features.foryou.impl.model.converter.EarnApyInfo
|
import com.tangem.features.foryou.impl.model.converter.EarnApyInfo
|
||||||
import com.tangem.features.foryou.impl.model.converter.EarnOpportunities
|
import com.tangem.features.foryou.impl.model.converter.EarnOpportunities
|
||||||
import com.tangem.features.foryou.impl.model.converter.PERCENT_BASE
|
import com.tangem.features.foryou.impl.model.converter.PERCENT_BASE
|
||||||
|
|
@ -45,6 +48,8 @@ internal class ForYouEarnOpportunitiesConverter(
|
||||||
private val yieldSupplyAvailability: Map<String, BigDecimal>,
|
private val yieldSupplyAvailability: Map<String, BigDecimal>,
|
||||||
private val yieldStakingAvailability: Map<CryptoCurrency, StakingAvailability>,
|
private val yieldStakingAvailability: Map<CryptoCurrency, StakingAvailability>,
|
||||||
private val topEarnTokens: EarnTopToken?,
|
private val topEarnTokens: EarnTopToken?,
|
||||||
|
private val onTokenClick: (UserWalletId?, CryptoCurrency, ForYouEarnOpportunitiesType) -> Unit,
|
||||||
|
private val onAllEarnTokensClick: () -> Unit,
|
||||||
) : Converter<AccountStatusList?, EarnOpportunitiesUM> {
|
) : Converter<AccountStatusList?, EarnOpportunitiesUM> {
|
||||||
|
|
||||||
override fun convert(value: AccountStatusList?): EarnOpportunitiesUM {
|
override fun convert(value: AccountStatusList?): EarnOpportunitiesUM {
|
||||||
|
|
@ -86,19 +91,26 @@ internal class ForYouEarnOpportunitiesConverter(
|
||||||
data.isEmpty() -> {
|
data.isEmpty() -> {
|
||||||
ForYouEarnOpportunitiesNoTokensConverter(
|
ForYouEarnOpportunitiesNoTokensConverter(
|
||||||
topEarnTokens = topEarnTokens,
|
topEarnTokens = topEarnTokens,
|
||||||
|
onTokenClick = onTokenClick,
|
||||||
|
onAllEarnTokensClick = onAllEarnTokensClick,
|
||||||
).convert(data)
|
).convert(data)
|
||||||
}
|
}
|
||||||
data.all { earn -> earn.earnCurrencies.all { entry -> entry.value.isActive } } -> {
|
data.all { earn -> earn.earnCurrencies.all { entry -> entry.value.isActive } } -> {
|
||||||
ForYouEarnOpportunitiesTokensActiveConverter(
|
ForYouEarnOpportunitiesTokensActiveConverter(
|
||||||
topEarnTokens = topEarnTokens,
|
topEarnTokens = topEarnTokens,
|
||||||
|
onTokenClick = onTokenClick,
|
||||||
|
onAllEarnTokensClick = onAllEarnTokensClick,
|
||||||
).convert(data)
|
).convert(data)
|
||||||
}
|
}
|
||||||
else -> {
|
else -> {
|
||||||
ForYouEarnOpportunitiesPotentialRewardsConverter(
|
ForYouEarnOpportunitiesPotentialRewardsConverter(
|
||||||
appCurrency = appCurrency,
|
appCurrency = appCurrency,
|
||||||
|
userWalletId = value?.userWalletId,
|
||||||
isAccountsModeEnabled = isAccountsModeEnabled,
|
isAccountsModeEnabled = isAccountsModeEnabled,
|
||||||
expandedAssetIds = expandedAssetIds,
|
expandedAssetIds = expandedAssetIds,
|
||||||
expandClick = expandClick,
|
expandClick = expandClick,
|
||||||
|
onTokenClick = onTokenClick,
|
||||||
|
onAllEarnTokensClick = onAllEarnTokensClick,
|
||||||
).convert(data)
|
).convert(data)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -129,6 +141,7 @@ internal class ForYouEarnOpportunitiesConverter(
|
||||||
isActive = isActive,
|
isActive = isActive,
|
||||||
potentialRewards = cryptoCurrencyStatus.value.fiatAmount?.multiply(apy),
|
potentialRewards = cryptoCurrencyStatus.value.fiatAmount?.multiply(apy),
|
||||||
apy = apy,
|
apy = apy,
|
||||||
|
type = ForYouEarnOpportunitiesType.YieldSupply(yieldSupplyApy.toPlainString()),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -143,6 +156,7 @@ internal class ForYouEarnOpportunitiesConverter(
|
||||||
isActive = stakingInfo.isActive,
|
isActive = stakingInfo.isActive,
|
||||||
apy = stakingInfo.rate,
|
apy = stakingInfo.rate,
|
||||||
potentialRewards = cryptoCurrencyStatus.value.fiatAmount?.multiply(stakingInfo.rate),
|
potentialRewards = cryptoCurrencyStatus.value.fiatAmount?.multiply(stakingInfo.rate),
|
||||||
|
type = ForYouEarnOpportunitiesType.Staking(integrationID = stakingInfo.integrationId),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -205,6 +219,7 @@ internal class ForYouEarnOpportunitiesConverter(
|
||||||
rate = rateInfo.rate,
|
rate = rateInfo.rate,
|
||||||
isActive = isActive,
|
isActive = isActive,
|
||||||
rewardType = rateInfo.type,
|
rewardType = rateInfo.type,
|
||||||
|
integrationId = option.integrationId,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -212,5 +227,6 @@ internal class ForYouEarnOpportunitiesConverter(
|
||||||
val rate: BigDecimal,
|
val rate: BigDecimal,
|
||||||
val isActive: Boolean,
|
val isActive: Boolean,
|
||||||
val rewardType: RewardType,
|
val rewardType: RewardType,
|
||||||
|
val integrationId: StakingIntegrationID,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
@ -5,7 +5,10 @@ import com.tangem.core.ui.extensions.stringReference
|
||||||
import com.tangem.core.ui.format.bigdecimal.format
|
import com.tangem.core.ui.format.bigdecimal.format
|
||||||
import com.tangem.core.ui.format.bigdecimal.percent
|
import com.tangem.core.ui.format.bigdecimal.percent
|
||||||
import com.tangem.core.ui.utils.parseBigDecimalOrNull
|
import com.tangem.core.ui.utils.parseBigDecimalOrNull
|
||||||
|
import com.tangem.domain.models.currency.CryptoCurrency
|
||||||
import com.tangem.domain.models.earn.EarnTopToken
|
import com.tangem.domain.models.earn.EarnTopToken
|
||||||
|
import com.tangem.domain.models.wallet.UserWalletId
|
||||||
|
import com.tangem.features.foryou.impl.entity.ForYouEarnOpportunitiesType
|
||||||
import com.tangem.features.foryou.impl.entity.EarnOpportunitiesUM
|
import com.tangem.features.foryou.impl.entity.EarnOpportunitiesUM
|
||||||
import com.tangem.features.foryou.impl.model.converter.EarnOpportunities
|
import com.tangem.features.foryou.impl.model.converter.EarnOpportunities
|
||||||
import com.tangem.features.foryou.impl.model.converter.FOR_YOU_TOP_EARN_TOKENS_COUNT
|
import com.tangem.features.foryou.impl.model.converter.FOR_YOU_TOP_EARN_TOKENS_COUNT
|
||||||
|
|
@ -20,6 +23,8 @@ import kotlinx.collections.immutable.toPersistentList
|
||||||
*/
|
*/
|
||||||
internal class ForYouEarnOpportunitiesNoTokensConverter(
|
internal class ForYouEarnOpportunitiesNoTokensConverter(
|
||||||
private val topEarnTokens: EarnTopToken?,
|
private val topEarnTokens: EarnTopToken?,
|
||||||
|
private val onTokenClick: (UserWalletId?, CryptoCurrency, ForYouEarnOpportunitiesType) -> Unit,
|
||||||
|
private val onAllEarnTokensClick: () -> Unit,
|
||||||
) : Converter<List<EarnOpportunities>, EarnOpportunitiesUM> {
|
) : Converter<List<EarnOpportunities>, EarnOpportunitiesUM> {
|
||||||
|
|
||||||
override fun convert(value: List<EarnOpportunities>): EarnOpportunitiesUM {
|
override fun convert(value: List<EarnOpportunities>): EarnOpportunitiesUM {
|
||||||
|
|
@ -29,7 +34,7 @@ internal class ForYouEarnOpportunitiesNoTokensConverter(
|
||||||
val topEarnToken = topEarnTokenList?.firstOrNull()?.earnToken
|
val topEarnToken = topEarnTokenList?.firstOrNull()?.earnToken
|
||||||
val topEarnApy = topEarnToken?.apy?.parseBigDecimalOrNull()
|
val topEarnApy = topEarnToken?.apy?.parseBigDecimalOrNull()
|
||||||
|
|
||||||
val rowConverter = ForYouEarnOpportunitiesTopTokenRowConverter()
|
val rowConverter = ForYouEarnOpportunitiesTopTokenRowConverter(onTokenClick = onTokenClick)
|
||||||
|
|
||||||
return EarnOpportunitiesUM.Content(
|
return EarnOpportunitiesUM.Content(
|
||||||
tokenList = topEarnTokenList
|
tokenList = topEarnTokenList
|
||||||
|
|
@ -39,6 +44,7 @@ internal class ForYouEarnOpportunitiesNoTokensConverter(
|
||||||
subtitleRes = R.string.for_you_earn_opportunities_no_available_tokens,
|
subtitleRes = R.string.for_you_earn_opportunities_no_available_tokens,
|
||||||
potentialReward = stringReference(topEarnApy.format { percent() }),
|
potentialReward = stringReference(topEarnApy.format { percent() }),
|
||||||
potentialRewardType = topEarnToken?.rewardType?.name?.let(::stringReference),
|
potentialRewardType = topEarnToken?.rewardType?.name?.let(::stringReference),
|
||||||
|
onAllEarnTokensClick = onAllEarnTokensClick,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -11,7 +11,10 @@ import com.tangem.core.ui.format.bigdecimal.fiat
|
||||||
import com.tangem.core.ui.format.bigdecimal.format
|
import com.tangem.core.ui.format.bigdecimal.format
|
||||||
import com.tangem.domain.appcurrency.model.AppCurrency
|
import com.tangem.domain.appcurrency.model.AppCurrency
|
||||||
import com.tangem.domain.models.account.Account
|
import com.tangem.domain.models.account.Account
|
||||||
|
import com.tangem.domain.models.currency.CryptoCurrency
|
||||||
|
import com.tangem.domain.models.wallet.UserWalletId
|
||||||
import com.tangem.features.foryou.impl.entity.EarnOpportunitiesUM
|
import com.tangem.features.foryou.impl.entity.EarnOpportunitiesUM
|
||||||
|
import com.tangem.features.foryou.impl.entity.ForYouEarnOpportunitiesType
|
||||||
import com.tangem.features.foryou.impl.entity.ForYouTokenListItemUM
|
import com.tangem.features.foryou.impl.entity.ForYouTokenListItemUM
|
||||||
import com.tangem.features.foryou.impl.model.converter.EarnOpportunities
|
import com.tangem.features.foryou.impl.model.converter.EarnOpportunities
|
||||||
import com.tangem.utils.StringsSigns
|
import com.tangem.utils.StringsSigns
|
||||||
|
|
@ -29,14 +32,22 @@ import java.math.BigDecimal
|
||||||
* [ForYouEarnOpportunitiesTokenRowConverter], expansion keyed by account id); with it off, the
|
* [ForYouEarnOpportunitiesTokenRowConverter], expansion keyed by account id); with it off, the
|
||||||
* tokens are rendered as flat non-expandable rows.
|
* tokens are rendered as flat non-expandable rows.
|
||||||
*/
|
*/
|
||||||
|
@Suppress("LongParameterList")
|
||||||
internal class ForYouEarnOpportunitiesPotentialRewardsConverter(
|
internal class ForYouEarnOpportunitiesPotentialRewardsConverter(
|
||||||
private val appCurrency: AppCurrency,
|
private val appCurrency: AppCurrency,
|
||||||
|
private val userWalletId: UserWalletId?,
|
||||||
private val isAccountsModeEnabled: Boolean,
|
private val isAccountsModeEnabled: Boolean,
|
||||||
private val expandedAssetIds: Set<String>,
|
private val expandedAssetIds: Set<String>,
|
||||||
private val expandClick: (assetId: String) -> Unit,
|
private val expandClick: (assetId: String) -> Unit,
|
||||||
|
private val onTokenClick: (UserWalletId?, CryptoCurrency, ForYouEarnOpportunitiesType) -> Unit,
|
||||||
|
private val onAllEarnTokensClick: () -> Unit,
|
||||||
) : Converter<List<EarnOpportunities>, EarnOpportunitiesUM> {
|
) : Converter<List<EarnOpportunities>, EarnOpportunitiesUM> {
|
||||||
|
|
||||||
private val rowConverter = ForYouEarnOpportunitiesTokenRowConverter(appCurrency = appCurrency)
|
private val rowConverter = ForYouEarnOpportunitiesTokenRowConverter(
|
||||||
|
appCurrency = appCurrency,
|
||||||
|
userWalletId = userWalletId,
|
||||||
|
onTokenClick = onTokenClick,
|
||||||
|
)
|
||||||
|
|
||||||
override fun convert(value: List<EarnOpportunities>): EarnOpportunitiesUM {
|
override fun convert(value: List<EarnOpportunities>): EarnOpportunitiesUM {
|
||||||
val totalPotentialReward = value.sumOf { it.accountPotentialReward }
|
val totalPotentialReward = value.sumOf { it.accountPotentialReward }
|
||||||
|
|
@ -82,6 +93,7 @@ internal class ForYouEarnOpportunitiesPotentialRewardsConverter(
|
||||||
subtitleRes = R.string.for_you_earn_opportunities_tokens_rewards,
|
subtitleRes = R.string.for_you_earn_opportunities_tokens_rewards,
|
||||||
potentialReward = totalPotentialRewardText,
|
potentialReward = totalPotentialRewardText,
|
||||||
potentialRewardType = null,
|
potentialRewardType = null,
|
||||||
|
onAllEarnTokensClick = onAllEarnTokensClick,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -14,7 +14,10 @@ import com.tangem.core.ui.format.bigdecimal.percent
|
||||||
import com.tangem.core.ui.res.TangemTheme
|
import com.tangem.core.ui.res.TangemTheme
|
||||||
import com.tangem.domain.appcurrency.model.AppCurrency
|
import com.tangem.domain.appcurrency.model.AppCurrency
|
||||||
import com.tangem.domain.models.StatusSource
|
import com.tangem.domain.models.StatusSource
|
||||||
|
import com.tangem.domain.models.currency.CryptoCurrency
|
||||||
import com.tangem.domain.models.currency.CryptoCurrencyStatus
|
import com.tangem.domain.models.currency.CryptoCurrencyStatus
|
||||||
|
import com.tangem.domain.models.wallet.UserWalletId
|
||||||
|
import com.tangem.features.foryou.impl.entity.ForYouEarnOpportunitiesType
|
||||||
import com.tangem.features.foryou.impl.model.converter.EarnApyInfo
|
import com.tangem.features.foryou.impl.model.converter.EarnApyInfo
|
||||||
import com.tangem.utils.StringsSigns
|
import com.tangem.utils.StringsSigns
|
||||||
import com.tangem.utils.converter.Converter
|
import com.tangem.utils.converter.Converter
|
||||||
|
|
@ -31,6 +34,8 @@ import java.math.BigDecimal
|
||||||
*/
|
*/
|
||||||
internal class ForYouEarnOpportunitiesTokenRowConverter(
|
internal class ForYouEarnOpportunitiesTokenRowConverter(
|
||||||
private val appCurrency: AppCurrency,
|
private val appCurrency: AppCurrency,
|
||||||
|
private val userWalletId: UserWalletId?,
|
||||||
|
private val onTokenClick: (UserWalletId?, CryptoCurrency, ForYouEarnOpportunitiesType) -> Unit,
|
||||||
) : Converter<Pair<CryptoCurrencyStatus, EarnApyInfo>, TangemTokenRowUM> {
|
) : Converter<Pair<CryptoCurrencyStatus, EarnApyInfo>, TangemTokenRowUM> {
|
||||||
|
|
||||||
private val iconConverter = CryptoCurrencyToIconStateConverter()
|
private val iconConverter = CryptoCurrencyToIconStateConverter()
|
||||||
|
|
@ -55,7 +60,13 @@ internal class ForYouEarnOpportunitiesTokenRowConverter(
|
||||||
),
|
),
|
||||||
topEndContentUM = toRowTopEnd(cryptoCurrencyStatus, possibleEarnAmount),
|
topEndContentUM = toRowTopEnd(cryptoCurrencyStatus, possibleEarnAmount),
|
||||||
bottomEndContentUM = toRowBottomEnd(cryptoCurrencyStatus, earnApyInfo.apy.orZero()),
|
bottomEndContentUM = toRowBottomEnd(cryptoCurrencyStatus, earnApyInfo.apy.orZero()),
|
||||||
onItemClick = null,
|
onItemClick = {
|
||||||
|
onTokenClick(
|
||||||
|
userWalletId,
|
||||||
|
cryptoCurrencyStatus.currency,
|
||||||
|
earnApyInfo.type,
|
||||||
|
)
|
||||||
|
},
|
||||||
onItemLongClick = null,
|
onItemLongClick = null,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,11 @@
|
||||||
package com.tangem.features.foryou.impl.model.converter.earnOpportunities
|
package com.tangem.features.foryou.impl.model.converter.earnOpportunities
|
||||||
|
|
||||||
import com.tangem.common.ui.R
|
import com.tangem.common.ui.R
|
||||||
|
import com.tangem.domain.models.currency.CryptoCurrency
|
||||||
import com.tangem.domain.models.earn.EarnTopToken
|
import com.tangem.domain.models.earn.EarnTopToken
|
||||||
|
import com.tangem.domain.models.wallet.UserWalletId
|
||||||
import com.tangem.features.foryou.impl.entity.EarnOpportunitiesUM
|
import com.tangem.features.foryou.impl.entity.EarnOpportunitiesUM
|
||||||
|
import com.tangem.features.foryou.impl.entity.ForYouEarnOpportunitiesType
|
||||||
import com.tangem.features.foryou.impl.model.converter.EarnOpportunities
|
import com.tangem.features.foryou.impl.model.converter.EarnOpportunities
|
||||||
import com.tangem.features.foryou.impl.model.converter.FOR_YOU_TOP_EARN_TOKENS_COUNT
|
import com.tangem.features.foryou.impl.model.converter.FOR_YOU_TOP_EARN_TOKENS_COUNT
|
||||||
import com.tangem.features.foryou.impl.model.converter.forYouEarnAssetKey
|
import com.tangem.features.foryou.impl.model.converter.forYouEarnAssetKey
|
||||||
|
|
@ -20,6 +23,8 @@ import kotlinx.collections.immutable.toPersistentList
|
||||||
*/
|
*/
|
||||||
internal class ForYouEarnOpportunitiesTokensActiveConverter(
|
internal class ForYouEarnOpportunitiesTokensActiveConverter(
|
||||||
private val topEarnTokens: EarnTopToken?,
|
private val topEarnTokens: EarnTopToken?,
|
||||||
|
private val onTokenClick: (UserWalletId?, CryptoCurrency, ForYouEarnOpportunitiesType) -> Unit,
|
||||||
|
private val onAllEarnTokensClick: () -> Unit,
|
||||||
) : Converter<List<EarnOpportunities>, EarnOpportunitiesUM> {
|
) : Converter<List<EarnOpportunities>, EarnOpportunitiesUM> {
|
||||||
|
|
||||||
override fun convert(value: List<EarnOpportunities>): EarnOpportunitiesUM {
|
override fun convert(value: List<EarnOpportunities>): EarnOpportunitiesUM {
|
||||||
|
|
@ -28,7 +33,7 @@ internal class ForYouEarnOpportunitiesTokensActiveConverter(
|
||||||
.map { status -> status.currency.forYouEarnAssetKey() }
|
.map { status -> status.currency.forYouEarnAssetKey() }
|
||||||
.toSet()
|
.toSet()
|
||||||
|
|
||||||
val rowConverter = ForYouEarnOpportunitiesTopTokenRowConverter()
|
val rowConverter = ForYouEarnOpportunitiesTopTokenRowConverter(onTokenClick)
|
||||||
|
|
||||||
return EarnOpportunitiesUM.Content(
|
return EarnOpportunitiesUM.Content(
|
||||||
tokenList = topEarnTokens?.getOrNull()
|
tokenList = topEarnTokens?.getOrNull()
|
||||||
|
|
@ -40,6 +45,7 @@ internal class ForYouEarnOpportunitiesTokensActiveConverter(
|
||||||
subtitleRes = R.string.for_you_earn_opportunities_all_tokens_active,
|
subtitleRes = R.string.for_you_earn_opportunities_all_tokens_active,
|
||||||
potentialReward = null,
|
potentialReward = null,
|
||||||
potentialRewardType = null,
|
potentialRewardType = null,
|
||||||
|
onAllEarnTokensClick = onAllEarnTokensClick,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -10,8 +10,12 @@ import com.tangem.core.ui.extensions.stringReference
|
||||||
import com.tangem.core.ui.extensions.wrappedList
|
import com.tangem.core.ui.extensions.wrappedList
|
||||||
import com.tangem.core.ui.format.bigdecimal.format
|
import com.tangem.core.ui.format.bigdecimal.format
|
||||||
import com.tangem.core.ui.format.bigdecimal.percent
|
import com.tangem.core.ui.format.bigdecimal.percent
|
||||||
|
import com.tangem.domain.models.currency.CryptoCurrency
|
||||||
import com.tangem.domain.models.earn.EarnTokenWithCurrency
|
import com.tangem.domain.models.earn.EarnTokenWithCurrency
|
||||||
import com.tangem.domain.models.earn.EarnType
|
import com.tangem.domain.models.earn.EarnType
|
||||||
|
import com.tangem.domain.models.wallet.UserWalletId
|
||||||
|
import com.tangem.domain.staking.model.StakingIntegrationID
|
||||||
|
import com.tangem.features.foryou.impl.entity.ForYouEarnOpportunitiesType
|
||||||
import com.tangem.features.foryou.impl.entity.ForYouTokenListItemUM
|
import com.tangem.features.foryou.impl.entity.ForYouTokenListItemUM
|
||||||
import com.tangem.utils.converter.Converter
|
import com.tangem.utils.converter.Converter
|
||||||
import kotlinx.collections.immutable.persistentListOf
|
import kotlinx.collections.immutable.persistentListOf
|
||||||
|
|
@ -23,7 +27,9 @@ import java.math.BigDecimal
|
||||||
* converters that surface suggestions ([ForYouEarnOpportunitiesNoTokensConverter],
|
* converters that surface suggestions ([ForYouEarnOpportunitiesNoTokensConverter],
|
||||||
* [ForYouEarnOpportunitiesTokensActiveConverter]).
|
* [ForYouEarnOpportunitiesTokensActiveConverter]).
|
||||||
*/
|
*/
|
||||||
internal class ForYouEarnOpportunitiesTopTokenRowConverter : Converter<EarnTokenWithCurrency, ForYouTokenListItemUM> {
|
internal class ForYouEarnOpportunitiesTopTokenRowConverter(
|
||||||
|
private val onTokenClick: (UserWalletId?, CryptoCurrency, ForYouEarnOpportunitiesType) -> Unit,
|
||||||
|
) : Converter<EarnTokenWithCurrency, ForYouTokenListItemUM> {
|
||||||
|
|
||||||
private val iconConverter = CryptoCurrencyToIconStateConverter()
|
private val iconConverter = CryptoCurrencyToIconStateConverter()
|
||||||
|
|
||||||
|
|
@ -57,7 +63,22 @@ internal class ForYouEarnOpportunitiesTopTokenRowConverter : Converter<EarnToken
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
onItemClick = {},
|
onItemClick = {
|
||||||
|
val type = when (earnToken.type) {
|
||||||
|
EarnType.STAKING -> {
|
||||||
|
val integrationID = StakingIntegrationID.create(currencyId = value.cryptoCurrency.id)
|
||||||
|
?: return@Content
|
||||||
|
|
||||||
|
ForYouEarnOpportunitiesType.Staking(integrationID = integrationID)
|
||||||
|
}
|
||||||
|
EarnType.YIELD -> ForYouEarnOpportunitiesType.YieldSupply(apy = earnToken.apy)
|
||||||
|
}
|
||||||
|
onTokenClick(
|
||||||
|
null,
|
||||||
|
value.cryptoCurrency,
|
||||||
|
type,
|
||||||
|
)
|
||||||
|
},
|
||||||
onItemLongClick = { _, _ -> },
|
onItemLongClick = { _, _ -> },
|
||||||
),
|
),
|
||||||
tokenList = persistentListOf(),
|
tokenList = persistentListOf(),
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@ package com.tangem.features.foryou.impl.ui
|
||||||
import android.content.res.Configuration
|
import android.content.res.Configuration
|
||||||
import androidx.compose.foundation.background
|
import androidx.compose.foundation.background
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
import androidx.compose.foundation.text.InlineTextContent
|
import androidx.compose.foundation.text.InlineTextContent
|
||||||
|
|
@ -19,6 +20,7 @@ import androidx.compose.ui.tooling.preview.Preview
|
||||||
import androidx.compose.ui.tooling.preview.PreviewParameter
|
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||||
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
|
import com.tangem.core.ui.ds2.button.TangemButton
|
||||||
import com.tangem.core.ui.ds2.shimmers.TangemShimmer
|
import com.tangem.core.ui.ds2.shimmers.TangemShimmer
|
||||||
import com.tangem.core.ui.extensions.*
|
import com.tangem.core.ui.extensions.*
|
||||||
import com.tangem.core.ui.res.TangemTheme
|
import com.tangem.core.ui.res.TangemTheme
|
||||||
|
|
@ -84,6 +86,20 @@ internal fun ForYouEarnOpportunities(earnOpportunitiesUM: EarnOpportunitiesUM, m
|
||||||
tokenList = earnOpportunitiesUM.tokenList,
|
tokenList = earnOpportunitiesUM.tokenList,
|
||||||
modifier = Modifier.padding(top = 8.dp),
|
modifier = Modifier.padding(top = 8.dp),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if (earnOpportunitiesUM is EarnOpportunitiesUM.Content) {
|
||||||
|
TangemButton(
|
||||||
|
text = stringReference("Explore all tokens"), // todo FOR YOU lokalize
|
||||||
|
variant = TangemButton.Variant.Secondary,
|
||||||
|
size = TangemButton.Size.X9,
|
||||||
|
isEnabled = true,
|
||||||
|
contentDescription = "Explore all tokens", // todo FOR YOU lokalize
|
||||||
|
onClick = earnOpportunitiesUM.onAllEarnTokensClick,
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(top = 8.dp),
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -36,6 +36,7 @@ internal object ForYouEarnOpportunitiesPreviewData {
|
||||||
bottomEnd = "6,2%",
|
bottomEnd = "6,2%",
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
onAllEarnTokensClick = {},
|
||||||
)
|
)
|
||||||
|
|
||||||
/** No earnable tokens in the portfolio: top earn tokens teaser. */
|
/** No earnable tokens in the portfolio: top earn tokens teaser. */
|
||||||
|
|
@ -52,6 +53,7 @@ internal object ForYouEarnOpportunitiesPreviewData {
|
||||||
bottomEnd = "Staking",
|
bottomEnd = "Staking",
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
onAllEarnTokensClick = {},
|
||||||
)
|
)
|
||||||
|
|
||||||
/** Every earnable token is already earning: plain subtitle, no reward badge, no rows. */
|
/** Every earnable token is already earning: plain subtitle, no reward badge, no rows. */
|
||||||
|
|
@ -60,6 +62,7 @@ internal object ForYouEarnOpportunitiesPreviewData {
|
||||||
potentialReward = null,
|
potentialReward = null,
|
||||||
potentialRewardType = null,
|
potentialRewardType = null,
|
||||||
tokenList = persistentListOf(),
|
tokenList = persistentListOf(),
|
||||||
|
onAllEarnTokensClick = {},
|
||||||
)
|
)
|
||||||
|
|
||||||
val loading = EarnOpportunitiesUM.Loading(
|
val loading = EarnOpportunitiesUM.Loading(
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,8 @@ package com.tangem.features.foryou.impl.model
|
||||||
|
|
||||||
import arrow.core.right
|
import arrow.core.right
|
||||||
import com.google.common.truth.Truth.assertThat
|
import com.google.common.truth.Truth.assertThat
|
||||||
|
import com.tangem.common.routing.AppRoute
|
||||||
|
import com.tangem.common.routing.AppRouter
|
||||||
import com.tangem.common.test.domain.wallet.MockUserWalletFactory
|
import com.tangem.common.test.domain.wallet.MockUserWalletFactory
|
||||||
import com.tangem.core.decompose.model.MutableParamsContainer
|
import com.tangem.core.decompose.model.MutableParamsContainer
|
||||||
import com.tangem.core.ui.ds.row.token.TangemTokenRowUM
|
import com.tangem.core.ui.ds.row.token.TangemTokenRowUM
|
||||||
|
|
@ -21,15 +23,16 @@ import com.tangem.domain.models.TotalFiatBalance
|
||||||
import com.tangem.domain.models.account.AccountStatus
|
import com.tangem.domain.models.account.AccountStatus
|
||||||
import com.tangem.domain.models.currency.CryptoCurrency
|
import com.tangem.domain.models.currency.CryptoCurrency
|
||||||
import com.tangem.domain.models.currency.CryptoCurrencyStatus
|
import com.tangem.domain.models.currency.CryptoCurrencyStatus
|
||||||
import com.tangem.domain.models.earn.EarnError
|
import com.tangem.domain.models.currency.yieldSupplyKey
|
||||||
import com.tangem.domain.models.earn.EarnRewardType
|
import com.tangem.domain.models.earn.*
|
||||||
import com.tangem.domain.models.earn.EarnToken
|
|
||||||
import com.tangem.domain.models.earn.EarnTokenWithCurrency
|
|
||||||
import com.tangem.domain.models.earn.EarnType
|
|
||||||
import com.tangem.domain.models.network.Network
|
import com.tangem.domain.models.network.Network
|
||||||
import com.tangem.domain.models.wallet.UserWalletId
|
import com.tangem.domain.models.wallet.UserWalletId
|
||||||
|
import com.tangem.domain.staking.model.StakingAvailability
|
||||||
|
import com.tangem.domain.staking.model.StakingIntegrationID
|
||||||
|
import com.tangem.domain.staking.model.StakingOption
|
||||||
import com.tangem.domain.staking.usecase.StakingAvailabilityListUseCase
|
import com.tangem.domain.staking.usecase.StakingAvailabilityListUseCase
|
||||||
import com.tangem.domain.yield.supply.usecase.YieldSupplyApyFlowUseCase
|
import com.tangem.domain.yield.supply.usecase.YieldSupplyApyFlowUseCase
|
||||||
|
import com.tangem.features.commonfeatures.api.addtoportfolio.AddToPortfolioManager
|
||||||
import com.tangem.features.foryou.ForYouComponent
|
import com.tangem.features.foryou.ForYouComponent
|
||||||
import com.tangem.features.foryou.impl.components.state.MarketChartUM
|
import com.tangem.features.foryou.impl.components.state.MarketChartUM
|
||||||
import com.tangem.features.foryou.impl.entity.EarnOpportunitiesUM
|
import com.tangem.features.foryou.impl.entity.EarnOpportunitiesUM
|
||||||
|
|
@ -41,12 +44,7 @@ import com.tangem.pagination.BatchListState
|
||||||
import com.tangem.pagination.PaginationStatus
|
import com.tangem.pagination.PaginationStatus
|
||||||
import com.tangem.test.mock.MockAccounts
|
import com.tangem.test.mock.MockAccounts
|
||||||
import com.tangem.utils.coroutines.TestingCoroutineDispatcherProvider
|
import com.tangem.utils.coroutines.TestingCoroutineDispatcherProvider
|
||||||
import io.mockk.CapturingSlot
|
import io.mockk.*
|
||||||
import io.mockk.coEvery
|
|
||||||
import io.mockk.every
|
|
||||||
import io.mockk.mockk
|
|
||||||
import io.mockk.slot
|
|
||||||
import io.mockk.verify
|
|
||||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import kotlinx.coroutines.flow.first
|
import kotlinx.coroutines.flow.first
|
||||||
|
|
@ -72,6 +70,8 @@ internal class ForYouModelTest {
|
||||||
private val stakingAvailabilityListUseCase: StakingAvailabilityListUseCase = mockk()
|
private val stakingAvailabilityListUseCase: StakingAvailabilityListUseCase = mockk()
|
||||||
private val isAccountsModeEnabledUseCase: IsAccountsModeEnabledUseCase = mockk()
|
private val isAccountsModeEnabledUseCase: IsAccountsModeEnabledUseCase = mockk()
|
||||||
private val earnErrorResolver: EarnErrorResolver = mockk()
|
private val earnErrorResolver: EarnErrorResolver = mockk()
|
||||||
|
private val addToPortfolioManagerFactory: AddToPortfolioManager.Factory = mockk()
|
||||||
|
private val router: AppRouter = mockk(relaxUnitFun = true)
|
||||||
|
|
||||||
private var model: ForYouModel? = null
|
private var model: ForYouModel? = null
|
||||||
|
|
||||||
|
|
@ -286,9 +286,67 @@ internal class ForYouModelTest {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Nested
|
||||||
|
inner class EarnNavigation {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `GIVEN held yield-eligible token WHEN earn row clicked THEN yield-supply entry route is pushed`() =
|
||||||
|
runTest {
|
||||||
|
// Arrange — the token's backend rate is 5.5%
|
||||||
|
val token = createYieldToken()
|
||||||
|
every { yieldSupplyApyFlowUseCase() } returns flowOf(mapOf(token.yieldSupplyKey() to BigDecimal("5.5")))
|
||||||
|
stubSelectedWallet(currencies = listOf(createStatus(token, loadedValue(BigDecimal("100")))))
|
||||||
|
val model = createModel(testScope = this)
|
||||||
|
advanceUntilIdle()
|
||||||
|
|
||||||
|
// Act
|
||||||
|
model.clickFirstEarnRow()
|
||||||
|
|
||||||
|
// Assert — the raw percent string travels into the route
|
||||||
|
val expected = AppRoute.YieldSupplyEntry(
|
||||||
|
userWalletId = WALLET_ID,
|
||||||
|
cryptoCurrency = token,
|
||||||
|
apy = "5.5",
|
||||||
|
)
|
||||||
|
verify { router.push(route = expected, onComplete = any()) }
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `GIVEN held stakeable token WHEN earn row clicked THEN staking route is pushed`() = runTest {
|
||||||
|
// Arrange
|
||||||
|
val currency = createCoin(rawCurrencyId = "eth", symbol = "ETH")
|
||||||
|
val option: StakingOption.P2PEthPool = mockk {
|
||||||
|
every { apy } returns BigDecimal("0.05")
|
||||||
|
every { integrationId } returns StakingIntegrationID.P2PEthPool
|
||||||
|
}
|
||||||
|
coEvery { stakingAvailabilityListUseCase.invokeSync(any(), any()) } returns
|
||||||
|
mapOf(currency to StakingAvailability.Available(option))
|
||||||
|
stubSelectedWallet(currencies = listOf(createStatus(currency, loadedValue(BigDecimal("100")))))
|
||||||
|
val model = createModel(testScope = this)
|
||||||
|
advanceUntilIdle()
|
||||||
|
|
||||||
|
// Act
|
||||||
|
model.clickFirstEarnRow()
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
val expected = AppRoute.Staking(
|
||||||
|
userWalletId = WALLET_ID,
|
||||||
|
cryptoCurrency = currency,
|
||||||
|
integrationId = StakingIntegrationID.P2PEthPool,
|
||||||
|
)
|
||||||
|
verify { router.push(route = expected, onComplete = any()) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private fun PortfolioReviewUM.Content.assetRow(): TangemTokenRowUM.Content =
|
private fun PortfolioReviewUM.Content.assetRow(): TangemTokenRowUM.Content =
|
||||||
tokenList.single().tokenRowUM as TangemTokenRowUM.Content
|
tokenList.single().tokenRowUM as TangemTokenRowUM.Content
|
||||||
|
|
||||||
|
private fun ForYouModel.clickFirstEarnRow() {
|
||||||
|
val earn = uiState.value.earnOpportunities as EarnOpportunitiesUM.Content
|
||||||
|
val row = earn.tokenList.first().tokenRowUM as TangemTokenRowUM.Content
|
||||||
|
row.onItemClick?.invoke()
|
||||||
|
}
|
||||||
|
|
||||||
/** Wires the repository + supplier so the model derives Content from a single selected wallet. */
|
/** Wires the repository + supplier so the model derives Content from a single selected wallet. */
|
||||||
private fun stubSelectedWallet(
|
private fun stubSelectedWallet(
|
||||||
currencies: List<CryptoCurrencyStatus>,
|
currencies: List<CryptoCurrencyStatus>,
|
||||||
|
|
@ -343,18 +401,21 @@ internal class ForYouModelTest {
|
||||||
ForYouComponent.Params(
|
ForYouComponent.Params(
|
||||||
callbacks = object : ForYouComponent.ForYouModelCallbacks {
|
callbacks = object : ForYouComponent.ForYouModelCallbacks {
|
||||||
override fun onTokenClick(userWalletId: UserWalletId, currency: CryptoCurrency) = Unit
|
override fun onTokenClick(userWalletId: UserWalletId, currency: CryptoCurrency) = Unit
|
||||||
|
override fun onAllEarnTokensClick() = Unit
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
userWalletsListRepository = userWalletsListRepository,
|
userWalletsListRepository = userWalletsListRepository,
|
||||||
multiAccountStatusListSupplier = multiAccountStatusListSupplier,
|
multiAccountStatusListSupplier = multiAccountStatusListSupplier,
|
||||||
yieldSupplyApyFlowUseCase = yieldSupplyApyFlowUseCase,
|
yieldSupplyApyFlowUseCase = yieldSupplyApyFlowUseCase,
|
||||||
|
router = router,
|
||||||
dispatchers = testScope.createTestingCoroutineDispatcherProvider(),
|
dispatchers = testScope.createTestingCoroutineDispatcherProvider(),
|
||||||
getSelectedAppCurrencyUseCase = getSelectedAppCurrencyUseCase,
|
getSelectedAppCurrencyUseCase = getSelectedAppCurrencyUseCase,
|
||||||
getEarnTokensBatchFlowUseCase = getEarnTokensBatchFlowUseCase,
|
getEarnTokensBatchFlowUseCase = getEarnTokensBatchFlowUseCase,
|
||||||
stakingAvailabilityListUseCase = stakingAvailabilityListUseCase,
|
stakingAvailabilityListUseCase = stakingAvailabilityListUseCase,
|
||||||
isAccountsModeEnabledUseCase = isAccountsModeEnabledUseCase,
|
isAccountsModeEnabledUseCase = isAccountsModeEnabledUseCase,
|
||||||
earnErrorResolver = earnErrorResolver,
|
earnErrorResolver = earnErrorResolver,
|
||||||
|
addToPortfolioManagerFactory = addToPortfolioManagerFactory,
|
||||||
).also { model = it }
|
).also { model = it }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -424,6 +485,25 @@ internal class ForYouModelTest {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** A token whose `yieldSupplyKey()` resolves to `"ethereum_0xabc"`. */
|
||||||
|
private fun createYieldToken(): CryptoCurrency.Token {
|
||||||
|
val network = createNetwork(networkRawId = "ethereum")
|
||||||
|
val currencyId: CryptoCurrency.ID = mockk {
|
||||||
|
every { value } returns "token-usdc"
|
||||||
|
every { rawCurrencyId } returns CryptoCurrency.RawID("usd-coin")
|
||||||
|
}
|
||||||
|
return mockk {
|
||||||
|
every { id } returns currencyId
|
||||||
|
every { symbol } returns "USDC"
|
||||||
|
every { name } returns "USD Coin"
|
||||||
|
every { this@mockk.network } returns network
|
||||||
|
every { decimals } returns 6
|
||||||
|
every { isCustom } returns false
|
||||||
|
every { iconUrl } returns null
|
||||||
|
every { contractAddress } returns "0xabc"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private fun createNetwork(networkRawId: String): Network {
|
private fun createNetwork(networkRawId: String): Network {
|
||||||
val networkId: Network.ID = mockk {
|
val networkId: Network.ID = mockk {
|
||||||
every { rawId } returns Network.RawID(networkRawId)
|
every { rawId } returns Network.RawID(networkRawId)
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,7 @@ import com.tangem.domain.models.earn.EarnType
|
||||||
import com.tangem.domain.models.network.Network
|
import com.tangem.domain.models.network.Network
|
||||||
import com.tangem.domain.models.staking.StakingBalance
|
import com.tangem.domain.models.staking.StakingBalance
|
||||||
import com.tangem.domain.models.yield.supply.YieldSupplyStatus
|
import com.tangem.domain.models.yield.supply.YieldSupplyStatus
|
||||||
|
import com.tangem.features.foryou.impl.entity.ForYouEarnOpportunitiesType
|
||||||
import com.tangem.features.foryou.impl.model.converter.EarnApyInfo
|
import com.tangem.features.foryou.impl.model.converter.EarnApyInfo
|
||||||
import com.tangem.features.foryou.impl.model.converter.EarnOpportunities
|
import com.tangem.features.foryou.impl.model.converter.EarnOpportunities
|
||||||
import com.tangem.test.mock.MockAccounts
|
import com.tangem.test.mock.MockAccounts
|
||||||
|
|
@ -180,7 +181,8 @@ internal fun createEarnApyInfo(
|
||||||
isActive: Boolean = true,
|
isActive: Boolean = true,
|
||||||
apy: BigDecimal? = BigDecimal("0.05"),
|
apy: BigDecimal? = BigDecimal("0.05"),
|
||||||
potentialRewards: BigDecimal? = null,
|
potentialRewards: BigDecimal? = null,
|
||||||
): EarnApyInfo = EarnApyInfo(isActive = isActive, apy = apy, potentialRewards = potentialRewards)
|
type: ForYouEarnOpportunitiesType = ForYouEarnOpportunitiesType.YieldSupply(apy = "5.5"),
|
||||||
|
): EarnApyInfo = EarnApyInfo(isActive = isActive, apy = apy, potentialRewards = potentialRewards, type = type)
|
||||||
|
|
||||||
internal fun createPortfolioStatus(
|
internal fun createPortfolioStatus(
|
||||||
currencies: List<CryptoCurrencyStatus>,
|
currencies: List<CryptoCurrencyStatus>,
|
||||||
|
|
@ -192,4 +194,5 @@ internal fun createPortfolioStatus(
|
||||||
|
|
||||||
internal fun createAccountStatusList(vararg statuses: AccountStatus): AccountStatusList = mockk {
|
internal fun createAccountStatusList(vararg statuses: AccountStatus): AccountStatusList = mockk {
|
||||||
every { accountStatuses } returns statuses.toList()
|
every { accountStatuses } returns statuses.toList()
|
||||||
|
every { userWalletId } returns MockAccounts.userWalletId
|
||||||
}
|
}
|
||||||
|
|
@ -15,12 +15,14 @@ import com.tangem.domain.models.currency.yieldSupplyKey
|
||||||
import com.tangem.domain.models.earn.EarnTopToken
|
import com.tangem.domain.models.earn.EarnTopToken
|
||||||
import com.tangem.domain.models.staking.BalanceItem
|
import com.tangem.domain.models.staking.BalanceItem
|
||||||
import com.tangem.domain.models.staking.StakingBalance
|
import com.tangem.domain.models.staking.StakingBalance
|
||||||
|
import com.tangem.domain.models.wallet.UserWalletId
|
||||||
import com.tangem.domain.staking.model.StakingAvailability
|
import com.tangem.domain.staking.model.StakingAvailability
|
||||||
import com.tangem.domain.staking.model.StakingIntegrationID
|
import com.tangem.domain.staking.model.StakingIntegrationID
|
||||||
import com.tangem.domain.staking.model.StakingOption
|
import com.tangem.domain.staking.model.StakingOption
|
||||||
import com.tangem.domain.staking.model.common.RewardInfo
|
import com.tangem.domain.staking.model.common.RewardInfo
|
||||||
import com.tangem.domain.staking.model.common.RewardType
|
import com.tangem.domain.staking.model.common.RewardType
|
||||||
import com.tangem.domain.staking.model.stakekit.Yield
|
import com.tangem.domain.staking.model.stakekit.Yield
|
||||||
|
import com.tangem.features.foryou.impl.entity.ForYouEarnOpportunitiesType
|
||||||
import com.tangem.features.foryou.impl.entity.EarnOpportunitiesUM
|
import com.tangem.features.foryou.impl.entity.EarnOpportunitiesUM
|
||||||
import com.tangem.test.mock.MockAccounts
|
import com.tangem.test.mock.MockAccounts
|
||||||
import io.mockk.every
|
import io.mockk.every
|
||||||
|
|
@ -290,6 +292,55 @@ internal class ForYouEarnOpportunitiesConverterTest {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Nested
|
||||||
|
inner class TypeResolution {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `GIVEN yield-eligible token row clicked WHEN convert THEN yield type carries the raw percent apy`() {
|
||||||
|
// Arrange — the backend rate (10.00%) must reach the click callback unscaled
|
||||||
|
val token = createEarnTokenCurrency()
|
||||||
|
val status = createStatus(token, createEarnStatusValue(fiatAmount = BigDecimal("100")))
|
||||||
|
var clickedType: ForYouEarnOpportunitiesType? = null
|
||||||
|
val converter = createConverter(
|
||||||
|
yieldSupplyAvailability = mapOf(token.yieldSupplyKey() to BigDecimal("10.00")),
|
||||||
|
onTokenClick = { _, _, type -> clickedType = type },
|
||||||
|
)
|
||||||
|
|
||||||
|
// Act
|
||||||
|
val result = converter.convert(createAccountStatusList(createPortfolioStatus(listOf(status))))
|
||||||
|
result.clickFirstRow()
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
assertThat(clickedType).isEqualTo(ForYouEarnOpportunitiesType.YieldSupply(apy = "10.00"))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `GIVEN staking-eligible token row clicked WHEN convert THEN staking type carries the integration id`() {
|
||||||
|
// Arrange
|
||||||
|
val currency = createEarnCurrency()
|
||||||
|
val status = createStatus(currency, createEarnStatusValue(fiatAmount = BigDecimal("100")))
|
||||||
|
val availability = stakingAvailable(apy = BigDecimal("0.05"))
|
||||||
|
var clickedType: ForYouEarnOpportunitiesType? = null
|
||||||
|
val converter = createConverter(
|
||||||
|
yieldStakingAvailability = mapOf(currency to availability),
|
||||||
|
onTokenClick = { _, _, type -> clickedType = type },
|
||||||
|
)
|
||||||
|
|
||||||
|
// Act
|
||||||
|
val result = converter.convert(createAccountStatusList(createPortfolioStatus(listOf(status))))
|
||||||
|
result.clickFirstRow()
|
||||||
|
|
||||||
|
// Assert — the id comes from the resolved staking option
|
||||||
|
val option = (availability as StakingAvailability.Available).option
|
||||||
|
assertThat(clickedType).isEqualTo(ForYouEarnOpportunitiesType.Staking(integrationID = option.integrationId))
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun EarnOpportunitiesUM.clickFirstRow() {
|
||||||
|
val row = tokenList.first().tokenRowUM as TangemTokenRowUM.Content
|
||||||
|
row.onItemClick?.invoke()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Nested
|
@Nested
|
||||||
inner class AccountOrdering {
|
inner class AccountOrdering {
|
||||||
|
|
||||||
|
|
@ -328,6 +379,7 @@ internal class ForYouEarnOpportunitiesConverterTest {
|
||||||
yieldStakingAvailability: Map<CryptoCurrency, StakingAvailability> = emptyMap(),
|
yieldStakingAvailability: Map<CryptoCurrency, StakingAvailability> = emptyMap(),
|
||||||
topEarnTokens: EarnTopToken? = null,
|
topEarnTokens: EarnTopToken? = null,
|
||||||
isAccountsModeEnabled: Boolean = false,
|
isAccountsModeEnabled: Boolean = false,
|
||||||
|
onTokenClick: (UserWalletId?, CryptoCurrency, ForYouEarnOpportunitiesType) -> Unit = { _, _, _ -> },
|
||||||
) = ForYouEarnOpportunitiesConverter(
|
) = ForYouEarnOpportunitiesConverter(
|
||||||
appCurrency = appCurrency,
|
appCurrency = appCurrency,
|
||||||
isAccountsModeEnabled = isAccountsModeEnabled,
|
isAccountsModeEnabled = isAccountsModeEnabled,
|
||||||
|
|
@ -336,19 +388,23 @@ internal class ForYouEarnOpportunitiesConverterTest {
|
||||||
yieldSupplyAvailability = yieldSupplyAvailability,
|
yieldSupplyAvailability = yieldSupplyAvailability,
|
||||||
yieldStakingAvailability = yieldStakingAvailability,
|
yieldStakingAvailability = yieldStakingAvailability,
|
||||||
topEarnTokens = topEarnTokens,
|
topEarnTokens = topEarnTokens,
|
||||||
|
onTokenClick = onTokenClick,
|
||||||
|
onAllEarnTokensClick = {},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// Real StakingIntegrationID values are used below: mocking the sealed interface makes mockk try to
|
||||||
|
// retransform its enum implementations, which the JVM rejects ("cannot change the class modifiers").
|
||||||
private fun stakingOption(apy: BigDecimal): StakingOption.P2PEthPool = mockk {
|
private fun stakingOption(apy: BigDecimal): StakingOption.P2PEthPool = mockk {
|
||||||
every { this@mockk.apy } returns apy
|
every { this@mockk.apy } returns apy
|
||||||
|
every { integrationId } returns StakingIntegrationID.P2PEthPool
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun stakingAvailable(apy: BigDecimal): StakingAvailability =
|
private fun stakingAvailable(apy: BigDecimal): StakingAvailability =
|
||||||
StakingAvailability.Available(option = stakingOption(apy))
|
StakingAvailability.Available(option = stakingOption(apy))
|
||||||
|
|
||||||
private fun stakeKitAvailable(vararg validatorList: Yield.Validator): StakingAvailability {
|
private fun stakeKitAvailable(vararg validatorList: Yield.Validator): StakingAvailability {
|
||||||
// A real StakeKit option with a real integration id: stubbing `integrationId` on a mock would
|
// A real StakeKit option: stubbing `integrationId` on a mock would make mockk instrument
|
||||||
// make mockk instrument StakingIntegrationID.StakeKit, whose implementations are enums that the
|
// StakingIntegrationID.StakeKit, hitting the same enum-retransformation limitation.
|
||||||
// JVM refuses to retransform ("cannot change the class modifiers").
|
|
||||||
val yieldModel: Yield = mockk {
|
val yieldModel: Yield = mockk {
|
||||||
every { validators } returns validatorList.toList()
|
every { validators } returns validatorList.toList()
|
||||||
every { apy } returns BigDecimal("0.10")
|
every { apy } returns BigDecimal("0.10")
|
||||||
|
|
|
||||||
|
|
@ -20,6 +20,8 @@ internal class ForYouEarnOpportunitiesNoTokensConverterTest {
|
||||||
topEarnTokens = List(7) { index ->
|
topEarnTokens = List(7) { index ->
|
||||||
createTopEarnToken(tokenId = "token-$index", networkRawId = "NET")
|
createTopEarnToken(tokenId = "token-$index", networkRawId = "NET")
|
||||||
}.right(),
|
}.right(),
|
||||||
|
onTokenClick = { _, _, _ -> },
|
||||||
|
onAllEarnTokensClick = {},
|
||||||
)
|
)
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
|
|
@ -39,6 +41,8 @@ internal class ForYouEarnOpportunitiesNoTokensConverterTest {
|
||||||
createTopEarnToken(apy = "7.25", rewardType = EarnRewardType.APR),
|
createTopEarnToken(apy = "7.25", rewardType = EarnRewardType.APR),
|
||||||
createTopEarnToken(tokenId = "solana", apy = "99.9", rewardType = EarnRewardType.APY),
|
createTopEarnToken(tokenId = "solana", apy = "99.9", rewardType = EarnRewardType.APY),
|
||||||
).right(),
|
).right(),
|
||||||
|
onTokenClick = { _, _, _ -> },
|
||||||
|
onAllEarnTokensClick = {},
|
||||||
)
|
)
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
|
|
@ -56,6 +60,8 @@ internal class ForYouEarnOpportunitiesNoTokensConverterTest {
|
||||||
// Arrange
|
// Arrange
|
||||||
val converter = ForYouEarnOpportunitiesNoTokensConverter(
|
val converter = ForYouEarnOpportunitiesNoTokensConverter(
|
||||||
topEarnTokens = null,
|
topEarnTokens = null,
|
||||||
|
onTokenClick = { _, _, _ -> },
|
||||||
|
onAllEarnTokensClick = {},
|
||||||
)
|
)
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,9 @@ import com.tangem.core.ui.extensions.wrappedList
|
||||||
import com.tangem.core.ui.format.bigdecimal.fiat
|
import com.tangem.core.ui.format.bigdecimal.fiat
|
||||||
import com.tangem.core.ui.format.bigdecimal.format
|
import com.tangem.core.ui.format.bigdecimal.format
|
||||||
import com.tangem.domain.appcurrency.model.AppCurrency
|
import com.tangem.domain.appcurrency.model.AppCurrency
|
||||||
|
import com.tangem.domain.models.currency.CryptoCurrency
|
||||||
|
import com.tangem.domain.models.wallet.UserWalletId
|
||||||
|
import com.tangem.features.foryou.impl.entity.ForYouEarnOpportunitiesType
|
||||||
import com.tangem.features.foryou.impl.entity.EarnOpportunitiesUM
|
import com.tangem.features.foryou.impl.entity.EarnOpportunitiesUM
|
||||||
import com.tangem.test.mock.MockAccounts
|
import com.tangem.test.mock.MockAccounts
|
||||||
import org.junit.jupiter.api.Test
|
import org.junit.jupiter.api.Test
|
||||||
|
|
@ -108,6 +111,32 @@ internal class ForYouEarnOpportunitiesPotentialRewardsConverterTest {
|
||||||
assertThat(clickedAssetId).isEqualTo(account.accountId.value)
|
assertThat(clickedAssetId).isEqualTo(account.accountId.value)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `GIVEN token row clicked WHEN convert THEN token callback receives wallet currency and earn type`() {
|
||||||
|
// Arrange
|
||||||
|
val currency = createEarnCurrency()
|
||||||
|
val earnType = ForYouEarnOpportunitiesType.YieldSupply(apy = "7.5")
|
||||||
|
val earnData = createEarnOpportunities(
|
||||||
|
earnCurrencies = mapOf(
|
||||||
|
createStatus(currency, createRowLoadedValue()) to createEarnApyInfo(isActive = false, type = earnType),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
val walletId = UserWalletId("01")
|
||||||
|
var clicked: Triple<UserWalletId?, CryptoCurrency, ForYouEarnOpportunitiesType>? = null
|
||||||
|
val converter = createConverter(
|
||||||
|
isAccountsModeEnabled = false,
|
||||||
|
userWalletId = walletId,
|
||||||
|
onTokenClick = { id, clickedCurrency, type -> clicked = Triple(id, clickedCurrency, type) },
|
||||||
|
)
|
||||||
|
|
||||||
|
// Act
|
||||||
|
val result = converter.convert(listOf(earnData)) as EarnOpportunitiesUM.Content
|
||||||
|
(result.tokenList.single().tokenRowUM as TangemTokenRowUM.Content).onItemClick?.invoke()
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
assertThat(clicked).isEqualTo(Triple(walletId, currency, earnType))
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `GIVEN several accounts WHEN convert THEN header reward is the sum across accounts`() {
|
fun `GIVEN several accounts WHEN convert THEN header reward is the sum across accounts`() {
|
||||||
// Arrange
|
// Arrange
|
||||||
|
|
@ -137,10 +166,15 @@ internal class ForYouEarnOpportunitiesPotentialRewardsConverterTest {
|
||||||
isAccountsModeEnabled: Boolean,
|
isAccountsModeEnabled: Boolean,
|
||||||
expandedAssetIds: Set<String> = emptySet(),
|
expandedAssetIds: Set<String> = emptySet(),
|
||||||
expandClick: (String) -> Unit = {},
|
expandClick: (String) -> Unit = {},
|
||||||
|
userWalletId: UserWalletId? = UserWalletId("01"),
|
||||||
|
onTokenClick: (UserWalletId?, CryptoCurrency, ForYouEarnOpportunitiesType) -> Unit = { _, _, _ -> },
|
||||||
) = ForYouEarnOpportunitiesPotentialRewardsConverter(
|
) = ForYouEarnOpportunitiesPotentialRewardsConverter(
|
||||||
appCurrency = appCurrency,
|
appCurrency = appCurrency,
|
||||||
|
userWalletId = userWalletId,
|
||||||
isAccountsModeEnabled = isAccountsModeEnabled,
|
isAccountsModeEnabled = isAccountsModeEnabled,
|
||||||
expandedAssetIds = expandedAssetIds,
|
expandedAssetIds = expandedAssetIds,
|
||||||
expandClick = expandClick,
|
expandClick = expandClick,
|
||||||
|
onTokenClick = onTokenClick,
|
||||||
|
onAllEarnTokensClick = {},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
@ -13,7 +13,10 @@ import com.tangem.core.ui.format.bigdecimal.format
|
||||||
import com.tangem.core.ui.format.bigdecimal.percent
|
import com.tangem.core.ui.format.bigdecimal.percent
|
||||||
import com.tangem.domain.appcurrency.model.AppCurrency
|
import com.tangem.domain.appcurrency.model.AppCurrency
|
||||||
import com.tangem.domain.models.StatusSource
|
import com.tangem.domain.models.StatusSource
|
||||||
|
import com.tangem.domain.models.currency.CryptoCurrency
|
||||||
import com.tangem.domain.models.currency.CryptoCurrencyStatus
|
import com.tangem.domain.models.currency.CryptoCurrencyStatus
|
||||||
|
import com.tangem.domain.models.wallet.UserWalletId
|
||||||
|
import com.tangem.features.foryou.impl.entity.ForYouEarnOpportunitiesType
|
||||||
import com.tangem.utils.StringsSigns
|
import com.tangem.utils.StringsSigns
|
||||||
import io.mockk.every
|
import io.mockk.every
|
||||||
import io.mockk.mockk
|
import io.mockk.mockk
|
||||||
|
|
@ -23,7 +26,16 @@ import java.math.BigDecimal
|
||||||
internal class ForYouEarnOpportunitiesTokenRowConverterTest {
|
internal class ForYouEarnOpportunitiesTokenRowConverterTest {
|
||||||
|
|
||||||
private val appCurrency: AppCurrency = AppCurrency.Default
|
private val appCurrency: AppCurrency = AppCurrency.Default
|
||||||
private val converter = ForYouEarnOpportunitiesTokenRowConverter(appCurrency = appCurrency)
|
private val converter = createConverter()
|
||||||
|
|
||||||
|
private fun createConverter(
|
||||||
|
userWalletId: UserWalletId? = UserWalletId("01"),
|
||||||
|
onTokenClick: (UserWalletId?, CryptoCurrency, ForYouEarnOpportunitiesType) -> Unit = { _, _, _ -> },
|
||||||
|
) = ForYouEarnOpportunitiesTokenRowConverter(
|
||||||
|
appCurrency = appCurrency,
|
||||||
|
userWalletId = userWalletId,
|
||||||
|
onTokenClick = onTokenClick,
|
||||||
|
)
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `GIVEN loading status WHEN convert THEN row is Loading with currency id`() {
|
fun `GIVEN loading status WHEN convert THEN row is Loading with currency id`() {
|
||||||
|
|
@ -95,6 +107,27 @@ internal class ForYouEarnOpportunitiesTokenRowConverterTest {
|
||||||
assertThat(bottomEnd.startIcons).hasSize(1)
|
assertThat(bottomEnd.startIcons).hasSize(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `GIVEN row clicked WHEN convert THEN callback receives wallet currency and resolved earn type`() {
|
||||||
|
// Arrange
|
||||||
|
val currency = createEarnCurrency()
|
||||||
|
val status = createStatus(currency, createRowLoadedValue())
|
||||||
|
val earnType = ForYouEarnOpportunitiesType.YieldSupply(apy = "5.5")
|
||||||
|
val walletId = UserWalletId("01")
|
||||||
|
var clicked: Triple<UserWalletId?, CryptoCurrency, ForYouEarnOpportunitiesType>? = null
|
||||||
|
val converter = createConverter(
|
||||||
|
userWalletId = walletId,
|
||||||
|
onTokenClick = { id, clickedCurrency, type -> clicked = Triple(id, clickedCurrency, type) },
|
||||||
|
)
|
||||||
|
|
||||||
|
// Act
|
||||||
|
val result = converter.convert(status to createEarnApyInfo(type = earnType)) as TangemTokenRowUM.Content
|
||||||
|
result.onItemClick?.invoke()
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
assertThat(clicked).isEqualTo(Triple(walletId, currency, earnType))
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `GIVEN unreachable status WHEN convert THEN both ends are dashes`() {
|
fun `GIVEN unreachable status WHEN convert THEN both ends are dashes`() {
|
||||||
// Arrange
|
// Arrange
|
||||||
|
|
|
||||||
|
|
@ -24,6 +24,8 @@ internal class ForYouEarnOpportunitiesTokensActiveConverterTest {
|
||||||
createTopEarnToken(tokenId = "ethereum", networkRawId = "ETH"),
|
createTopEarnToken(tokenId = "ethereum", networkRawId = "ETH"),
|
||||||
createTopEarnToken(tokenId = "solana", networkRawId = "SOL"),
|
createTopEarnToken(tokenId = "solana", networkRawId = "SOL"),
|
||||||
).right(),
|
).right(),
|
||||||
|
onTokenClick = { _, _, _ -> },
|
||||||
|
onAllEarnTokensClick = {},
|
||||||
)
|
)
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
|
|
@ -45,6 +47,8 @@ internal class ForYouEarnOpportunitiesTokensActiveConverterTest {
|
||||||
topEarnTokens = List(8) { index ->
|
topEarnTokens = List(8) { index ->
|
||||||
createTopEarnToken(tokenId = "token-$index", networkRawId = "NET")
|
createTopEarnToken(tokenId = "token-$index", networkRawId = "NET")
|
||||||
}.right(),
|
}.right(),
|
||||||
|
onTokenClick = { _, _, _ -> },
|
||||||
|
onAllEarnTokensClick = {},
|
||||||
)
|
)
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
|
|
@ -69,6 +73,8 @@ internal class ForYouEarnOpportunitiesTokensActiveConverterTest {
|
||||||
createTopEarnToken(tokenId = "usd-coin", networkRawId = "ETH"),
|
createTopEarnToken(tokenId = "usd-coin", networkRawId = "ETH"),
|
||||||
createTopEarnToken(tokenId = "usd-coin", networkRawId = "SOL"),
|
createTopEarnToken(tokenId = "usd-coin", networkRawId = "SOL"),
|
||||||
).right(),
|
).right(),
|
||||||
|
onTokenClick = { _, _, _ -> },
|
||||||
|
onAllEarnTokensClick = {},
|
||||||
)
|
)
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
|
|
@ -80,9 +86,12 @@ internal class ForYouEarnOpportunitiesTokensActiveConverterTest {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `GIVEN no top tokens loaded WHEN convert THEN content with empty suggestions`() {
|
fun `GIVEN no top tokens loaded WHEN convert THEN content with empty suggestions`() {
|
||||||
// Arrange
|
// Arrange — the callback instance is shared with `expected` so the whole-object equality holds
|
||||||
|
val onAllEarnTokensClick: () -> Unit = {}
|
||||||
val converter = ForYouEarnOpportunitiesTokensActiveConverter(
|
val converter = ForYouEarnOpportunitiesTokensActiveConverter(
|
||||||
topEarnTokens = null,
|
topEarnTokens = null,
|
||||||
|
onTokenClick = { _, _, _ -> },
|
||||||
|
onAllEarnTokensClick = onAllEarnTokensClick,
|
||||||
)
|
)
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
|
|
@ -94,6 +103,7 @@ internal class ForYouEarnOpportunitiesTokensActiveConverterTest {
|
||||||
subtitleRes = R.string.for_you_earn_opportunities_all_tokens_active,
|
subtitleRes = R.string.for_you_earn_opportunities_all_tokens_active,
|
||||||
potentialReward = null,
|
potentialReward = null,
|
||||||
potentialRewardType = null,
|
potentialRewardType = null,
|
||||||
|
onAllEarnTokensClick = onAllEarnTokensClick,
|
||||||
)
|
)
|
||||||
assertThat(result).isEqualTo(expected)
|
assertThat(result).isEqualTo(expected)
|
||||||
}
|
}
|
||||||
|
|
@ -103,6 +113,8 @@ internal class ForYouEarnOpportunitiesTokensActiveConverterTest {
|
||||||
// Arrange
|
// Arrange
|
||||||
val converter = ForYouEarnOpportunitiesTokensActiveConverter(
|
val converter = ForYouEarnOpportunitiesTokensActiveConverter(
|
||||||
topEarnTokens = EarnError.NotHttpError().left(),
|
topEarnTokens = EarnError.NotHttpError().left(),
|
||||||
|
onTokenClick = { _, _, _ -> },
|
||||||
|
onAllEarnTokensClick = {},
|
||||||
)
|
)
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
|
|
|
||||||
|
|
@ -9,13 +9,16 @@ import com.tangem.core.ui.extensions.stringReference
|
||||||
import com.tangem.core.ui.extensions.wrappedList
|
import com.tangem.core.ui.extensions.wrappedList
|
||||||
import com.tangem.core.ui.format.bigdecimal.format
|
import com.tangem.core.ui.format.bigdecimal.format
|
||||||
import com.tangem.core.ui.format.bigdecimal.percent
|
import com.tangem.core.ui.format.bigdecimal.percent
|
||||||
|
import com.tangem.domain.models.currency.CryptoCurrency
|
||||||
import com.tangem.domain.models.earn.EarnType
|
import com.tangem.domain.models.earn.EarnType
|
||||||
|
import com.tangem.domain.models.wallet.UserWalletId
|
||||||
|
import com.tangem.features.foryou.impl.entity.ForYouEarnOpportunitiesType
|
||||||
import org.junit.jupiter.api.Test
|
import org.junit.jupiter.api.Test
|
||||||
import java.math.BigDecimal
|
import java.math.BigDecimal
|
||||||
|
|
||||||
internal class ForYouEarnOpportunitiesTopTokenRowConverterTest {
|
internal class ForYouEarnOpportunitiesTopTokenRowConverterTest {
|
||||||
|
|
||||||
private val converter = ForYouEarnOpportunitiesTopTokenRowConverter()
|
private val converter = ForYouEarnOpportunitiesTopTokenRowConverter(onTokenClick = { _, _, _ -> })
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `GIVEN top-earn token WHEN convert THEN row carries currency identity and network subtitle`() {
|
fun `GIVEN top-earn token WHEN convert THEN row carries currency identity and network subtitle`() {
|
||||||
|
|
@ -77,6 +80,25 @@ internal class ForYouEarnOpportunitiesTopTokenRowConverterTest {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `GIVEN yield token row clicked WHEN convert THEN callback gets null wallet and yield type with raw apy`() {
|
||||||
|
// Arrange — suggestions are tokens the user doesn't hold, so no wallet id is forwarded
|
||||||
|
val topToken = createTopEarnToken(type = EarnType.YIELD, apy = "7.25")
|
||||||
|
var clicked: Triple<UserWalletId?, CryptoCurrency, ForYouEarnOpportunitiesType>? = null
|
||||||
|
val converter = ForYouEarnOpportunitiesTopTokenRowConverter(
|
||||||
|
onTokenClick = { id, currency, type -> clicked = Triple(id, currency, type) },
|
||||||
|
)
|
||||||
|
|
||||||
|
// Act
|
||||||
|
val result = converter.convert(topToken)
|
||||||
|
(result.tokenRowUM as TangemTokenRowUM.Content).onItemClick?.invoke()
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
assertThat(clicked).isEqualTo(
|
||||||
|
Triple(null, topToken.cryptoCurrency, ForYouEarnOpportunitiesType.YieldSupply(apy = "7.25")),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `GIVEN top-earn token WHEN convert THEN item is a flat non-expandable row`() {
|
fun `GIVEN top-earn token WHEN convert THEN item is a flat non-expandable row`() {
|
||||||
// Act
|
// Act
|
||||||
|
|
|
||||||
|
|
@ -145,6 +145,7 @@ internal class SetPortfolioReviewTransformerTest {
|
||||||
subtitleRes = 0,
|
subtitleRes = 0,
|
||||||
potentialReward = null,
|
potentialReward = null,
|
||||||
potentialRewardType = null,
|
potentialRewardType = null,
|
||||||
|
onAllEarnTokensClick = {},
|
||||||
)
|
)
|
||||||
|
|
||||||
private fun accountStatusList(totalFiatBalance: TotalFiatBalance): AccountStatusList = mockk {
|
private fun accountStatusList(totalFiatBalance: TotalFiatBalance): AccountStatusList = mockk {
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue