Updated on 2026-08-14

This commit is contained in:
Tangem 2025-11-20 19:07:32 +05:00
parent e0fb0e3587
commit 4038e1b134
4 changed files with 118 additions and 65 deletions

View file

@ -1,21 +1,33 @@
package com.tangem.domain.models.currency
import java.math.BigDecimal
fun CryptoCurrency.Token.yieldSupplyKey(): String {
return "${network.backendId}_$contractAddress"
}
fun CryptoCurrencyStatus.yieldSupplyNotAllAmountSupplied(): Boolean {
if (this.currency !is CryptoCurrency.Token) return false
fun CryptoCurrencyStatus.hasNotSuppliedAmount(): Boolean {
val notSupplied = notSuppliedAmountOrNull() ?: return false
return notSupplied > BigDecimal.ZERO
}
fun CryptoCurrencyStatus.shouldShowNotSuppliedInfoIcon(minAmount: BigDecimal): Boolean {
val notSupplied = notSuppliedAmountOrNull() ?: return false
return notSupplied >= minAmount
}
fun CryptoCurrencyStatus.notSuppliedAmountOrNull(): BigDecimal? {
if (this.currency !is CryptoCurrency.Token) return null
val supplyStatus = this.value.yieldSupplyStatus
if (supplyStatus?.isActive != true) return false
if (supplyStatus?.isActive != true) return null
val protocolBalance = supplyStatus.effectiveProtocolBalance
val amount = this.value.amount
return if (protocolBalance != null && amount != null) {
amount > protocolBalance
amount.minus(protocolBalance)
} else {
false
null
}
}

View file

@ -17,7 +17,8 @@ import com.tangem.domain.balancehiding.GetBalanceHidingSettingsUseCase
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.yieldSupplyNotAllAmountSupplied
import com.tangem.domain.models.currency.hasNotSuppliedAmount
import com.tangem.domain.models.currency.shouldShowNotSuppliedInfoIcon
import com.tangem.domain.models.wallet.UserWallet
import com.tangem.domain.models.yield.supply.YieldSupplyStatus
import com.tangem.domain.networks.single.SingleNetworkStatusFetcher
@ -29,6 +30,7 @@ import com.tangem.domain.yield.supply.usecase.YieldSupplyActivateUseCase
import com.tangem.domain.yield.supply.usecase.YieldSupplyDeactivateUseCase
import com.tangem.domain.yield.supply.usecase.YieldSupplyGetTokenStatusUseCase
import com.tangem.domain.yield.supply.usecase.YieldSupplyIsAvailableUseCase
import com.tangem.domain.yield.supply.usecase.YieldSupplyMinAmountUseCase
import com.tangem.features.yield.supply.api.YieldSupplyComponent
import com.tangem.features.yield.supply.api.analytics.YieldSupplyAnalytics
import com.tangem.features.yield.supply.impl.R
@ -64,6 +66,7 @@ internal class YieldSupplyModel @Inject constructor(
private val yieldSupplyActivateUseCase: YieldSupplyActivateUseCase,
private val yieldSupplyDeactivateUseCase: YieldSupplyDeactivateUseCase,
private val yieldSupplyRepository: YieldSupplyRepository,
private val yieldSupplyMinAmountUseCase: YieldSupplyMinAmountUseCase,
) : Model(), YieldSupplyClickIntents {
private val params = paramsContainer.require<YieldSupplyComponent.Params>()
@ -303,7 +306,10 @@ internal class YieldSupplyModel @Inject constructor(
private fun loadActiveState(cryptoCurrencyStatus: CryptoCurrencyStatus, yieldSupplyStatus: YieldSupplyStatus) {
val cryptoCurrencyToken = cryptoCurrency as? CryptoCurrency.Token ?: return
val showWarningIcon = !yieldSupplyStatus.isAllowedToSpend
val showInfoIcon = cryptoCurrencyStatus.yieldSupplyNotAllAmountSupplied()
val showInfoIconPrevState = when (uiState) {
is YieldSupplyUM.Content -> uiState.showInfoIcon
else -> false
}
if (!yieldSupplyStatus.isAllowedToSpend) {
analyticsEventsHandler.send(
YieldSupplyAnalytics.NoticeApproveNeeded(
@ -331,10 +337,11 @@ internal class YieldSupplyModel @Inject constructor(
),
onClick = ::onActiveClick,
showWarningIcon = showWarningIcon,
showInfoIcon = showInfoIcon,
showInfoIcon = showInfoIconPrevState,
apy = tokenStatus.apy.toString(),
)
}
computeAndApplyShowInfoIcon(cryptoCurrencyStatus)
}.onLeft {
Timber.e(it)
uiState.update {
@ -348,14 +355,36 @@ internal class YieldSupplyModel @Inject constructor(
rewardsApy = TextReference.EMPTY,
onClick = ::onActiveClick,
showWarningIcon = showWarningIcon,
showInfoIcon = showInfoIcon,
showInfoIcon = showInfoIconPrevState,
apy = "",
)
}
computeAndApplyShowInfoIcon(cryptoCurrencyStatus)
}
}
}
private fun computeAndApplyShowInfoIcon(cryptoCurrencyStatus: CryptoCurrencyStatus) {
modelScope.launch(dispatchers.default) {
val showInfoIcon = if (cryptoCurrencyStatus.hasNotSuppliedAmount()) {
val minAmount = yieldSupplyMinAmountUseCase(userWallet, cryptoCurrencyStatus).getOrNull()
if (minAmount != null) {
cryptoCurrencyStatus.shouldShowNotSuppliedInfoIcon(minAmount)
} else {
false
}
} else {
false
}
uiState.update { state ->
when (state) {
is YieldSupplyUM.Content -> state.copy(showInfoIcon = showInfoIcon)
else -> state
}
}
}
}
private fun sendInfoAboutProtocolStatus(cryptoCurrencyStatus: CryptoCurrencyStatus) {
if (lastYieldSupplyStatus == cryptoCurrencyStatus.value.yieldSupplyStatus) return
val token = cryptoCurrency as? CryptoCurrency.Token ?: return

View file

@ -1,12 +1,10 @@
package com.tangem.features.yield.supply.impl.subcomponents.active.model
import arrow.core.getOrElse
import com.tangem.common.ui.notifications.NotificationUM
import com.tangem.core.analytics.api.AnalyticsEventHandler
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.components.notifications.NotificationConfig
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.extensions.stringReference
@ -16,7 +14,6 @@ import com.tangem.core.ui.format.bigdecimal.format
import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase
import com.tangem.domain.appcurrency.model.AppCurrency
import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.domain.models.currency.CryptoCurrencyStatus
import com.tangem.domain.yield.supply.usecase.YieldSupplyGetProtocolBalanceUseCase
import com.tangem.domain.yield.supply.usecase.YieldSupplyGetTokenStatusUseCase
import com.tangem.domain.yield.supply.usecase.YieldSupplyMinAmountUseCase
@ -31,13 +28,10 @@ import com.tangem.features.yield.supply.impl.subcomponents.active.model.transfor
import com.tangem.utils.StringsSigns.DASH_SIGN
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import com.tangem.utils.transformer.update
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.persistentListOf
import kotlinx.collections.immutable.toPersistentList
import kotlinx.coroutines.flow.*
import kotlinx.coroutines.launch
import timber.log.Timber
import java.math.BigDecimal
import javax.inject.Inject
@Suppress("LongParameterList")
@ -121,7 +115,6 @@ internal class YieldSupplyActiveModel @Inject constructor(
uiState.update {
it.copy(
notifications = getNotifications(cryptoCurrencyStatus),
availableBalance = stringReference(
protocolBalance.format {
crypto(
@ -156,54 +149,6 @@ internal class YieldSupplyActiveModel @Inject constructor(
}
}
private fun getNotifications(cryptoCurrencyStatus: CryptoCurrencyStatus): ImmutableList<NotificationUM> {
val approvalNotification = if (cryptoCurrencyStatus.value.yieldSupplyStatus?.isAllowedToSpend != true) {
NotificationUM.Error(
title = resourceReference(R.string.yield_module_approve_needed_notification_title),
subtitle = resourceReference(R.string.yield_module_approve_needed_notification_description),
iconResId = R.drawable.ic_alert_triangle_20,
buttonState = NotificationConfig.ButtonsState.PrimaryButtonConfig(
text = resourceReference(R.string.yield_module_approve_needed_notification_cta),
onClick = params.callback::onApprove,
),
)
} else {
null
}
val notSuppliedNotification = getNotSuppliedNotification(cryptoCurrencyStatus)
return listOfNotNull(
approvalNotification,
notSuppliedNotification,
).toPersistentList()
}
private fun getNotSuppliedNotification(cryptoCurrencyStatus: CryptoCurrencyStatus): NotificationUM? {
val value = cryptoCurrencyStatus.value
val isActive = value.yieldSupplyStatus?.isActive == true
val effectiveProtocolBalance = value.yieldSupplyStatus?.effectiveProtocolBalance ?: null
val amount = value.amount
if (!isActive || effectiveProtocolBalance == null || amount == null) return null
val notDepositedAmount = amount.minus(effectiveProtocolBalance)
return if (notDepositedAmount > BigDecimal.ZERO) {
val formattedAmount =
notDepositedAmount.format { crypto(symbol = "", decimals = cryptoCurrencyStatus.currency.decimals) }
analyticsHandler.send(
YieldSupplyAnalytics.NoticeAmountNotDeposited(
token = cryptoCurrency.symbol,
blockchain = cryptoCurrency.network.name,
),
)
NotificationUM.Info.YieldSupplyNotAllAmountSupplied(
formattedAmount = formattedAmount,
symbol = cryptoCurrency.symbol,
)
} else {
null
}
}
private fun loadMinAmount() {
modelScope.launch(dispatchers.default) {
yieldSupplyMinAmountUseCase(
@ -215,6 +160,8 @@ internal class YieldSupplyActiveModel @Inject constructor(
cryptoCurrencyStatus = cryptoCurrencyStatusFlow.value,
appCurrency = appCurrency,
minAmount = minAmount,
analyticsHandler = analyticsHandler,
onApprove = params.callback::onApprove,
),
)
}.onLeft {

View file

@ -1,5 +1,8 @@
package com.tangem.features.yield.supply.impl.subcomponents.active.model.transformers
import com.tangem.common.ui.notifications.NotificationUM
import com.tangem.core.analytics.api.AnalyticsEventHandler
import com.tangem.core.ui.components.notifications.NotificationConfig
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.extensions.stringReference
import com.tangem.core.ui.extensions.wrappedList
@ -8,18 +11,37 @@ import com.tangem.core.ui.format.bigdecimal.fiat
import com.tangem.core.ui.format.bigdecimal.format
import com.tangem.domain.appcurrency.model.AppCurrency
import com.tangem.domain.models.currency.CryptoCurrencyStatus
import com.tangem.domain.models.currency.notSuppliedAmountOrNull
import com.tangem.domain.models.currency.shouldShowNotSuppliedInfoIcon
import com.tangem.features.yield.supply.api.analytics.YieldSupplyAnalytics
import com.tangem.features.yield.supply.impl.R
import com.tangem.features.yield.supply.impl.subcomponents.active.entity.YieldSupplyActiveContentUM
import com.tangem.utils.transformer.Transformer
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.toPersistentList
import java.math.BigDecimal
/**
* Computes and sets minimum amount (fiat) and fee description note
* Transformer that populates minimum supply amount and related hints for the active Yield Supply screen.
*
* - Sets the displayed minimum amount in fiat and crypto.
* - Builds the fee policy note text using the minimum amount.
* - Adds contextual notifications:
* - Approval required notification when spending is not yet allowed (emits analytics on CTA).
* - "Not all amount supplied" info when wallet balance exceeds the supplied balance by more than [minAmount].
*
* @property cryptoCurrencyStatus Current currency status used to calculate values and flags.
* @property appCurrency Preferred fiat currency for formatting.
* @property minAmount Protocol-required minimal amount to deposit/supply (in crypto units).
* @property analyticsHandler Analytics reporter for user actions.
* @property onApprove Action invoked when the "Approve" notification button is tapped.
*/
internal class YieldSupplyActiveMinAmountTransformer(
private val cryptoCurrencyStatus: CryptoCurrencyStatus,
private val appCurrency: AppCurrency,
private val minAmount: BigDecimal,
private val analyticsHandler: AnalyticsEventHandler,
private val onApprove: () -> Unit,
) : Transformer<YieldSupplyActiveContentUM> {
override fun transform(prevState: YieldSupplyActiveContentUM): YieldSupplyActiveContentUM {
@ -41,6 +63,49 @@ internal class YieldSupplyActiveMinAmountTransformer(
return prevState.copy(
minAmount = stringReference(minAmountFiatText),
minFeeDescription = minFeeNoteValue,
notifications = getNotifications(),
)
}
private fun getNotifications(): ImmutableList<NotificationUM> {
val approvalNotification = if (cryptoCurrencyStatus.value.yieldSupplyStatus?.isAllowedToSpend != true) {
NotificationUM.Error(
title = resourceReference(R.string.yield_module_approve_needed_notification_title),
subtitle = resourceReference(R.string.yield_module_approve_needed_notification_description),
iconResId = R.drawable.ic_alert_triangle_20,
buttonState = NotificationConfig.ButtonsState.PrimaryButtonConfig(
text = resourceReference(R.string.yield_module_approve_needed_notification_cta),
onClick = onApprove,
),
)
} else {
null
}
val notSuppliedNotification = getNotSuppliedNotification(cryptoCurrencyStatus)
return listOfNotNull(
approvalNotification,
notSuppliedNotification,
).toPersistentList()
}
private fun getNotSuppliedNotification(cryptoCurrencyStatus: CryptoCurrencyStatus): NotificationUM? {
return if (cryptoCurrencyStatus.shouldShowNotSuppliedInfoIcon(minAmount)) {
val cryptoCurrency = cryptoCurrencyStatus.currency
val notDepositedAmount = cryptoCurrencyStatus.notSuppliedAmountOrNull()
val formattedAmount =
notDepositedAmount.format { crypto(symbol = "", decimals = cryptoCurrencyStatus.currency.decimals) }
analyticsHandler.send(
YieldSupplyAnalytics.NoticeAmountNotDeposited(
token = cryptoCurrency.symbol,
blockchain = cryptoCurrency.network.name,
),
)
NotificationUM.Info.YieldSupplyNotAllAmountSupplied(
formattedAmount = formattedAmount,
symbol = cryptoCurrency.symbol,
)
} else {
null
}
}
}