Updated on 2026-08-14

This commit is contained in:
Tangem 2025-10-15 14:25:11 +03:00
commit 5ee5b4f61b
563 changed files with 13917 additions and 4692 deletions

View file

@ -5,6 +5,7 @@ import com.tangem.blockchain.common.TransactionData
import com.tangem.core.ui.components.currency.icon.CurrencyIconState
import com.tangem.core.ui.extensions.TextReference
import kotlinx.collections.immutable.ImmutableList
import java.math.BigDecimal
@Immutable
internal sealed class YieldSupplyFeeUM {
@ -15,6 +16,7 @@ internal sealed class YieldSupplyFeeUM {
val feeValue: TextReference,
val currentNetworkFeeValue: TextReference,
val maxNetworkFeeValue: TextReference,
val minAmountFeeValue: TextReference,
) : YieldSupplyFeeUM()
}
@ -27,4 +29,5 @@ internal data class YieldSupplyActionUM(
val yieldSupplyFeeUM: YieldSupplyFeeUM,
val isPrimaryButtonEnabled: Boolean,
val isTransactionSending: Boolean,
val maxFee: BigDecimal = BigDecimal.ZERO,
)

View file

@ -0,0 +1,25 @@
package com.tangem.features.yield.supply.impl.common.formatter
import com.tangem.core.ui.extensions.TextReference
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 stringReference(cryptoFee + " ${StringsSigns.DOT} " + 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

@ -38,9 +38,8 @@ internal class DefaultYieldSupplyComponent @AssistedInject constructor(
override fun Content(modifier: Modifier) {
val yieldSupplyUM by model.uiState.collectAsStateWithLifecycle()
val bottomSheet by bottomSheetSlot.subscribeAsState()
val isBalanceHidden by model.isBalanceHiddenFlow.collectAsStateWithLifecycle()
YieldSupplyBlockContent(yieldSupplyUM = yieldSupplyUM, isBalanceHidden = isBalanceHidden, modifier = modifier)
YieldSupplyBlockContent(yieldSupplyUM = yieldSupplyUM, modifier = modifier)
bottomSheet.child?.instance?.BottomSheet()
}

View file

@ -6,7 +6,9 @@ import com.tangem.core.ui.extensions.TextReference
@Immutable
internal sealed class YieldSupplyUM {
data class Initial(
data object Initial : YieldSupplyUM()
data class Available(
val title: TextReference,
val onClick: () -> Unit,
) : YieldSupplyUM()
@ -16,7 +18,8 @@ internal sealed class YieldSupplyUM {
data object Unavailable : YieldSupplyUM()
data class Content(
val rewardsBalance: TextReference,
val title: TextReference,
val subtitle: TextReference,
val rewardsApy: TextReference,
val onClick: () -> Unit,
val isAllowedToSpend: Boolean,

View file

@ -4,24 +4,31 @@ import com.arkivanov.decompose.router.slot.SlotNavigation
import com.arkivanov.decompose.router.slot.activate
import com.tangem.common.routing.AppRoute.YieldSupplyPromo
import com.tangem.common.routing.AppRouter
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.extensions.TextReference
import com.tangem.core.ui.extensions.combinedReference
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.extensions.wrappedList
import com.tangem.core.ui.extensions.stringReference
import com.tangem.domain.balancehiding.GetBalanceHidingSettingsUseCase
import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.domain.models.currency.CryptoCurrencyStatus
import com.tangem.domain.models.network.TxInfo
import com.tangem.domain.models.wallet.UserWallet
import com.tangem.domain.models.yield.supply.YieldSupplyStatus
import com.tangem.domain.tokens.FetchCurrencyStatusUseCase
import com.tangem.domain.tokens.GetSingleCryptoCurrencyStatusUseCase
import com.tangem.domain.wallets.usecase.GetUserWalletUseCase
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.features.yield.supply.api.YieldSupplyComponent
import com.tangem.domain.yield.supply.usecase.YieldSupplyIsAvailableUseCase
import com.tangem.features.yield.supply.impl.R
import com.tangem.features.yield.supply.api.YieldSupplyComponent
import com.tangem.features.yield.supply.api.analytics.YieldSupplyAnalytics
import com.tangem.features.yield.supply.impl.main.entity.YieldSupplyUM
import com.tangem.features.yield.supply.impl.main.model.transformers.YieldSupplyTokenStatusSuccessTransformer
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import com.tangem.utils.coroutines.DelayedWork
import kotlinx.coroutines.CoroutineScope
@ -29,6 +36,7 @@ import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.*
import kotlinx.coroutines.launch
import timber.log.Timber
import com.tangem.utils.transformer.update
import javax.inject.Inject
import kotlin.properties.Delegates
@ -37,6 +45,7 @@ import kotlin.properties.Delegates
internal class YieldSupplyModel @Inject constructor(
paramsContainer: ParamsContainer,
override val dispatchers: CoroutineDispatcherProvider,
private val analyticsEventsHandler: AnalyticsEventHandler,
private val appRouter: AppRouter,
private val getUserWalletUseCase: GetUserWalletUseCase,
private val getSingleCryptoCurrencyStatusUseCase: GetSingleCryptoCurrencyStatusUseCase,
@ -44,12 +53,15 @@ internal class YieldSupplyModel @Inject constructor(
@DelayedWork private val coroutineScope: CoroutineScope,
private val getBalanceHidingSettingsUseCase: GetBalanceHidingSettingsUseCase,
private val yieldSupplyGetTokenStatusUseCase: YieldSupplyGetTokenStatusUseCase,
private val yieldSupplyIsAvailableUseCase: YieldSupplyIsAvailableUseCase,
private val yieldSupplyActivateUseCase: YieldSupplyActivateUseCase,
private val yieldSupplyDeactivateUseCase: YieldSupplyDeactivateUseCase,
) : Model(), YieldSupplyClickIntents {
private val params = paramsContainer.require<YieldSupplyComponent.Params>()
val uiState: StateFlow<YieldSupplyUM>
field = MutableStateFlow<YieldSupplyUM>(YieldSupplyUM.Loading)
field = MutableStateFlow<YieldSupplyUM>(YieldSupplyUM.Initial)
val bottomSheetNavigation: SlotNavigation<Unit> = SlotNavigation()
@ -67,9 +79,20 @@ internal class YieldSupplyModel @Inject constructor(
val isBalanceHiddenFlow: StateFlow<Boolean>
field = MutableStateFlow(false)
private var lastYieldSupplyStatus: YieldSupplyStatus? = null
init {
subscribeOnCurrencyStatusUpdates()
subscribeOnBalanceHidden()
checkIfYieldSupplyIsAvailable()
}
private fun checkIfYieldSupplyIsAvailable() {
modelScope.launch(dispatchers.io) {
val isAvailable = yieldSupplyIsAvailableUseCase(params.userWalletId, params.cryptoCurrency)
if (isAvailable) {
subscribeOnCurrencyStatusUpdates()
subscribeOnBalanceHidden()
}
}
}
private fun subscribeOnCurrencyStatusUpdates() {
@ -86,7 +109,7 @@ internal class YieldSupplyModel @Inject constructor(
maybeCryptoCurrency.fold(
ifRight = { cryptoCurrencyStatus ->
cryptoCurrencyStatusFlow.update { cryptoCurrencyStatus }
onDataLoaded(cryptoCurrencyStatus)
onCryptoCurrencyStatusUpdated(cryptoCurrencyStatus)
},
ifLeft = {
Timber.w(it.toString())
@ -102,23 +125,19 @@ internal class YieldSupplyModel @Inject constructor(
}
}
private fun loadTokenStatus(cryptoCurrency: CryptoCurrency.Token) {
private fun loadTokenStatus() {
val cryptoCurrencyToken = cryptoCurrency as? CryptoCurrency.Token ?: return
modelScope.launch(dispatchers.default) {
yieldSupplyGetTokenStatusUseCase(cryptoCurrency)
yieldSupplyGetTokenStatusUseCase(cryptoCurrencyToken)
.onRight { tokenStatus ->
val newState = if (tokenStatus.isActive) {
YieldSupplyUM.Initial(
title = resourceReference(
id = R.string.yield_module_token_details_earn_notification_title,
formatArgs = wrappedList(tokenStatus.apy),
),
onClick = ::onStartEarningClick,
)
} else {
YieldSupplyUM.Unavailable
}
uiState.update { newState }
uiState.update(
YieldSupplyTokenStatusSuccessTransformer(
tokenStatus = tokenStatus,
onStartEarningClick = ::onStartEarningClick,
),
)
}.onLeft {
Timber.e(it)
uiState.update { YieldSupplyUM.Unavailable }
}
}
@ -148,38 +167,85 @@ internal class YieldSupplyModel @Inject constructor(
.launchIn(modelScope)
}
private fun onDataLoaded(cryptoCurrencyStatus: CryptoCurrencyStatus) {
@Suppress("MaximumLineLength")
private fun onCryptoCurrencyStatusUpdated(cryptoCurrencyStatus: CryptoCurrencyStatus) {
val yieldSupplyStatus = cryptoCurrencyStatus.value.yieldSupplyStatus
val hasActiveTransaction = cryptoCurrencyStatus.value.hasCurrentNetworkTransactions
val yieldTransaction = cryptoCurrencyStatus.value.pendingTransactions.firstOrNull {
it.type is TxInfo.TransactionType.YieldSupply
}?.type as? TxInfo.TransactionType.YieldSupply
sendInfoAboutProtocolStatus(cryptoCurrencyStatus)
val yieldSupplyUM = when {
when {
hasActiveTransaction && yieldTransaction != null -> {
coroutineScope.launch(dispatchers.io) {
delay(PROCESSING_UPDATE_DELAY)
fetchCurrencyStatusUseCase(userWalletId = userWallet.walletId, cryptoCurrency.id)
}
when (yieldTransaction) {
TxInfo.TransactionType.YieldSupply.Enter -> YieldSupplyUM.Processing.Enter
TxInfo.TransactionType.YieldSupply.Exit -> YieldSupplyUM.Processing.Exit
uiState.update {
when (yieldTransaction) {
TxInfo.TransactionType.YieldSupply.Enter -> YieldSupplyUM.Processing.Enter
TxInfo.TransactionType.YieldSupply.Exit -> YieldSupplyUM.Processing.Exit
}
}
}
yieldSupplyStatus?.isActive == true ->
YieldSupplyUM.Content(
rewardsBalance = TextReference.EMPTY,
rewardsApy = TextReference.EMPTY,
onClick = ::onActiveClick,
isAllowedToSpend = yieldSupplyStatus.isAllowedToSpend,
)
else -> YieldSupplyUM.Loading
yieldSupplyStatus?.isActive == true -> {
val cryptoCurrencyToken = cryptoCurrency as? CryptoCurrency.Token ?: return
if (!yieldSupplyStatus.isAllowedToSpend) {
analyticsEventsHandler.send(
YieldSupplyAnalytics.NoticeApproveNeeded(
token = cryptoCurrency.symbol,
blockchain = cryptoCurrency.network.name,
),
)
}
modelScope.launch(dispatchers.default) {
yieldSupplyGetTokenStatusUseCase(cryptoCurrencyToken)
.onRight { tokenStatus ->
uiState.update {
YieldSupplyUM.Content(
title = resourceReference(
R.string.yield_module_token_details_earn_notification_earning_on_your_balance_title,
),
subtitle = resourceReference(
R.string.yield_module_token_details_earn_notification_earning_on_your_balance_subtitle,
),
rewardsApy = combinedReference(
resourceReference(
R.string.yield_module_token_details_earn_notification_apy,
),
stringReference(" ${tokenStatus.apy}%"),
),
onClick = ::onActiveClick,
isAllowedToSpend = yieldSupplyStatus.isAllowedToSpend,
)
}
}.onLeft {
Timber.e(it)
uiState.update { YieldSupplyUM.Loading }
}
}
}
else -> {
loadTokenStatus()
}
}
}
uiState.update { yieldSupplyUM }
if (yieldSupplyUM is YieldSupplyUM.Loading) {
(cryptoCurrency as? CryptoCurrency.Token)?.let(::loadTokenStatus)
private fun sendInfoAboutProtocolStatus(cryptoCurrencyStatus: CryptoCurrencyStatus) {
if (lastYieldSupplyStatus == cryptoCurrencyStatus.value.yieldSupplyStatus) return
val token = cryptoCurrency as? CryptoCurrency.Token ?: return
modelScope.launch(dispatchers.default) {
if (cryptoCurrencyStatus.value.yieldSupplyStatus?.isActive == true) {
yieldSupplyActivateUseCase(token).onRight {
lastYieldSupplyStatus = cryptoCurrencyStatus.value.yieldSupplyStatus
}
} else {
yieldSupplyDeactivateUseCase(token).onRight {
lastYieldSupplyStatus = cryptoCurrencyStatus.value.yieldSupplyStatus
}
}
}
}

View file

@ -0,0 +1,26 @@
package com.tangem.features.yield.supply.impl.main.model.transformers
import com.tangem.features.yield.supply.impl.R
import com.tangem.features.yield.supply.impl.main.entity.YieldSupplyUM
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.extensions.wrappedList
import com.tangem.domain.yield.supply.models.YieldMarketToken
import com.tangem.utils.transformer.Transformer
internal class YieldSupplyTokenStatusSuccessTransformer(
private val tokenStatus: YieldMarketToken,
private val onStartEarningClick: () -> Unit,
) : Transformer<YieldSupplyUM> {
override fun transform(prevState: YieldSupplyUM): YieldSupplyUM {
if (!tokenStatus.isActive) return YieldSupplyUM.Unavailable
return YieldSupplyUM.Available(
title = resourceReference(
id = R.string.yield_module_token_details_earn_notification_title,
formatArgs = wrappedList(tokenStatus.apy),
),
onClick = onStartEarningClick,
)
}
}

View file

@ -3,6 +3,7 @@ package com.tangem.features.yield.supply.impl.main.ui
import android.content.res.Configuration
import androidx.compose.animation.AnimatedContent
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.*
@ -17,12 +18,14 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.vectorResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
import androidx.compose.ui.unit.dp
import com.tangem.core.ui.components.SecondaryButton
import com.tangem.core.ui.components.SpacerW12
import com.tangem.core.ui.components.SpacerW8
import com.tangem.core.ui.components.TextShimmer
import com.tangem.core.ui.components.buttons.common.TangemButtonSize
@ -34,19 +37,15 @@ import com.tangem.features.yield.supply.impl.main.entity.YieldSupplyUM
import com.tangem.utils.StringsSigns
@Composable
internal fun YieldSupplyBlockContent(
yieldSupplyUM: YieldSupplyUM,
isBalanceHidden: Boolean,
modifier: Modifier = Modifier,
) {
internal fun YieldSupplyBlockContent(yieldSupplyUM: YieldSupplyUM, modifier: Modifier = Modifier) {
AnimatedContent(
targetState = yieldSupplyUM,
modifier = modifier,
) { supplyUM ->
when (supplyUM) {
is YieldSupplyUM.Initial -> SupplyInitial(supplyUM)
is YieldSupplyUM.Available -> SupplyAvailable(supplyUM)
YieldSupplyUM.Loading -> SupplyLoading()
is YieldSupplyUM.Content -> SupplyContent(supplyUM, isBalanceHidden)
is YieldSupplyUM.Content -> SupplyContent(supplyUM)
YieldSupplyUM.Processing.Enter -> SupplyProcessing(
resourceReference(R.string.yield_module_token_details_earn_notification_processing),
)
@ -54,12 +53,13 @@ internal fun YieldSupplyBlockContent(
resourceReference(R.string.yield_module_stop_earning),
)
YieldSupplyUM.Unavailable -> SupplyUnavailable()
YieldSupplyUM.Initial -> {}
}
}
}
@Composable
private fun SupplyInitial(supplyUM: YieldSupplyUM.Initial) {
private fun SupplyAvailable(supplyUM: YieldSupplyUM.Available) {
SupplyInfo(
title = supplyUM.title,
subtitle = resourceReference(R.string.yield_module_token_details_earn_notification_description),
@ -86,7 +86,7 @@ private fun SupplyUnavailable() {
}
@Composable
private fun SupplyContent(supplyUM: YieldSupplyUM.Content, isBalanceHidden: Boolean) {
private fun SupplyContent(supplyUM: YieldSupplyUM.Content) {
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier
@ -95,22 +95,21 @@ private fun SupplyContent(supplyUM: YieldSupplyUM.Content, isBalanceHidden: Bool
.clickable(onClick = supplyUM.onClick)
.padding(12.dp),
) {
Image(
painter = painterResource(R.drawable.img_aave_22),
modifier = Modifier.size(36.dp),
contentDescription = null,
)
SpacerW12()
Column(
verticalArrangement = Arrangement.spacedBy(4.dp),
modifier = Modifier.weight(1f),
) {
Text(
text = stringResourceSafe(
R.string.yield_module_token_details_earn_notification_earning_on_your_balance_title,
),
style = TangemTheme.typography.subtitle2,
color = TangemTheme.colors.text.tertiary,
)
Row(
horizontalArrangement = Arrangement.spacedBy(4.dp),
) {
Text(
text = supplyUM.rewardsBalance.orMaskWithStars(isBalanceHidden).resolveReference(),
text = supplyUM.title.resolveReference(),
style = TangemTheme.typography.subtitle1,
color = TangemTheme.colors.text.primary1,
)
@ -122,9 +121,15 @@ private fun SupplyContent(supplyUM: YieldSupplyUM.Content, isBalanceHidden: Bool
Text(
text = supplyUM.rewardsApy.resolveReference(),
style = TangemTheme.typography.subtitle1,
color = TangemTheme.colors.text.tertiary,
maxLines = 1,
color = TangemTheme.colors.text.accent,
)
}
Text(
text = supplyUM.subtitle.resolveReference(),
style = TangemTheme.typography.caption2,
color = TangemTheme.colors.text.primary1,
)
}
SpacerW8()
AnimatedVisibility(supplyUM.isAllowedToSpend.not()) {
@ -145,36 +150,42 @@ private fun SupplyContent(supplyUM: YieldSupplyUM.Content, isBalanceHidden: Bool
@Composable
private fun SupplyProcessing(text: TextReference) {
Column(
verticalArrangement = Arrangement.spacedBy(4.dp),
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier
.fillMaxWidth()
.clip(RoundedCornerShape(16.dp))
.background(TangemTheme.colors.background.primary)
.padding(12.dp),
) {
Text(
text = stringResourceSafe(
R.string.yield_module_token_details_earn_notification_earning_on_your_balance_title,
),
style = TangemTheme.typography.subtitle2,
color = TangemTheme.colors.text.tertiary,
Image(
painter = painterResource(R.drawable.img_aave_22),
modifier = Modifier.size(36.dp),
contentDescription = null,
)
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(4.dp),
SpacerW12()
Column(
verticalArrangement = Arrangement.spacedBy(4.dp),
modifier = Modifier.weight(1f),
) {
Text(
text = text.resolveReference(),
style = TangemTheme.typography.body1,
text = stringResourceSafe(
R.string.yield_module_token_details_earn_notification_earning_on_your_balance_title,
),
style = TangemTheme.typography.subtitle2,
color = TangemTheme.colors.text.tertiary,
)
CircularProgressIndicator(
modifier = Modifier.size(16.dp),
color = TangemTheme.colors.icon.accent,
strokeWidth = 2.dp,
Text(
text = text.resolveReference(),
style = TangemTheme.typography.caption2,
color = TangemTheme.colors.text.primary1,
)
}
SpacerW8()
CircularProgressIndicator(
modifier = Modifier.size(20.dp),
color = TangemTheme.colors.icon.accent,
strokeWidth = 2.dp,
)
}
}
@ -285,14 +296,14 @@ private fun SupplyInfo(
@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES)
private fun YieldSupplyBlockContent_Preview(@PreviewParameter(PreviewProvider::class) params: YieldSupplyUM) {
TangemThemePreview {
YieldSupplyBlockContent(params, true)
YieldSupplyBlockContent(yieldSupplyUM = params, modifier = Modifier)
}
}
private class PreviewProvider : PreviewParameterProvider<YieldSupplyUM> {
override val values: Sequence<YieldSupplyUM>
get() = sequenceOf(
YieldSupplyUM.Initial(
YieldSupplyUM.Available(
title = TextReference.Res(
R.string.yield_module_token_details_earn_notification_title,
wrappedList("5.1"),
@ -300,13 +311,15 @@ private class PreviewProvider : PreviewParameterProvider<YieldSupplyUM> {
onClick = {},
),
YieldSupplyUM.Content(
rewardsBalance = stringReference("1 USDT"),
title = stringReference("Aave lending is active"),
subtitle = stringReference("Interest accrues automatically"),
rewardsApy = stringReference("5.1 % APY"),
onClick = {},
isAllowedToSpend = false,
),
YieldSupplyUM.Content(
rewardsBalance = stringReference("1 USDT"),
title = stringReference("Aave lending is active"),
subtitle = stringReference("Interest accrues automatically"),
rewardsApy = stringReference("5.1 % APY"),
onClick = {},
isAllowedToSpend = true,

View file

@ -3,30 +3,48 @@ package com.tangem.features.yield.supply.impl.promo.model
import com.arkivanov.decompose.router.slot.SlotNavigation
import com.arkivanov.decompose.router.slot.activate
import com.tangem.common.routing.AppRouter
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.navigation.url.UrlOpener
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.extensions.wrappedList
import com.tangem.features.yield.supply.api.YieldSupplyPromoComponent
import com.tangem.features.yield.supply.impl.R
import com.tangem.features.yield.supply.api.analytics.YieldSupplyAnalytics
import com.tangem.features.yield.supply.impl.promo.YieldSupplyPromoConfig
import com.tangem.features.yield.supply.impl.promo.entity.YieldSupplyPromoUM
import com.tangem.utils.TangemBlogUrlBuilder.YIELD_SUPPLY_HOW_IT_WORKS_URL
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import javax.inject.Inject
@ModelScoped
internal class YieldSupplyPromoModel @Inject constructor(
override val dispatchers: CoroutineDispatcherProvider,
paramsContainer: ParamsContainer,
private val analytics: AnalyticsEventHandler,
private val urlOpener: UrlOpener,
private val appRouter: AppRouter,
) : Model(), YieldSupplyPromoClickIntents {
val params: YieldSupplyPromoComponent.Params = paramsContainer.require()
val uiState: YieldSupplyPromoUM = YieldSupplyPromoUM(
tosLink = "https://tangem.com/terms-of-service/", // TODO replace with real link
policyLink = "https://tangem.com/privacy-policy/", // TODO replace with real link
title = resourceReference(R.string.yield_module_promo_screen_title, wrappedList("5.3")),
)
init {
analytics.send(
YieldSupplyAnalytics.EarningScreenInfoOpened(
token = params.currency.symbol,
blockchain = params.currency.network.name,
),
)
}
val bottomSheetNavigation: SlotNavigation<YieldSupplyPromoConfig> = SlotNavigation()
override fun onBackClick() {
@ -34,6 +52,7 @@ internal class YieldSupplyPromoModel @Inject constructor(
}
override fun onApyInfoClick() {
analytics.send(YieldSupplyAnalytics.ApyChartViewed)
bottomSheetNavigation.activate(YieldSupplyPromoConfig.Apy)
}
@ -42,7 +61,7 @@ internal class YieldSupplyPromoModel @Inject constructor(
}
override fun onHowItWorksClick() {
urlOpener.openUrl("https://tangem.com/") // TODO replace with real link
urlOpener.openUrl(YIELD_SUPPLY_HOW_IT_WORKS_URL)
}
override fun onStartEarningClick() {

View file

@ -8,16 +8,19 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.tangem.core.decompose.context.AppComponentContext
import com.tangem.core.decompose.context.child
import com.tangem.core.decompose.model.getOrCreateModel
import com.tangem.core.ui.components.SecondaryButton
import com.tangem.core.ui.decompose.ComposableModularContentComponent
import com.tangem.core.ui.extensions.stringResourceSafe
import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.domain.models.currency.CryptoCurrencyStatus
import com.tangem.domain.models.wallet.UserWallet
import com.tangem.features.yield.supply.impl.subcomponents.active.model.YieldSupplyActiveModel
import com.tangem.features.yield.supply.impl.subcomponents.active.ui.YieldSupplyActiveContent
import com.tangem.features.yield.supply.impl.subcomponents.active.ui.YieldSupplyActiveTitle
import com.tangem.features.yield.supply.impl.R
import com.tangem.features.yield.supply.impl.chart.DefaultYieldSupplyChartComponent
import kotlinx.coroutines.flow.StateFlow
internal class YieldSupplyActiveComponent(
@ -26,6 +29,12 @@ internal class YieldSupplyActiveComponent(
) : ComposableModularContentComponent, AppComponentContext by appComponentContext {
private val model: YieldSupplyActiveModel = getOrCreateModel(params = params)
private val chartComponent = DefaultYieldSupplyChartComponent(
appComponentContext = child("chartComponent"),
params = DefaultYieldSupplyChartComponent.Params(
cryptoCurrency = params.cryptoCurrencyStatusFlow.value.currency as CryptoCurrency.Token,
),
)
@Composable
override fun Title() {
@ -37,7 +46,12 @@ internal class YieldSupplyActiveComponent(
val state by model.uiState.collectAsStateWithLifecycle()
val isBalanceHidden by params.isBalanceHiddenFlow.collectAsStateWithLifecycle()
YieldSupplyActiveContent(state = state, isBalanceHidden = isBalanceHidden, modifier = Modifier)
YieldSupplyActiveContent(
state = state,
isBalanceHidden = isBalanceHidden,
chartComponent = chartComponent,
modifier = Modifier,
)
}
@Composable

View file

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

View file

@ -1,35 +1,53 @@
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
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.api.analytics.YieldSupplyAnalytics
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
import timber.log.Timber
import javax.inject.Inject
@Suppress("LongParameterList")
@ModelScoped
internal class YieldSupplyActiveModel @Inject constructor(
paramsContainer: ParamsContainer,
override val dispatchers: CoroutineDispatcherProvider,
analyticsHandler: AnalyticsEventHandler,
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(
@ -43,13 +61,21 @@ internal class YieldSupplyActiveModel @Inject constructor(
),
subtitleLink = resourceReference(R.string.common_read_more),
notificationUM = null,
minAmount = null,
),
)
init {
analyticsHandler.send(
YieldSupplyAnalytics.EarningScreenInfoOpened(
token = cryptoCurrency.symbol,
blockchain = cryptoCurrency.network.name,
),
)
subscribeOnCurrencyUpdates()
modelScope.launch(dispatchers.default) {
appCurrency = getSelectedAppCurrencyUseCase.invokeSync().getOrElse { AppCurrency.Default }
val protocolBalance = yieldSupplyGetProtocolBalanceUseCase(
userWalletId = params.userWallet.walletId,
cryptoCurrency = cryptoCurrency,
@ -87,6 +113,9 @@ internal class YieldSupplyActiveModel @Inject constructor(
null
}
loadApy()
loadMinAmount()
uiState.update {
it.copy(
notificationUM = approvalNotification,
@ -104,6 +133,42 @@ internal class YieldSupplyActiveModel @Inject constructor(
.launchIn(modelScope)
}
private fun loadApy() {
val cryptoCurrencyToken = cryptoCurrency as? CryptoCurrency.Token ?: return
modelScope.launch(dispatchers.default) {
yieldSupplyGetTokenStatusUseCase(cryptoCurrencyToken).onRight { tokenStatus ->
uiState.update {
it.copy(
apy = TextReference.Str("${tokenStatus.apy}%"),
)
}
}.onLeft {
Timber.e("Error loading token status")
}
}
}
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

@ -8,6 +8,7 @@ import androidx.compose.foundation.background
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.Icon
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
@ -15,6 +16,7 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.vectorResource
import androidx.compose.ui.text.LinkAnnotation
import androidx.compose.ui.text.withLink
@ -25,6 +27,7 @@ import androidx.compose.ui.unit.dp
import com.tangem.common.ui.notifications.NotificationUM
import com.tangem.core.ui.components.*
import com.tangem.core.ui.components.notifications.Notification
import com.tangem.core.ui.decompose.ComposableContentComponent
import com.tangem.core.ui.extensions.*
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreview
@ -35,6 +38,7 @@ import com.tangem.features.yield.supply.impl.subcomponents.active.entity.YieldSu
internal fun YieldSupplyActiveContent(
state: YieldSupplyActiveContentUM,
isBalanceHidden: Boolean,
chartComponent: ComposableContentComponent,
modifier: Modifier = Modifier,
) {
Column(
@ -52,18 +56,9 @@ internal fun YieldSupplyActiveContent(
.fillMaxWidth()
.padding(12.dp),
) {
Text(
text = stringResourceSafe(R.string.yield_module_earn_sheet_total_earnings_title),
style = TangemTheme.typography.subtitle2,
color = TangemTheme.colors.text.tertiary,
)
ResizableText(
text = state.totalEarnings.orMaskWithStars(isBalanceHidden).resolveReference(),
style = TangemTheme.typography.h2,
color = TangemTheme.colors.text.primary1,
)
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) }
@ -73,6 +68,48 @@ internal fun YieldSupplyActiveContent(
containerColor = TangemTheme.colors.background.action,
)
}
YieldSupplyActiveMyFunds(state = state, isBalanceHidden = isBalanceHidden)
}
}
@Composable
private fun CurrentApy(apy: TextReference?, modifier: Modifier = Modifier) {
Column(modifier = modifier.padding(vertical = 12.dp)) {
Text(
modifier = Modifier,
text = stringResourceSafe(R.string.yield_module_earn_sheet_current_apy_title),
style = TangemTheme.typography.subtitle2,
color = TangemTheme.colors.text.tertiary,
)
AnimatedContent(
modifier = Modifier.height(32.dp),
targetState = apy?.resolveReference(),
label = "CurrentApy",
) { apyText ->
if (apyText == null) {
TextShimmer(
modifier = modifier.width(94.dp),
text = "",
style = TangemTheme.typography.head,
)
} else {
Row(verticalAlignment = Alignment.CenterVertically) {
Icon(
painterResource(R.drawable.ic_arrow_up_8),
tint = TangemTheme.colors.text.accent,
contentDescription = null,
modifier = Modifier.padding(end = 6.dp).size(12.dp),
)
Text(
modifier = modifier,
text = apyText,
style = TangemTheme.typography.body1,
color = TangemTheme.colors.text.accent,
)
}
}
}
}
}
@ -127,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,
)
}
}
@ -193,7 +239,11 @@ private fun YieldSupplyActiveBottomSheet_Preview(
@PreviewParameter(YieldSupplyActiveBottomSheetPreviewProvider::class) params: YieldSupplyActiveContentUM,
) {
TangemThemePreview {
YieldSupplyActiveContent(params, true)
YieldSupplyActiveContent(
state = params,
isBalanceHidden = true,
chartComponent = ComposableContentComponent.EMPTY,
)
}
}
@ -210,6 +260,8 @@ 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,9 @@
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.analytics.api.AnalyticsEventHandler
import com.tangem.core.decompose.di.ModelScoped
import com.tangem.core.decompose.model.Model
import com.tangem.core.decompose.model.ParamsContainer
@ -19,6 +22,7 @@ import com.tangem.domain.transaction.usecase.CreateApprovalTransactionUseCase
import com.tangem.domain.transaction.usecase.GetFeeUseCase
import com.tangem.domain.transaction.usecase.SendTransactionUseCase
import com.tangem.domain.yield.supply.usecase.YieldSupplyGetContractAddressUseCase
import com.tangem.features.yield.supply.api.analytics.YieldSupplyAnalytics
import com.tangem.features.yield.supply.impl.R
import com.tangem.features.yield.supply.impl.common.YieldSupplyAlertFactory
import com.tangem.features.yield.supply.impl.common.entity.YieldSupplyActionUM
@ -44,6 +48,7 @@ import javax.inject.Inject
internal class YieldSupplyApproveModel @Inject constructor(
override val dispatchers: CoroutineDispatcherProvider,
paramsContainer: ParamsContainer,
private val analyticsEventHandler: AnalyticsEventHandler,
private val urlOpener: UrlOpener,
private val yieldSupplyNotificationsUpdateTrigger: YieldSupplyNotificationsUpdateTrigger,
private val createApprovalTransactionUseCase: CreateApprovalTransactionUseCase,
@ -138,6 +143,11 @@ internal class YieldSupplyApproveModel @Inject constructor(
)
},
ifRight = {
analyticsEventHandler.send(YieldSupplyAnalytics.ApprovalAction(
token = cryptoCurrency.symbol,
blockchain = cryptoCurrency.network.name,
action = YieldSupplyAnalytics.Action.Approve,
))
params.callback.onTransactionSent()
},
)
@ -194,40 +204,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

@ -5,11 +5,13 @@ import com.tangem.common.routing.AppRouter
import com.tangem.common.ui.notifications.NotificationUM
import com.tangem.common.ui.notifications.NotificationsFactory.addExceedsBalanceNotification
import com.tangem.common.ui.notifications.NotificationsFactory.addFeeUnreachableNotification
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.domain.models.currency.CryptoCurrency
import com.tangem.domain.tokens.GetBalanceNotEnoughForFeeWarningUseCase
import com.tangem.features.yield.supply.api.analytics.YieldSupplyAnalytics
import com.tangem.features.yield.supply.impl.subcomponents.notifications.YieldSupplyNotificationsComponent
import com.tangem.features.yield.supply.impl.subcomponents.notifications.YieldSupplyNotificationsUpdateListener
import com.tangem.lib.crypto.BlockchainUtils
@ -25,6 +27,7 @@ import javax.inject.Inject
internal class YieldSupplyNotificationsModel @Inject constructor(
paramsContainer: ParamsContainer,
override val dispatchers: CoroutineDispatcherProvider,
private val analyticsEventHandler: AnalyticsEventHandler,
private val appRouter: AppRouter,
private val getBalanceNotEnoughForFeeWarningUseCase: GetBalanceNotEnoughForFeeWarningUseCase,
private val yieldSupplyNotificationsUpdateListener: YieldSupplyNotificationsUpdateListener,
@ -73,6 +76,15 @@ internal class YieldSupplyNotificationsModel @Inject constructor(
)
}
if (notifications.any { it is NotificationUM.Error.TokenExceedsBalance }) {
analyticsEventHandler.send(
YieldSupplyAnalytics.NoticeNotEnoughFee(
token = cryptoCurrencyStatus.currency.symbol,
blockchain = cryptoCurrencyStatus.currency.network.name,
),
)
}
uiState.update { notifications.toPersistentList() }
yieldSupplyNotificationsUpdateListener.callbackHasError(notifications.any())

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

@ -1,5 +1,6 @@
package com.tangem.features.yield.supply.impl.subcomponents.startearning.model
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
@ -8,6 +9,7 @@ import com.tangem.core.ui.components.currency.icon.converter.CryptoCurrencyToIco
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.extensions.wrappedList
import com.tangem.features.yield.supply.impl.R
import com.tangem.features.yield.supply.api.analytics.YieldSupplyAnalytics
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.subcomponents.feepolicy.YieldSupplyFeePolicyComponent
@ -24,6 +26,7 @@ import javax.inject.Inject
internal class YieldSupplyStartEarningEntryModel @Inject constructor(
override val dispatchers: CoroutineDispatcherProvider,
private val router: Router,
private val analyticsEventHandler: AnalyticsEventHandler,
paramsContainer: ParamsContainer,
) : Model(), YieldSupplyStartEarningComponent.ModelCallback, YieldSupplyFeePolicyComponent.ModelCallback {
@ -55,6 +58,9 @@ internal class YieldSupplyStartEarningEntryModel @Inject constructor(
}
override fun onFeePolicyClick() {
analyticsEventHandler.send(
YieldSupplyAnalytics.ButtonFeePolicy,
)
router.push(YieldSupplyStartEarningRoute.FeePolicy)
}

View file

@ -2,24 +2,32 @@ package com.tangem.features.yield.supply.impl.subcomponents.startearning.model
import arrow.core.getOrElse
import com.tangem.blockchain.common.TransactionSender
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.currency.icon.converter.CryptoCurrencyToIconStateConverter
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.extensions.wrappedList
import com.tangem.core.ui.utils.parseToBigDecimal
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.models.wallet.UserWallet
import com.tangem.domain.tokens.FetchCurrencyStatusUseCase
import com.tangem.domain.tokens.GetFeePaidCryptoCurrencyStatusSyncUseCase
import com.tangem.domain.tokens.GetSingleCryptoCurrencyStatusUseCase
import com.tangem.domain.transaction.error.GetFeeError
import com.tangem.domain.transaction.usecase.SendTransactionUseCase
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.api.analytics.YieldSupplyAnalytics
import com.tangem.features.yield.supply.impl.common.YieldSupplyAlertFactory
import com.tangem.features.yield.supply.impl.common.entity.YieldSupplyActionUM
import com.tangem.features.yield.supply.impl.common.entity.YieldSupplyFeeUM
@ -33,6 +41,7 @@ import com.tangem.features.yield.supply.impl.subcomponents.startearning.model.tr
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import com.tangem.utils.extensions.orZero
import com.tangem.utils.transformer.update
import kotlinx.coroutines.NonCancellable
import kotlinx.coroutines.flow.*
import kotlinx.coroutines.launch
import timber.log.Timber
@ -45,6 +54,7 @@ import kotlin.properties.Delegates
internal class YieldSupplyStartEarningModel @Inject constructor(
override val dispatchers: CoroutineDispatcherProvider,
paramsContainer: ParamsContainer,
private val analytics: AnalyticsEventHandler,
private val getUserWalletUseCase: GetUserWalletUseCase,
private val getSingleCryptoCurrencyStatusUseCase: GetSingleCryptoCurrencyStatusUseCase,
private val getFeePaidCryptoCurrencyStatusSyncUseCase: GetFeePaidCryptoCurrencyStatusSyncUseCase,
@ -55,11 +65,15 @@ internal class YieldSupplyStartEarningModel @Inject constructor(
private val yieldSupplyNotificationsUpdateTrigger: YieldSupplyNotificationsUpdateTrigger,
private val fetchCurrencyStatusUseCase: FetchCurrencyStatusUseCase,
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>
@ -99,6 +113,12 @@ internal class YieldSupplyStartEarningModel @Inject constructor(
private var appCurrency = AppCurrency.Default
init {
analytics.send(
YieldSupplyAnalytics.StartEarningScreen(
token = params.cryptoCurrency.symbol,
blockchain = params.cryptoCurrency.network.name,
),
)
modelScope.launch {
appCurrency = getSelectedAppCurrencyUseCase.invokeSync().getOrElse { AppCurrency.Default }
subscribeOnCurrencyStatusUpdates()
@ -106,17 +126,51 @@ 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)
.getOrNull()
return yieldTokenStatus?.maxFeeNative?.parseToBigDecimal(cryptoCurrency.decimals)
}
private suspend fun onLoadFee() {
if (cryptoCurrencyStatus.value is CryptoCurrencyStatus.Loading || uiState.value.isTransactionSending) return
uiState.update {
it.copy(yieldSupplyFeeUM = YieldSupplyFeeUM.Loading)
}
val maxFee = if (uiState.value.maxFee == BigDecimal.ZERO) {
getMaxFee()
} else {
uiState.value.maxFee
} ?: return
val transactionListData = yieldSupplyStartEarningUseCase(
userWalletId = userWallet.walletId,
cryptoCurrencyStatus = cryptoCurrencyStatus,
maxNetworkFee = MAX_NETWORK_FEE,
).getOrNull() ?: return
maxNetworkFee = maxFee,
).getOrNull()
uiState.update {
it.copy(yieldSupplyFeeUM = YieldSupplyFeeUM.Loading)
if (transactionListData == null) {
uiState.update {
it.copy(yieldSupplyFeeUM = YieldSupplyFeeUM.Error)
}
yieldSupplyNotificationsUpdateTrigger.triggerUpdate(
data = YieldSupplyNotificationData(
feeValue = null,
feeError = GetFeeError.UnknownError,
),
)
return
}
yieldSupplyEstimateEnterFeeUseCase.invoke(
@ -146,7 +200,8 @@ internal class YieldSupplyStartEarningModel @Inject constructor(
appCurrency = appCurrency,
updatedTransactionList = updatedTransactionList,
feeValue = feeSum,
maxNetworkFee = MAX_NETWORK_FEE,
maxNetworkFee = maxFee,
minAmount = minAmount,
),
)
yieldSupplyNotificationsUpdateTrigger.triggerUpdate(
@ -161,6 +216,12 @@ internal class YieldSupplyStartEarningModel @Inject constructor(
fun onClick() {
val yieldSupplyFeeUM = uiState.value.yieldSupplyFeeUM as? YieldSupplyFeeUM.Content ?: return
analytics.send(
YieldSupplyAnalytics.ButtonStartEarning(
token = params.cryptoCurrency.symbol,
blockchain = params.cryptoCurrency.network.name,
),
)
uiState.update(YieldSupplyTransactionInProgressTransformer)
modelScope.launch(dispatchers.default) {
@ -173,6 +234,10 @@ internal class YieldSupplyStartEarningModel @Inject constructor(
ifLeft = { error ->
Timber.e(error.toString())
uiState.update(YieldSupplyTransactionReadyTransformer)
analytics.send(YieldSupplyAnalytics.EarnErrors(
action = YieldSupplyAnalytics.Action.Approve,
errorDescription = error.getAnalyticsDescription(),
))
yieldSupplyAlertFactory.getSendTransactionErrorState(
error = error,
popBack = params.callback::onBackClick,
@ -188,7 +253,14 @@ internal class YieldSupplyStartEarningModel @Inject constructor(
)
},
ifRight = {
fetchCurrencyStatusUseCase(userWalletId = userWallet.walletId, cryptoCurrency.id)
yieldSupplyActivateUseCase(cryptoCurrency)
modelScope.launch(NonCancellable) {
fetchCurrencyStatusUseCase(
userWalletId = userWallet.walletId,
cryptoCurrency.id,
)
}
analytics.send(YieldSupplyAnalytics.FundsEarned)
modelScope.launch {
params.callback.onTransactionSent()
}
@ -256,6 +328,7 @@ internal class YieldSupplyStartEarningModel @Inject constructor(
feeCryptoCurrencyStatusFlow.update { feeCurrencyStatus }
modelScope.launch {
calculateMinAmount(userWallet, currencyStatus)
onLoadFee()
}
}
@ -274,8 +347,4 @@ internal class YieldSupplyStartEarningModel @Inject constructor(
popBack = params.callback::onBackClick,
)
}
private companion object {
val MAX_NETWORK_FEE: BigDecimal = BigDecimal.TEN // TODO replace with value from api
}
}

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

@ -1,6 +1,7 @@
package com.tangem.features.yield.supply.impl.subcomponents.stopearning.model
import arrow.core.getOrElse
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
@ -12,11 +13,14 @@ 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
import com.tangem.domain.yield.supply.usecase.YieldSupplyDeactivateUseCase
import com.tangem.domain.yield.supply.usecase.YieldSupplyStopEarningUseCase
import com.tangem.features.yield.supply.impl.R
import com.tangem.features.yield.supply.api.analytics.YieldSupplyAnalytics
import com.tangem.features.yield.supply.impl.common.YieldSupplyAlertFactory
import com.tangem.features.yield.supply.impl.common.entity.YieldSupplyActionUM
import com.tangem.features.yield.supply.impl.common.entity.YieldSupplyFeeUM
@ -31,6 +35,7 @@ import com.tangem.utils.TangemBlogUrlBuilder
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import com.tangem.utils.extensions.orZero
import com.tangem.utils.transformer.update
import kotlinx.coroutines.NonCancellable
import kotlinx.coroutines.flow.*
import kotlinx.coroutines.launch
import timber.log.Timber
@ -41,6 +46,7 @@ import javax.inject.Inject
internal class YieldSupplyStopEarningModel @Inject constructor(
override val dispatchers: CoroutineDispatcherProvider,
paramsContainer: ParamsContainer,
private val analytics: AnalyticsEventHandler,
private val getFeeUseCase: GetFeeUseCase,
private val getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase,
private val getFeePaidCryptoCurrencyStatusSyncUseCase: GetFeePaidCryptoCurrencyStatusSyncUseCase,
@ -49,6 +55,8 @@ internal class YieldSupplyStopEarningModel @Inject constructor(
private val urlOpener: UrlOpener,
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()
@ -108,6 +116,12 @@ internal class YieldSupplyStopEarningModel @Inject constructor(
fun onClick() {
val yieldSupplyFeeUM = uiState.value.yieldSupplyFeeUM as? YieldSupplyFeeUM.Content ?: return
analytics.send(
YieldSupplyAnalytics.ButtonStopEarning(
token = cryptoCurrency.symbol,
blockchain = cryptoCurrency.network.name,
),
)
uiState.update(YieldSupplyTransactionInProgressTransformer)
modelScope.launch(dispatchers.default) {
@ -134,6 +148,16 @@ internal class YieldSupplyStopEarningModel @Inject constructor(
)
},
ifRight = {
analytics.send(
YieldSupplyAnalytics.FundsWithdrawn(
token = cryptoCurrency.symbol,
blockchain = cryptoCurrency.network.name,
),
)
yieldSupplyDeactivateUseCase(cryptoCurrency)
modelScope.launch(NonCancellable) {
fetchCurrencyStatusUseCase(userWalletId = userWallet.walletId, cryptoCurrency.id)
}
params.callback.onTransactionSent()
},
)

View file

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