Updated on 2026-08-14

This commit is contained in:
Tangem 2025-10-13 20:52:31 +05:00
parent a821732662
commit 30431a3dbb
20 changed files with 540 additions and 34 deletions

View file

@ -16,6 +16,7 @@ internal sealed class YieldSupplyFeeUM {
val feeValue: TextReference,
val currentNetworkFeeValue: TextReference,
val maxNetworkFeeValue: TextReference,
val minAmountFeeValue: TextReference,
) : YieldSupplyFeeUM()
}

View file

@ -0,0 +1,30 @@
package com.tangem.features.yield.supply.impl.common.formatter
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.combinedReference
import com.tangem.core.ui.extensions.stringReference
import com.tangem.core.ui.format.bigdecimal.crypto
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.CryptoCurrency
import com.tangem.utils.StringsSigns
import java.math.BigDecimal
internal class YieldSupplyMinAmountFormatter(
private val feeCryptoCurrency: CryptoCurrency,
private val appCurrency: AppCurrency,
) {
operator fun invoke(feeValue: BigDecimal, fiatRate: BigDecimal?): TextReference {
val cryptoFee = feeValue.format { crypto(feeCryptoCurrency) }
val fiatFeeValue = fiatRate?.let(feeValue::multiply)
val fiatFee = fiatFeeValue.format { fiat(appCurrency.code, appCurrency.symbol) }
return combinedReference(
stringReference(cryptoFee),
stringReference(" ${StringsSigns.DOT} "),
stringReference(fiatFee),
)
}
}

View file

@ -141,6 +141,7 @@ private class YieldSupplyActionContentPreviewProvider : PreviewParameterProvider
feeValue = stringReference("0.00020 ETH • \$0.99"),
currentNetworkFeeValue = stringReference("1.45 USDT • \$1.45"),
maxNetworkFeeValue = stringReference("8.50 USDT • \$8.50"),
minAmountFeeValue = stringReference("50 USDT • \$50"),
),
isPrimaryButtonEnabled = false,
isTransactionSending = false,

View file

@ -203,7 +203,7 @@ internal class YieldSupplyModel @Inject constructor(
resourceReference(
R.string.yield_module_token_details_earn_notification_apy,
),
stringReference(tokenStatus.apy.toString() + "%"),
stringReference(" ${tokenStatus.apy}%"),
),
onClick = ::onActiveClick,
isAllowedToSpend = yieldSupplyStatus.isAllowedToSpend,

View file

@ -42,10 +42,14 @@ internal class YieldSupplyPromoModel @Inject constructor(
}
override fun onHowItWorksClick() {
urlOpener.openUrl("https://tangem.com/") // TODO replace with real link
urlOpener.openUrl(HOW_IT_WORKS_URL)
}
override fun onStartEarningClick() {
bottomSheetNavigation.activate(YieldSupplyPromoConfig.Action)
}
companion object {
private const val HOW_IT_WORKS_URL = "https://tangem.com/en/blog/post/savings-account "
}
}

View file

@ -11,4 +11,5 @@ internal data class YieldSupplyActiveContentUM(
val subtitleLink: TextReference,
val notificationUM: NotificationUM?,
val apy: TextReference? = null,
val minAmount: TextReference?,
)

View file

@ -1,5 +1,6 @@
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.decompose.di.ModelScoped
import com.tangem.core.decompose.model.Model
@ -11,12 +12,17 @@ import com.tangem.core.ui.extensions.stringReference
import com.tangem.core.ui.extensions.wrappedList
import com.tangem.core.ui.format.bigdecimal.crypto
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.yield.supply.usecase.YieldSupplyGetProtocolBalanceUseCase
import com.tangem.domain.yield.supply.usecase.YieldSupplyGetTokenStatusUseCase
import com.tangem.domain.yield.supply.usecase.YieldSupplyMinAmountUseCase
import com.tangem.features.yield.supply.impl.R
import com.tangem.features.yield.supply.impl.common.formatter.YieldSupplyMinAmountFormatter
import com.tangem.features.yield.supply.impl.subcomponents.active.YieldSupplyActiveComponent
import com.tangem.features.yield.supply.impl.subcomponents.active.entity.YieldSupplyActiveContentUM
import com.tangem.utils.StringsSigns.DASH_SIGN
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import kotlinx.coroutines.flow.*
import kotlinx.coroutines.launch
@ -29,12 +35,15 @@ internal class YieldSupplyActiveModel @Inject constructor(
override val dispatchers: CoroutineDispatcherProvider,
private val yieldSupplyGetProtocolBalanceUseCase: YieldSupplyGetProtocolBalanceUseCase,
private val yieldSupplyGetTokenStatusUseCase: YieldSupplyGetTokenStatusUseCase,
private val yieldSupplyMinAmountUseCase: YieldSupplyMinAmountUseCase,
private val getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase,
) : Model() {
private val params: YieldSupplyActiveComponent.Params = paramsContainer.require()
private val cryptoCurrencyStatusFlow = params.cryptoCurrencyStatusFlow
private val cryptoCurrency = cryptoCurrencyStatusFlow.value.currency
private var appCurrency = AppCurrency.Default
val uiState: StateFlow<YieldSupplyActiveContentUM>
field = MutableStateFlow(
@ -48,6 +57,7 @@ internal class YieldSupplyActiveModel @Inject constructor(
),
subtitleLink = resourceReference(R.string.common_read_more),
notificationUM = null,
minAmount = null,
),
)
@ -55,6 +65,7 @@ internal class YieldSupplyActiveModel @Inject constructor(
subscribeOnCurrencyUpdates()
modelScope.launch(dispatchers.default) {
appCurrency = getSelectedAppCurrencyUseCase.invokeSync().getOrElse { AppCurrency.Default }
val protocolBalance = yieldSupplyGetProtocolBalanceUseCase(
userWalletId = params.userWallet.walletId,
cryptoCurrency = cryptoCurrency,
@ -93,6 +104,7 @@ internal class YieldSupplyActiveModel @Inject constructor(
}
loadApy()
loadMinAmount()
uiState.update {
it.copy(
@ -126,6 +138,27 @@ internal class YieldSupplyActiveModel @Inject constructor(
}
}
private fun loadMinAmount() {
modelScope.launch(dispatchers.default) {
yieldSupplyMinAmountUseCase(
params.userWallet,
cryptoCurrencyStatusFlow.value,
).onRight { minAmount ->
val minAmountTextReference = YieldSupplyMinAmountFormatter(
cryptoCurrencyStatusFlow.value.currency,
appCurrency,
).invoke(minAmount, cryptoCurrencyStatusFlow.value.value.fiatRate)
uiState.update {
it.copy(minAmount = minAmountTextReference)
}
}.onLeft {
uiState.update {
it.copy(minAmount = TextReference.Str(DASH_SIGN))
}
}
}
}
private companion object {
const val AAVEV3_PREFIX = "a"
}

View file

@ -59,7 +59,6 @@ internal fun YieldSupplyActiveContent(
CurrentApy(state.apy)
chartComponent.Content(Modifier.padding(bottom = 12.dp))
}
YieldSupplyActiveMyFunds(state = state, isBalanceHidden = isBalanceHidden)
AnimatedVisibility(state.notificationUM != null) {
val wrappedNotification = remember(this) { requireNotNull(state.notificationUM) }
@ -69,6 +68,8 @@ internal fun YieldSupplyActiveContent(
containerColor = TangemTheme.colors.background.action,
)
}
YieldSupplyActiveMyFunds(state = state, isBalanceHidden = isBalanceHidden)
}
}
@ -163,6 +164,15 @@ private fun YieldSupplyActiveMyFunds(state: YieldSupplyActiveContentUM, isBalanc
info = state.availableBalance,
isBalanceHidden = isBalanceHidden,
)
HorizontalDivider(
thickness = 0.5.dp,
color = TangemTheme.colors.stroke.primary,
)
InfoRow(
title = resourceReference(R.string.yield_module_fee_policy_sheet_min_amount_title),
info = state.minAmount,
isBalanceHidden = false,
)
}
}
@ -251,6 +261,7 @@ private class YieldSupplyActiveBottomSheetPreviewProvider : PreviewParameterProv
subtitleLink = resourceReference(R.string.common_read_more),
notificationUM = NotificationUM.Error.InvalidAmount,
apy = stringReference("5,14%"),
minAmount = stringReference("50 USDT"),
),
)
}

View file

@ -1,6 +1,8 @@
package com.tangem.features.yield.supply.impl.subcomponents.approve.model
import arrow.core.getOrElse
import com.tangem.blockchain.common.TransactionData
import com.tangem.blockchain.common.transaction.TransactionFee
import com.tangem.core.decompose.di.ModelScoped
import com.tangem.core.decompose.model.Model
import com.tangem.core.decompose.model.ParamsContainer
@ -194,40 +196,47 @@ internal class YieldSupplyApproveModel @Inject constructor(
}
},
ifRight = { fee ->
val feeCryptoValue = fee.normal.amount.value
applyFee(fee, approvalTransitionData)
},
)
}
val feeFiatValue = feeCryptoCurrencyStatus.value.fiatRate?.let { rate ->
feeCryptoValue?.multiply(rate)
}
val cryptoFee = feeCryptoValue.format { crypto(feeCryptoCurrencyStatus.currency) }
val fiatFee = feeFiatValue.format { fiat(appCurrency.code, appCurrency.symbol) }
private suspend fun applyFee(transactionFee: TransactionFee, approvalTransitionData: TransactionData.Uncompiled) {
val feeCryptoValue = transactionFee.normal.amount.value
uiState.update {
if (cryptoCurrencyStatus.value is CryptoCurrencyStatus.Loading) {
it.copy(yieldSupplyFeeUM = YieldSupplyFeeUM.Loading)
} else {
it.copy(
isPrimaryButtonEnabled = true,
yieldSupplyFeeUM = YieldSupplyFeeUM.Content(
transactionDataList = persistentListOf(approvalTransitionData.copy(fee = fee.normal)),
feeValue = combinedReference(
stringReference(cryptoFee),
stringReference(" $DOT "),
stringReference(fiatFee),
),
currentNetworkFeeValue = TextReference.EMPTY,
maxNetworkFeeValue = TextReference.EMPTY,
),
)
}
}
yieldSupplyNotificationsUpdateTrigger.triggerUpdate(
data = YieldSupplyNotificationData(
feeValue = feeCryptoValue,
feeError = null,
val feeFiatValue = feeCryptoCurrencyStatus.value.fiatRate?.let { rate ->
feeCryptoValue?.multiply(rate)
}
val cryptoFee = feeCryptoValue.format { crypto(feeCryptoCurrencyStatus.currency) }
val fiatFee = feeFiatValue.format { fiat(appCurrency.code, appCurrency.symbol) }
uiState.update {
if (cryptoCurrencyStatus.value is CryptoCurrencyStatus.Loading) {
it.copy(yieldSupplyFeeUM = YieldSupplyFeeUM.Loading)
} else {
it.copy(
isPrimaryButtonEnabled = true,
yieldSupplyFeeUM = YieldSupplyFeeUM.Content(
transactionDataList = persistentListOf(
approvalTransitionData.copy(fee = transactionFee.normal),
),
feeValue = combinedReference(
stringReference(cryptoFee),
stringReference(" $DOT "),
stringReference(fiatFee),
),
currentNetworkFeeValue = TextReference.EMPTY,
maxNetworkFeeValue = TextReference.EMPTY,
minAmountFeeValue = TextReference.EMPTY,
),
)
},
}
}
yieldSupplyNotificationsUpdateTrigger.triggerUpdate(
data = YieldSupplyNotificationData(
feeValue = feeCryptoValue,
feeError = null,
),
)
}

View file

@ -29,6 +29,7 @@ import com.tangem.features.yield.supply.impl.common.ui.YieldSupplyFeeRow
import com.tangem.utils.StringsSigns
import kotlinx.collections.immutable.persistentListOf
@Suppress("LongMethod")
@Composable
internal fun YieldSupplyFeePolicyContent(
yieldSupplyFeeUM: YieldSupplyFeeUM,
@ -61,6 +62,28 @@ internal fun YieldSupplyFeePolicyContent(
modifier = Modifier.padding(horizontal = 16.dp),
)
SpacerH24()
FooterContainer(
footer = resourceReference(
id = R.string.yield_module_fee_policy_sheet_min_amount_note,
formatArgs = wrappedList(networkName),
),
paddingValues = PaddingValues(
top = 8.dp,
start = 12.dp,
end = 12.dp,
),
) {
val minAmount = when (yieldSupplyFeeUM) {
is YieldSupplyFeeUM.Content -> yieldSupplyFeeUM.minAmountFeeValue
YieldSupplyFeeUM.Error -> stringReference(StringsSigns.DASH_SIGN)
YieldSupplyFeeUM.Loading -> null
}
YieldSupplyFeeRow(
title = resourceReference(R.string.yield_module_fee_policy_sheet_min_amount_title),
value = minAmount,
)
}
SpacerH16()
FooterContainer(
footer = resourceReference(
id = R.string.yield_module_fee_policy_sheet_current_fee_note,
@ -101,6 +124,18 @@ internal fun YieldSupplyFeePolicyContent(
value = maxFee,
)
}
Text(
text = stringResourceSafe(R.string.yield_module_fee_policy_tangem_service_fee_title),
style = TangemTheme.typography.caption2,
color = TangemTheme.colors.text.tertiary,
modifier = Modifier
.fillMaxWidth()
.padding(
top = 8.dp,
start = 12.dp,
end = 12.dp,
),
)
}
}
@ -116,11 +151,11 @@ private fun YieldSupplyFeePolicyContent_Preview() {
feeValue = stringReference("0.0001 ETH • \$1.45"),
maxNetworkFeeValue = stringReference("8.50 USDT • \$8.50"),
currentNetworkFeeValue = stringReference("1.45 USDT • \$1.45"),
minAmountFeeValue = stringReference("50 USDT • \$50"),
),
tokenSymbol = "USDT",
networkName = "Ethereum",
modifier = Modifier.background(TangemTheme.colors.background.primary),
)
}
}

View file

@ -117,6 +117,7 @@ internal class YieldSupplyStartEarningComponent(
onClick = model::onClick,
enabled = state.isPrimaryButtonEnabled,
iconResId = icon,
showProgress = state.isTransactionSending,
modifier = Modifier
.fillMaxWidth()
.padding(16.dp),

View file

@ -22,6 +22,7 @@ import com.tangem.domain.wallets.usecase.GetUserWalletUseCase
import com.tangem.domain.yield.supply.usecase.YieldSupplyActivateUseCase
import com.tangem.domain.yield.supply.usecase.YieldSupplyEstimateEnterFeeUseCase
import com.tangem.domain.yield.supply.usecase.YieldSupplyGetTokenStatusUseCase
import com.tangem.domain.yield.supply.usecase.YieldSupplyMinAmountUseCase
import com.tangem.domain.yield.supply.usecase.YieldSupplyStartEarningUseCase
import com.tangem.features.yield.supply.impl.R
import com.tangem.features.yield.supply.impl.common.YieldSupplyAlertFactory
@ -61,11 +62,13 @@ internal class YieldSupplyStartEarningModel @Inject constructor(
private val yieldSupplyAlertFactory: YieldSupplyAlertFactory,
private val yieldSupplyActivateUseCase: YieldSupplyActivateUseCase,
private val yieldSupplyGetTokenStatusUseCase: YieldSupplyGetTokenStatusUseCase,
private val yieldSupplyMinAmountUseCase: YieldSupplyMinAmountUseCase,
) : Model(), YieldSupplyNotificationsComponent.ModelCallback {
private val params: YieldSupplyStartEarningComponent.Params = paramsContainer.require()
private val cryptoCurrency = params.cryptoCurrency
private var minAmount: BigDecimal by Delegates.notNull()
var userWallet: UserWallet by Delegates.notNull()
val cryptoCurrencyStatusFlow: StateFlow<CryptoCurrencyStatus>
@ -112,6 +115,14 @@ internal class YieldSupplyStartEarningModel @Inject constructor(
}
}
private suspend fun calculateMinAmount(userWallet: UserWallet, cryptoCurrencyStatus: CryptoCurrencyStatus) {
yieldSupplyMinAmountUseCase(userWallet, cryptoCurrencyStatus).onRight {
minAmount = it
}.onLeft {
minAmount = BigDecimal.ZERO
}
}
private suspend fun getMaxFee(): BigDecimal? {
if (uiState.value.maxFee != BigDecimal.ZERO) return uiState.value.maxFee
val yieldTokenStatus = yieldSupplyGetTokenStatusUseCase(cryptoCurrency as CryptoCurrency.Token)
@ -166,6 +177,7 @@ internal class YieldSupplyStartEarningModel @Inject constructor(
updatedTransactionList = updatedTransactionList,
feeValue = feeSum,
maxNetworkFee = maxFee,
minAmount = minAmount,
),
)
yieldSupplyNotificationsUpdateTrigger.triggerUpdate(
@ -276,6 +288,7 @@ internal class YieldSupplyStartEarningModel @Inject constructor(
feeCryptoCurrencyStatusFlow.update { feeCurrencyStatus }
modelScope.launch {
calculateMinAmount(userWallet, currencyStatus)
onLoadFee()
}
}

View file

@ -10,12 +10,14 @@ import com.tangem.domain.appcurrency.model.AppCurrency
import com.tangem.domain.models.currency.CryptoCurrencyStatus
import com.tangem.features.yield.supply.impl.common.entity.YieldSupplyActionUM
import com.tangem.features.yield.supply.impl.common.entity.YieldSupplyFeeUM
import com.tangem.features.yield.supply.impl.common.formatter.YieldSupplyMinAmountFormatter
import com.tangem.utils.StringsSigns.DOT
import com.tangem.utils.transformer.Transformer
import kotlinx.collections.immutable.toPersistentList
import java.math.BigDecimal
import java.math.RoundingMode
@Suppress("LongParameterList")
internal class YieldSupplyStartEarningFeeContentTransformer(
private val cryptoCurrencyStatus: CryptoCurrencyStatus,
private val feeCryptoCurrencyStatus: CryptoCurrencyStatus,
@ -23,6 +25,7 @@ internal class YieldSupplyStartEarningFeeContentTransformer(
private val updatedTransactionList: List<TransactionData.Uncompiled>,
private val feeValue: BigDecimal,
private val maxNetworkFee: BigDecimal,
private val minAmount: BigDecimal,
) : Transformer<YieldSupplyActionUM> {
override fun transform(prevState: YieldSupplyActionUM): YieldSupplyActionUM {
val cryptoCurrency = cryptoCurrencyStatus.currency
@ -46,6 +49,11 @@ internal class YieldSupplyStartEarningFeeContentTransformer(
}
val maxFiatFee = maxFiatFeeValue.format { fiat(appCurrency.code, appCurrency.symbol) }
val minAmountTextReference = YieldSupplyMinAmountFormatter(
cryptoCurrency,
appCurrency,
).invoke(minAmount, cryptoCurrencyStatus.value.fiatRate)
return if (cryptoCurrencyStatus.value is CryptoCurrencyStatus.Loading) {
prevState.copy(yieldSupplyFeeUM = YieldSupplyFeeUM.Loading)
} else {
@ -67,6 +75,7 @@ internal class YieldSupplyStartEarningFeeContentTransformer(
stringReference(" $DOT "),
stringReference(maxFiatFee),
),
minAmountFeeValue = minAmountTextReference,
),
)
}

View file

@ -92,6 +92,7 @@ internal class YieldSupplyStopEarningComponent(
onClick = model::onClick,
iconResId = walletInterationIcon(params.userWallet),
enabled = state.isPrimaryButtonEnabled,
showProgress = state.isTransactionSending,
modifier = Modifier
.fillMaxWidth()
.padding(16.dp),

View file

@ -12,6 +12,7 @@ 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.tokens.FetchCurrencyStatusUseCase
import com.tangem.domain.tokens.GetFeePaidCryptoCurrencyStatusSyncUseCase
import com.tangem.domain.transaction.usecase.GetFeeUseCase
import com.tangem.domain.transaction.usecase.SendTransactionUseCase
@ -51,6 +52,7 @@ internal class YieldSupplyStopEarningModel @Inject constructor(
private val yieldSupplyNotificationsUpdateTrigger: YieldSupplyNotificationsUpdateTrigger,
private val yieldSupplyAlertFactory: YieldSupplyAlertFactory,
private val yieldSupplyDeactivateUseCase: YieldSupplyDeactivateUseCase,
private val fetchCurrencyStatusUseCase: FetchCurrencyStatusUseCase,
) : Model(), YieldSupplyNotificationsComponent.ModelCallback {
private val params: YieldSupplyStopEarningComponent.Params = paramsContainer.require()
@ -136,6 +138,7 @@ internal class YieldSupplyStopEarningModel @Inject constructor(
)
},
ifRight = {
fetchCurrencyStatusUseCase(userWalletId = userWallet.walletId, cryptoCurrency.id)
yieldSupplyDeactivateUseCase(cryptoCurrency)
params.callback.onTransactionSent()
},

View file

@ -44,6 +44,7 @@ internal class YieldSupplyStopEarningFeeContentTransformer(
),
currentNetworkFeeValue = TextReference.EMPTY,
maxNetworkFeeValue = TextReference.EMPTY,
minAmountFeeValue = TextReference.EMPTY,
),
)
}