Updated on 2026-08-14

This commit is contained in:
Tangem 2025-11-07 10:48:16 +03:00
commit 714eb51ce5
34 changed files with 345 additions and 114 deletions

View file

@ -296,6 +296,7 @@ internal class ChildFactory @Inject constructor(
params = TokenDetailsComponent.Params( params = TokenDetailsComponent.Params(
userWalletId = route.userWalletId, userWalletId = route.userWalletId,
currency = route.currency, currency = route.currency,
navigationAction = route.navigationAction,
), ),
componentFactory = tokenDetailsComponentFactory, componentFactory = tokenDetailsComponentFactory,
) )

View file

@ -20,6 +20,7 @@ import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.domain.nft.models.NFTAsset import com.tangem.domain.nft.models.NFTAsset
import com.tangem.domain.onramp.model.OnrampSource import com.tangem.domain.onramp.model.OnrampSource
import com.tangem.domain.pay.TangemPayDetailsConfig import com.tangem.domain.pay.TangemPayDetailsConfig
import com.tangem.domain.tokens.model.details.NavigationAction
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable
@SuppressLint("UnsafeOptInUsageError") @SuppressLint("UnsafeOptInUsageError")
@ -55,6 +56,7 @@ sealed class AppRoute(val path: String) : Route {
data class CurrencyDetails( data class CurrencyDetails(
val userWalletId: UserWalletId, val userWalletId: UserWalletId,
val currency: CryptoCurrency, val currency: CryptoCurrency,
val navigationAction: NavigationAction? = null,
) : AppRoute(path = "/currency_details/${userWalletId.stringValue}/${currency.id.value}") ) : AppRoute(path = "/currency_details/${userWalletId.stringValue}/${currency.id.value}")
@Serializable @Serializable

View file

@ -48,8 +48,11 @@ class TokenItemStateConverter(
private val iconStateProvider: (CryptoCurrencyStatus) -> CurrencyIconState = { private val iconStateProvider: (CryptoCurrencyStatus) -> CurrencyIconState = {
CryptoCurrencyToIconStateConverter().convert(it) CryptoCurrencyToIconStateConverter().convert(it)
}, },
private val onApyLabelClick: ((CryptoCurrencyStatus) -> Unit)? = null,
private val titleStateProvider: (CryptoCurrencyStatus) -> TokenItemState.TitleState = { private val titleStateProvider: (CryptoCurrencyStatus) -> TokenItemState.TitleState = {
createTitleState(it, yieldModuleApyMap, stakingApyMap) createTitleState(it, yieldModuleApyMap, stakingApyMap, {
onApyLabelClick?.invoke(it)
})
}, },
private val subtitleStateProvider: (CryptoCurrencyStatus) -> TokenItemState.SubtitleState? = { private val subtitleStateProvider: (CryptoCurrencyStatus) -> TokenItemState.SubtitleState? = {
createSubtitleState(it, appCurrency) createSubtitleState(it, appCurrency)
@ -102,6 +105,9 @@ class TokenItemStateConverter(
onItemLongClick = onItemLongClick?.let { onItemLongClick -> onItemLongClick = onItemLongClick?.let { onItemLongClick ->
{ onItemLongClick(it, this) } { onItemLongClick(it, this) }
}, },
onApyLabelClick = onApyLabelClick?.let { onApyLabelClick ->
{ onApyLabelClick(this) }
},
) )
} }
@ -160,6 +166,7 @@ class TokenItemStateConverter(
currencyStatus: CryptoCurrencyStatus, currencyStatus: CryptoCurrencyStatus,
yieldModuleApyMap: Map<String, String>, yieldModuleApyMap: Map<String, String>,
stakingApyMap: Map<String, List<Yield.Validator>>, stakingApyMap: Map<String, List<Yield.Validator>>,
onApyLabelClick: () -> Unit,
): TokenItemState.TitleState { ): TokenItemState.TitleState {
return when (val value = currencyStatus.value) { return when (val value = currencyStatus.value) {
is CryptoCurrencyStatus.Loading, is CryptoCurrencyStatus.Loading,
@ -184,6 +191,7 @@ class TokenItemStateConverter(
hasPending = value.hasCurrentNetworkTransactions, hasPending = value.hasCurrentNetworkTransactions,
earnApy = earnApyText, earnApy = earnApyText,
earnApyIsActive = isActive, earnApyIsActive = isActive,
onApyLabelClick = onApyLabelClick,
) )
} }
} }

View file

@ -2,7 +2,10 @@ package com.tangem.core.analytics.models.event
import com.tangem.core.analytics.models.AnalyticsEvent import com.tangem.core.analytics.models.AnalyticsEvent
import com.tangem.core.analytics.models.AnalyticsParam import com.tangem.core.analytics.models.AnalyticsParam
import com.tangem.core.analytics.models.AnalyticsParam.Key.ACTION
import com.tangem.core.analytics.models.AnalyticsParam.Key.BLOCKCHAIN
import com.tangem.core.analytics.models.AnalyticsParam.Key.ERROR_CODE import com.tangem.core.analytics.models.AnalyticsParam.Key.ERROR_CODE
import com.tangem.core.analytics.models.AnalyticsParam.Key.STATE
import com.tangem.core.analytics.models.AnalyticsParam.Key.TOKEN_PARAM import com.tangem.core.analytics.models.AnalyticsParam.Key.TOKEN_PARAM
/** /**
@ -108,6 +111,21 @@ sealed class MainScreenAnalyticsEvent(
event = "Hot Token Error", event = "Hot Token Error",
params = mapOf(ERROR_CODE to errorCode), params = mapOf(ERROR_CODE to errorCode),
) )
data class ApyClicked(
val token: String,
val blockchain: String,
val action: String,
val state: String,
) : MainScreenAnalyticsEvent(
event = "APY Clicked",
params = mapOf(
TOKEN_PARAM to token,
BLOCKCHAIN to blockchain,
ACTION to action,
STATE to state,
),
)
// endregion // endregion
companion object { companion object {

View file

@ -8,6 +8,7 @@ import com.tangem.datasource.api.tangemTech.models.YieldSupplyMarketTokenDto
import com.tangem.datasource.api.tangemTech.models.YieldTokenChartResponse import com.tangem.datasource.api.tangemTech.models.YieldTokenChartResponse
import retrofit2.http.Body import retrofit2.http.Body
import retrofit2.http.GET import retrofit2.http.GET
import retrofit2.http.Header
import retrofit2.http.POST import retrofit2.http.POST
import retrofit2.http.Path import retrofit2.http.Path
import retrofit2.http.Query import retrofit2.http.Query
@ -34,6 +35,7 @@ interface YieldSupplyApi {
@POST("api/v1/module/activate") @POST("api/v1/module/activate")
suspend fun activateYieldModule( suspend fun activateYieldModule(
@Body body: YieldSupplyChangeTokenStatusBody, @Body body: YieldSupplyChangeTokenStatusBody,
@Header("userWalletId") userWalletId: String,
): ApiResponse<YieldModuleStatusResponse> ): ApiResponse<YieldModuleStatusResponse>
@POST("api/v1/module/deactivate") @POST("api/v1/module/deactivate")

View file

@ -3,6 +3,7 @@ package com.tangem.core.ui.components.token.internal
import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.AnimatedVisibility
import androidx.compose.foundation.Image import androidx.compose.foundation.Image
import androidx.compose.foundation.background import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.* import androidx.compose.foundation.layout.*
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
@ -60,7 +61,11 @@ private fun ContentTitle(state: TokenTitleState.Content, modifier: Modifier = Mo
YieldSupplyApyLabel( YieldSupplyApyLabel(
apy = state.earnApy, apy = state.earnApy,
isActive = state.earnApyIsActive, isActive = state.earnApyIsActive,
modifier = Modifier.align(alignment = Alignment.CenterVertically), modifier = Modifier
.align(alignment = Alignment.CenterVertically)
.clickable {
state.onApyLabelClick?.invoke()
},
) )
} }
} }

View file

@ -40,6 +40,9 @@ sealed class TokenItemState {
/** Callback which will be called when an item is long clicked */ /** Callback which will be called when an item is long clicked */
abstract val onItemLongClick: ((TokenItemState) -> Unit)? abstract val onItemLongClick: ((TokenItemState) -> Unit)?
/** Callback which will be called when an apy label is clicked */
abstract val onApyLabelClick: ((TokenItemState) -> Unit)?
/** /**
* Loading token state * Loading token state
* *
@ -58,6 +61,7 @@ sealed class TokenItemState {
override val subtitle2State: Subtitle2State = Subtitle2State.Loading override val subtitle2State: Subtitle2State = Subtitle2State.Loading
override val onItemClick: ((TokenItemState) -> Unit)? = null override val onItemClick: ((TokenItemState) -> Unit)? = null
override val onItemLongClick: ((TokenItemState) -> Unit)? = null override val onItemLongClick: ((TokenItemState) -> Unit)? = null
override val onApyLabelClick: ((TokenItemState) -> Unit)? = null
} }
/** /**
@ -73,6 +77,7 @@ sealed class TokenItemState {
override val subtitle2State: Subtitle2State = Subtitle2State.Locked override val subtitle2State: Subtitle2State = Subtitle2State.Locked
override val onItemClick: ((TokenItemState) -> Unit)? = null override val onItemClick: ((TokenItemState) -> Unit)? = null
override val onItemLongClick: ((TokenItemState) -> Unit)? = null override val onItemLongClick: ((TokenItemState) -> Unit)? = null
override val onApyLabelClick: ((TokenItemState) -> Unit)? = null
} }
/** /**
@ -96,6 +101,7 @@ sealed class TokenItemState {
override val subtitle2State: Subtitle2State?, override val subtitle2State: Subtitle2State?,
override val onItemClick: ((TokenItemState) -> Unit)?, override val onItemClick: ((TokenItemState) -> Unit)?,
override val onItemLongClick: ((TokenItemState) -> Unit)?, override val onItemLongClick: ((TokenItemState) -> Unit)?,
override val onApyLabelClick: ((TokenItemState) -> Unit)? = null,
) : TokenItemState() ) : TokenItemState()
/** /**
@ -116,6 +122,7 @@ sealed class TokenItemState {
override val fiatAmountState: FiatAmountState? = null override val fiatAmountState: FiatAmountState? = null
override val onItemClick: ((TokenItemState) -> Unit)? = null override val onItemClick: ((TokenItemState) -> Unit)? = null
override val onItemLongClick: ((TokenItemState) -> Unit)? = null override val onItemLongClick: ((TokenItemState) -> Unit)? = null
override val onApyLabelClick: ((TokenItemState) -> Unit)? = null
} }
/** /**
@ -133,6 +140,7 @@ sealed class TokenItemState {
override val iconState: CurrencyIconState, override val iconState: CurrencyIconState,
override val titleState: TitleState, override val titleState: TitleState,
override val subtitleState: SubtitleState? = null, override val subtitleState: SubtitleState? = null,
override val onApyLabelClick: ((TokenItemState) -> Unit)? = null,
override val onItemClick: ((TokenItemState) -> Unit)?, override val onItemClick: ((TokenItemState) -> Unit)?,
override val onItemLongClick: ((TokenItemState) -> Unit)?, override val onItemLongClick: ((TokenItemState) -> Unit)?,
) : TokenItemState() { ) : TokenItemState() {
@ -159,6 +167,7 @@ sealed class TokenItemState {
override val fiatAmountState: FiatAmountState? = null override val fiatAmountState: FiatAmountState? = null
override val subtitle2State: Subtitle2State? = null override val subtitle2State: Subtitle2State? = null
override val onItemClick: ((TokenItemState) -> Unit)? = null override val onItemClick: ((TokenItemState) -> Unit)? = null
override val onApyLabelClick: ((TokenItemState) -> Unit)? = null
} }
@Immutable @Immutable
@ -170,6 +179,7 @@ sealed class TokenItemState {
val isAvailable: Boolean = true, val isAvailable: Boolean = true,
val earnApy: TextReference? = null, val earnApy: TextReference? = null,
val earnApyIsActive: Boolean = false, val earnApyIsActive: Boolean = false,
val onApyLabelClick: (() -> Unit)? = null,
) : TitleState() ) : TitleState()
data object Loading : TitleState() data object Loading : TitleState()

View file

@ -76,18 +76,22 @@ internal class DefaultYieldSupplyRepository(
(walletManager as? YieldSupplyProvider)?.isSupported() ?: false (walletManager as? YieldSupplyProvider)?.isSupported() ?: false
} }
override suspend fun activateProtocol(cryptoCurrencyToken: CryptoCurrency.Token, address: String): Boolean = override suspend fun activateProtocol(
withContext(dispatchers.io) { userWalletId: UserWalletId,
val chainId = Blockchain.fromNetworkId(cryptoCurrencyToken.network.backendId)?.getChainId() cryptoCurrencyToken: CryptoCurrency.Token,
?: error("Chain id is required for evm's") address: String,
yieldSupplyApi.activateYieldModule( ): Boolean = withContext(dispatchers.io) {
YieldSupplyChangeTokenStatusBody( val chainId = Blockchain.fromNetworkId(cryptoCurrencyToken.network.backendId)?.getChainId()
tokenAddress = cryptoCurrencyToken.contractAddress, ?: error("Chain id is required for evm's")
chainId = chainId, yieldSupplyApi.activateYieldModule(
userAddress = address, body = YieldSupplyChangeTokenStatusBody(
), tokenAddress = cryptoCurrencyToken.contractAddress,
).getOrThrow().isActive chainId = chainId,
} userAddress = address,
),
userWalletId = userWalletId.stringValue,
).getOrThrow().isActive
}
override suspend fun deactivateProtocol(cryptoCurrencyToken: CryptoCurrency.Token, address: String): Boolean = override suspend fun deactivateProtocol(cryptoCurrencyToken: CryptoCurrency.Token, address: String): Boolean =
withContext(dispatchers.io) { withContext(dispatchers.io) {
@ -103,14 +107,18 @@ internal class DefaultYieldSupplyRepository(
} }
override suspend fun saveTokenProtocolStatus( override suspend fun saveTokenProtocolStatus(
userWalletId: UserWalletId,
cryptoCurrency: CryptoCurrency, cryptoCurrency: CryptoCurrency,
yieldSupplyEnterStatus: YieldSupplyEnterStatus, yieldSupplyEnterStatus: YieldSupplyEnterStatus,
) { ) {
statusMap[cryptoCurrency.id.value] = yieldSupplyEnterStatus statusMap["${userWalletId}_${cryptoCurrency.id.value}"] = yieldSupplyEnterStatus
} }
override fun getTokenProtocolStatus(cryptoCurrency: CryptoCurrency): YieldSupplyEnterStatus? { override fun getTokenProtocolStatus(
return statusMap[cryptoCurrency.id.value] userWalletId: UserWalletId,
cryptoCurrency: CryptoCurrency,
): YieldSupplyEnterStatus? {
return statusMap["${userWalletId}_${cryptoCurrency.id.value}"]
} }
private fun List<YieldMarketToken>.enrichNetworkIds(): List<YieldMarketToken> { private fun List<YieldMarketToken>.enrichNetworkIds(): List<YieldMarketToken> {

View file

@ -106,6 +106,10 @@ internal class FeedbackDataBuilder {
fun addBlockchainError(info: BlockchainInfo, error: BlockchainErrorInfo) { fun addBlockchainError(info: BlockchainInfo, error: BlockchainErrorInfo) {
builder.appendKeyValue("Blockchain", info.blockchain) builder.appendKeyValue("Blockchain", info.blockchain)
builder.appendAddresses(
key = "Explorer link${info.explorerLinks.isMultiple(suffix = "s")}",
addresses = info.explorerLinks,
)
builder.appendKeyValue("Derivation path", info.derivationPath) builder.appendKeyValue("Derivation path", info.derivationPath)
builder.appendKeyValue("Host", info.host) builder.appendKeyValue("Host", info.host)
builder.appendKeyValue("Token", error.tokenSymbol) builder.appendKeyValue("Token", error.tokenSymbol)

View file

@ -0,0 +1,9 @@
package com.tangem.domain.tokens.model.details
import kotlinx.serialization.Serializable
@Serializable
sealed class NavigationAction {
data object Staking : NavigationAction()
data class YieldSupply(val isActive: Boolean) : NavigationAction()
}

View file

@ -0,0 +1,9 @@
package com.tangem.domain.yield.supply.models
import java.math.BigDecimal
data class YieldSupplyMaxFee(
val nativeMaxFee: BigDecimal,
val tokenMaxFee: BigDecimal,
val fiatMaxFee: BigDecimal,
)

View file

@ -46,7 +46,11 @@ interface YieldSupplyRepository {
* May throw on network/backend errors or if required chain id cannot be resolved. * May throw on network/backend errors or if required chain id cannot be resolved.
*/ */
@Throws @Throws
suspend fun activateProtocol(cryptoCurrencyToken: CryptoCurrency.Token, address: String): Boolean suspend fun activateProtocol(
userWalletId: UserWalletId,
cryptoCurrencyToken: CryptoCurrency.Token,
address: String,
): Boolean
/** /**
* Deactivate yield protocol for the specified token. * Deactivate yield protocol for the specified token.
@ -59,25 +63,32 @@ interface YieldSupplyRepository {
suspend fun deactivateProtocol(cryptoCurrencyToken: CryptoCurrency.Token, address: String): Boolean suspend fun deactivateProtocol(cryptoCurrencyToken: CryptoCurrency.Token, address: String): Boolean
/** /**
* Save the last user-initiated yield protocol action for the given currency. * Save the last userinitiated yield protocol action for the given wallet and currency.
* *
* The saved value helps the UI render an intermediate "processing" state while waiting * The saved value helps the UI render an intermediate "processing" state while waiting
* for the definitive protocol status to be fetched from the network. This information * for the definitive protocol status to be fetched from the network. This information
* is transient and not intended to be persisted across app restarts. * is transient (inmemory only) and is not persisted across app restarts.
* *
* @param userWalletId the wallet the action was performed with
* @param cryptoCurrency the currency or token the action relates to * @param cryptoCurrency the currency or token the action relates to
* @param yieldSupplyEnterStatus the last action intent: [YieldSupplyEnterStatus.Enter] or [YieldSupplyEnterStatus.Exit] * @param yieldSupplyEnterStatus the last action intent: [YieldSupplyEnterStatus.Enter] or [YieldSupplyEnterStatus.Exit]
*/ */
suspend fun saveTokenProtocolStatus(cryptoCurrency: CryptoCurrency, yieldSupplyEnterStatus: YieldSupplyEnterStatus) suspend fun saveTokenProtocolStatus(
userWalletId: UserWalletId,
cryptoCurrency: CryptoCurrency,
yieldSupplyEnterStatus: YieldSupplyEnterStatus,
)
/** /**
* Get the last saved user-initiated yield protocol action for the given currency, if any. * Get the last saved userinitiated yield protocol action for the given wallet and currency, if any.
* *
* Used to determine whether the UI should display an intermediate "processing" state * Used to determine whether the UI should display an intermediate "processing" state
* until the protocol status retrieved from backend reflects the change. * until the protocol status retrieved from backend reflects the change. The value is
* transient (inmemory only) and is not persisted across app restarts.
* *
* @param userWalletId the wallet to query
* @param cryptoCurrency the currency or token to query * @param cryptoCurrency the currency or token to query
* @return the last action intent or null if nothing has been recorded * @return the last action intent or null if nothing has been recorded
*/ */
fun getTokenProtocolStatus(cryptoCurrency: CryptoCurrency): YieldSupplyEnterStatus? fun getTokenProtocolStatus(userWalletId: UserWalletId, cryptoCurrency: CryptoCurrency): YieldSupplyEnterStatus?
} }

View file

@ -2,15 +2,23 @@ package com.tangem.domain.yield.supply.usecase
import arrow.core.Either import arrow.core.Either
import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.domain.yield.supply.YieldSupplyRepository import com.tangem.domain.yield.supply.YieldSupplyRepository
class YieldSupplyActivateUseCase( class YieldSupplyActivateUseCase(
private val yieldSupplyRepository: YieldSupplyRepository, private val yieldSupplyRepository: YieldSupplyRepository,
) { ) {
suspend operator fun invoke(cryptoCurrency: CryptoCurrency, address: String): Either<Throwable, Boolean> = suspend operator fun invoke(
Either.catch { userWalletId: UserWalletId,
val token = cryptoCurrency as? CryptoCurrency.Token ?: error("Token expected") cryptoCurrency: CryptoCurrency,
yieldSupplyRepository.activateProtocol(token, address) address: String,
} ): Either<Throwable, Boolean> = Either.catch {
val token = cryptoCurrency as? CryptoCurrency.Token ?: error("Token expected")
yieldSupplyRepository.activateProtocol(
userWalletId = userWalletId,
cryptoCurrencyToken = token,
address = address,
)
}
} }

View file

@ -10,16 +10,17 @@ import com.tangem.domain.models.wallet.UserWallet
import com.tangem.domain.quotes.QuotesRepository import com.tangem.domain.quotes.QuotesRepository
import com.tangem.domain.tokens.repository.CurrenciesRepository import com.tangem.domain.tokens.repository.CurrenciesRepository
import com.tangem.domain.yield.supply.YieldSupplyRepository import com.tangem.domain.yield.supply.YieldSupplyRepository
import com.tangem.domain.yield.supply.models.YieldSupplyMaxFee
import java.math.BigDecimal import java.math.BigDecimal
import java.math.RoundingMode import java.math.RoundingMode
/** /**
* Calculates the max allowed network fee for a Yield Supply transaction. * Calculates the max allowed network fee for a Yield Supply transaction.
* *
* Result is a Pair<BigDecimal, BigDecimal> where: * Returns [Either] of [YieldSupplyMaxFee] with:
* - first: fee in native coin units (maxFeeNative) * - nativeMaxFee: fee in native coin units
* - second: the same fee converted to token units using the fiat-rate ratio * - tokenMaxFee: fee converted to token units using the native/token rate ratio
* (nativeFiatRate / tokenFiatRate). * - tokenFiatMaxFee: fee value in fiat
* *
* Uses YieldMarketToken.maxFeeNative and the same conversion logic as * Uses YieldMarketToken.maxFeeNative and the same conversion logic as
* [YieldSupplyGetCurrentFeeUseCase]. * [YieldSupplyGetCurrentFeeUseCase].
@ -33,7 +34,7 @@ class YieldSupplyGetMaxFeeUseCase(
suspend operator fun invoke( suspend operator fun invoke(
userWallet: UserWallet, userWallet: UserWallet,
cryptoCurrencyStatus: CryptoCurrencyStatus, cryptoCurrencyStatus: CryptoCurrencyStatus,
): Either<Throwable, Pair<BigDecimal, BigDecimal>> = catch { ): Either<Throwable, YieldSupplyMaxFee> = catch {
val token = cryptoCurrencyStatus.currency as? CryptoCurrency.Token val token = cryptoCurrencyStatus.currency as? CryptoCurrency.Token
?: error("CryptoCurrency must be token for max fee calculation") ?: error("CryptoCurrency must be token for max fee calculation")
@ -60,6 +61,7 @@ class YieldSupplyGetMaxFeeUseCase(
.firstOrNull { it.yieldSupplyKey == token.yieldSupplyKey() } .firstOrNull { it.yieldSupplyKey == token.yieldSupplyKey() }
val marketToken = cachedMarketToken ?: yieldSupplyRepository.getTokenStatus(token) val marketToken = cachedMarketToken ?: yieldSupplyRepository.getTokenStatus(token)
val maxFeeNative = marketToken.maxFeeNative val maxFeeNative = marketToken.maxFeeNative
val maxFeeToken = maxFeeNative.multiply(nativeFiatRate)
val rateRatio = nativeFiatRate.divide( val rateRatio = nativeFiatRate.divide(
fiatRate, fiatRate,
@ -69,6 +71,10 @@ class YieldSupplyGetMaxFeeUseCase(
val tokenValue = rateRatio.multiply(maxFeeNative) val tokenValue = rateRatio.multiply(maxFeeNative)
maxFeeNative to tokenValue.stripTrailingZeros() YieldSupplyMaxFee(
nativeMaxFee = maxFeeNative,
tokenMaxFee = maxFeeToken,
fiatMaxFee = tokenValue.stripTrailingZeros(),
)
} }
} }

View file

@ -4,12 +4,14 @@ import com.tangem.core.decompose.factory.ComponentFactory
import com.tangem.core.ui.decompose.ComposableContentComponent import com.tangem.core.ui.decompose.ComposableContentComponent
import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.domain.models.wallet.UserWalletId import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.domain.tokens.model.details.NavigationAction
interface TokenDetailsComponent : ComposableContentComponent { interface TokenDetailsComponent : ComposableContentComponent {
data class Params( data class Params(
val userWalletId: UserWalletId, val userWalletId: UserWalletId,
val currency: CryptoCurrency, val currency: CryptoCurrency,
val navigationAction: NavigationAction? = null,
) )
interface Factory : ComponentFactory<Params, TokenDetailsComponent> interface Factory : ComponentFactory<Params, TokenDetailsComponent>

View file

@ -16,6 +16,7 @@ import com.tangem.core.decompose.model.getOrCreateModel
import com.tangem.core.ui.components.NavigationBar3ButtonsScrim import com.tangem.core.ui.components.NavigationBar3ButtonsScrim
import com.tangem.core.ui.decompose.ComposableBottomSheetComponent import com.tangem.core.ui.decompose.ComposableBottomSheetComponent
import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.domain.tokens.model.details.NavigationAction
import com.tangem.feature.tokendetails.presentation.tokendetails.model.TokenDetailsModel import com.tangem.feature.tokendetails.presentation.tokendetails.model.TokenDetailsModel
import com.tangem.feature.tokendetails.presentation.tokendetails.route.TokenDetailsBottomSheetConfig import com.tangem.feature.tokendetails.presentation.tokendetails.route.TokenDetailsBottomSheetConfig
import com.tangem.feature.tokendetails.presentation.tokendetails.ui.TokenDetailsScreen import com.tangem.feature.tokendetails.presentation.tokendetails.ui.TokenDetailsScreen
@ -76,6 +77,8 @@ internal class DefaultTokenDetailsComponent @AssistedInject constructor(
params = YieldSupplyComponent.Params( params = YieldSupplyComponent.Params(
userWalletId = params.userWalletId, userWalletId = params.userWalletId,
cryptoCurrency = params.currency, cryptoCurrency = params.currency,
handleNavigation = (params.navigationAction as? NavigationAction.YieldSupply)
?.isActive,
), ),
) )

View file

@ -63,6 +63,7 @@ import com.tangem.domain.tokens.model.TokenActionsState
import com.tangem.domain.tokens.model.analytics.* import com.tangem.domain.tokens.model.analytics.*
import com.tangem.domain.tokens.model.analytics.TokenScreenAnalyticsEvent.Companion.toReasonAnalyticsText import com.tangem.domain.tokens.model.analytics.TokenScreenAnalyticsEvent.Companion.toReasonAnalyticsText
import com.tangem.domain.tokens.model.analytics.TokenScreenAnalyticsEvent.DetailsScreenOpened.TokenBalance import com.tangem.domain.tokens.model.analytics.TokenScreenAnalyticsEvent.DetailsScreenOpened.TokenBalance
import com.tangem.domain.tokens.model.details.NavigationAction
import com.tangem.domain.tokens.model.details.TokenAction import com.tangem.domain.tokens.model.details.TokenAction
import com.tangem.domain.transaction.error.AssociateAssetError import com.tangem.domain.transaction.error.AssociateAssetError
import com.tangem.domain.transaction.error.IncompleteTransactionError import com.tangem.domain.transaction.error.IncompleteTransactionError
@ -222,6 +223,7 @@ internal class TokenDetailsModel @Inject constructor(
updateContent() updateContent()
handleBalanceHiding() handleBalanceHiding()
checkForActionUpdates() checkForActionUpdates()
handleNavigationParam()
} }
fun onResume() { fun onResume() {
@ -1246,6 +1248,12 @@ internal class TokenDetailsModel @Inject constructor(
} }
} }
private fun handleNavigationParam() {
if (params.navigationAction is NavigationAction.Staking) {
openStaking()
}
}
private companion object { private companion object {
const val EXPRESS_STATUS_UPDATE_DELAY = 10_000L const val EXPRESS_STATUS_UPDATE_DELAY = 10_000L
} }

View file

@ -3,9 +3,12 @@ package com.tangem.feature.wallet.child.wallet.model.intents
import arrow.core.getOrElse import arrow.core.getOrElse
import com.tangem.common.ui.expressStatus.ExpressStatusBottomSheetConfig import com.tangem.common.ui.expressStatus.ExpressStatusBottomSheetConfig
import com.tangem.core.analytics.api.AnalyticsEventHandler import com.tangem.core.analytics.api.AnalyticsEventHandler
import com.tangem.core.analytics.models.event.MainScreenAnalyticsEvent
import com.tangem.core.decompose.di.ModelScoped import com.tangem.core.decompose.di.ModelScoped
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.CryptoCurrencyStatus import com.tangem.domain.models.currency.CryptoCurrencyStatus
import com.tangem.domain.models.staking.YieldBalance
import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.models.wallet.UserWallet
import com.tangem.domain.models.wallet.UserWalletId import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.domain.models.wallet.isLocked import com.tangem.domain.models.wallet.isLocked
@ -14,6 +17,7 @@ import com.tangem.domain.settings.ShouldShowMarketsTooltipUseCase
import com.tangem.domain.tokens.GetCryptoCurrencyActionsUseCase import com.tangem.domain.tokens.GetCryptoCurrencyActionsUseCase
import com.tangem.domain.tokens.GetSingleCryptoCurrencyStatusUseCase import com.tangem.domain.tokens.GetSingleCryptoCurrencyStatusUseCase
import com.tangem.domain.tokens.model.TokenActionsState import com.tangem.domain.tokens.model.TokenActionsState
import com.tangem.domain.tokens.model.details.NavigationAction
import com.tangem.domain.txhistory.usecase.GetExplorerTransactionUrlUseCase import com.tangem.domain.txhistory.usecase.GetExplorerTransactionUrlUseCase
import com.tangem.domain.wallets.usecase.GetUserWalletUseCase import com.tangem.domain.wallets.usecase.GetUserWalletUseCase
import com.tangem.feature.wallet.presentation.account.AccountDependencies import com.tangem.feature.wallet.presentation.account.AccountDependencies
@ -45,6 +49,8 @@ internal interface WalletContentClickIntents {
fun onTokenItemLongClick(userWalletId: UserWalletId, cryptoCurrencyStatus: CryptoCurrencyStatus) fun onTokenItemLongClick(userWalletId: UserWalletId, cryptoCurrencyStatus: CryptoCurrencyStatus)
fun onApyLabelClick(userWalletId: UserWalletId, currencyStatus: CryptoCurrencyStatus)
fun onAccountExpandClick(account: Account) fun onAccountExpandClick(account: Account)
fun onAccountCollapseClick(account: Account) fun onAccountCollapseClick(account: Account)
@ -154,6 +160,42 @@ internal class WalletContentClickIntentsImplementor @Inject constructor(
} }
} }
override fun onApyLabelClick(userWalletId: UserWalletId, currencyStatus: CryptoCurrencyStatus) {
val navigationAction = if (currencyStatus.currency is CryptoCurrency.Token) {
NavigationAction.YieldSupply(currencyStatus.value.yieldSupplyStatus?.isActive == true)
} else {
NavigationAction.Staking
}
val event = when (navigationAction) {
is NavigationAction.YieldSupply -> {
MainScreenAnalyticsEvent.ApyClicked(
token = currencyStatus.currency.symbol,
blockchain = currencyStatus.currency.network.name,
action = "Earning",
state = if (currencyStatus.value.yieldSupplyStatus?.isActive == true) {
"Enabled"
} else {
"Disabled"
},
)
}
is NavigationAction.Staking -> {
MainScreenAnalyticsEvent.ApyClicked(
token = currencyStatus.currency.symbol,
blockchain = currencyStatus.currency.network.name,
action = "Staking",
state = if (currencyStatus.value.yieldBalance is YieldBalance.Data) {
"Enabled"
} else {
"Disabled"
},
)
}
}
analyticsEventHandler.send(event)
router.openTokenDetails(userWalletId, currencyStatus, navigationAction)
}
override fun onAccountExpandClick(account: Account) { override fun onAccountExpandClick(account: Account) {
val userWalletId = stateHolder.getSelectedWalletId() val userWalletId = stateHolder.getSelectedWalletId()
accountDependencies.expandedAccountsHolder.expandAccount(userWalletId, account.accountId) accountDependencies.expandedAccountsHolder.expandAccount(userWalletId, account.accountId)

View file

@ -15,6 +15,7 @@ import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.domain.pay.TangemPayDetailsConfig import com.tangem.domain.pay.TangemPayDetailsConfig
import com.tangem.domain.redux.ReduxStateHolder import com.tangem.domain.redux.ReduxStateHolder
import com.tangem.domain.redux.StateDialog import com.tangem.domain.redux.StateDialog
import com.tangem.domain.tokens.model.details.NavigationAction
import com.tangem.domain.tokens.model.details.TokenAction import com.tangem.domain.tokens.model.details.TokenAction
import com.tangem.feature.wallet.navigation.WalletRoute import com.tangem.feature.wallet.navigation.WalletRoute
import com.tangem.feature.wallet.presentation.wallet.state.model.WalletDialogConfig import com.tangem.feature.wallet.presentation.wallet.state.model.WalletDialogConfig
@ -66,13 +67,18 @@ internal class DefaultWalletRouter @Inject constructor(
urlOpener.openUrl(url) urlOpener.openUrl(url)
} }
override fun openTokenDetails(userWalletId: UserWalletId, currencyStatus: CryptoCurrencyStatus) { override fun openTokenDetails(
userWalletId: UserWalletId,
currencyStatus: CryptoCurrencyStatus,
navigationAction: NavigationAction?,
) {
val networkAddress = currencyStatus.value.networkAddress val networkAddress = currencyStatus.value.networkAddress
if (networkAddress != null && networkAddress.defaultAddress.value.isNotEmpty()) { if (networkAddress != null && networkAddress.defaultAddress.value.isNotEmpty()) {
router.push( router.push(
AppRoute.CurrencyDetails( AppRoute.CurrencyDetails(
userWalletId = userWalletId, userWalletId = userWalletId,
currency = currencyStatus.currency, currency = currencyStatus.currency,
navigationAction = navigationAction,
), ),
) )
} }

View file

@ -9,6 +9,7 @@ import com.tangem.domain.models.scan.ScanResponse
import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.models.wallet.UserWallet
import com.tangem.domain.models.wallet.UserWalletId import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.domain.pay.TangemPayDetailsConfig import com.tangem.domain.pay.TangemPayDetailsConfig
import com.tangem.domain.tokens.model.details.NavigationAction
import com.tangem.domain.tokens.model.details.TokenAction import com.tangem.domain.tokens.model.details.TokenAction
import com.tangem.feature.wallet.navigation.WalletRoute import com.tangem.feature.wallet.navigation.WalletRoute
import com.tangem.feature.wallet.presentation.wallet.state.model.WalletDialogConfig import com.tangem.feature.wallet.presentation.wallet.state.model.WalletDialogConfig
@ -42,7 +43,11 @@ internal interface InnerWalletRouter {
fun openUrl(url: String) fun openUrl(url: String)
/** Open token details screen */ /** Open token details screen */
fun openTokenDetails(userWalletId: UserWalletId, currencyStatus: CryptoCurrencyStatus) fun openTokenDetails(
userWalletId: UserWalletId,
currencyStatus: CryptoCurrencyStatus,
navigationAction: NavigationAction? = null,
)
/** Open stories screen */ /** Open stories screen */
fun openStoriesScreen() fun openStoriesScreen()

View file

@ -49,12 +49,17 @@ internal class TokenListStateConverter(
clickIntents.onTokenItemLongClick(selectedWallet.walletId, currencyStatus) clickIntents.onTokenItemLongClick(selectedWallet.walletId, currencyStatus)
} }
private val onApyLabelClick: (currencyStatus: CryptoCurrencyStatus) -> Unit = { currencyStatus ->
clickIntents.onApyLabelClick(selectedWallet.walletId, currencyStatus)
}
private fun tokenStatusConverter(accountId: AccountId? = null) = TokenItemStateConverter( private fun tokenStatusConverter(accountId: AccountId? = null) = TokenItemStateConverter(
appCurrency = appCurrency, appCurrency = appCurrency,
yieldModuleApyMap = yieldModuleApyMap, yieldModuleApyMap = yieldModuleApyMap,
stakingApyMap = stakingApyMap, stakingApyMap = stakingApyMap,
onItemClick = { _, status -> onTokenClick(accountId, status) }, onItemClick = { _, status -> onTokenClick(accountId, status) },
onItemLongClick = { _, status -> onTokenLongClick(accountId, status) }, onItemLongClick = { _, status -> onTokenLongClick(accountId, status) },
onApyLabelClick = { status -> onApyLabelClick(status) },
) )
override fun convert(value: WalletTokensListState): WalletTokensListState { override fun convert(value: WalletTokensListState): WalletTokensListState {

View file

@ -10,6 +10,7 @@ interface YieldSupplyComponent : ComposableContentComponent {
data class Params( data class Params(
val userWalletId: UserWalletId, val userWalletId: UserWalletId,
val cryptoCurrency: CryptoCurrency, val cryptoCurrency: CryptoCurrency,
val handleNavigation: Boolean? = null,
) )
interface Factory : ComponentFactory<Params, YieldSupplyComponent> interface Factory : ComponentFactory<Params, YieldSupplyComponent>

View file

@ -99,8 +99,15 @@ sealed class YieldSupplyAnalytics(
), ),
) )
data object FundsEarned : YieldSupplyAnalytics( data class FundsEarned(
val token: String,
val blockchain: String,
) : YieldSupplyAnalytics(
event = "Funds Earned", event = "Funds Earned",
params = mapOf(
TOKEN_PARAM to token,
BLOCKCHAIN to blockchain,
),
) )
data class FundsWithdrawn( data class FundsWithdrawn(

View file

@ -13,6 +13,8 @@ internal sealed class YieldSupplyFeeUM {
data class Content( data class Content(
val transactionDataList: ImmutableList<TransactionData.Uncompiled>, val transactionDataList: ImmutableList<TransactionData.Uncompiled>,
val feeFiatValue: TextReference, val feeFiatValue: TextReference,
// TODO move to FeePolicyUM
val estimatedFiatValue: TextReference,
val tokenFeeFiatValue: TextReference, val tokenFeeFiatValue: TextReference,
val maxNetworkFeeFiatValue: TextReference, val maxNetworkFeeFiatValue: TextReference,
val minTopUpFiatValue: TextReference, val minTopUpFiatValue: TextReference,

View file

@ -149,6 +149,7 @@ private class YieldSupplyActionContentPreviewProvider : PreviewParameterProvider
maxNetworkFeeFiatValue = stringReference("$8.50"), maxNetworkFeeFiatValue = stringReference("$8.50"),
minTopUpFiatValue = stringReference("$50"), minTopUpFiatValue = stringReference("$50"),
feeNoteValue = TextReference.EMPTY, feeNoteValue = TextReference.EMPTY,
estimatedFiatValue = TextReference.EMPTY,
), ),
isPrimaryButtonEnabled = false, isPrimaryButtonEnabled = false,
isTransactionSending = false, isTransactionSending = false,

View file

@ -91,6 +91,16 @@ internal class YieldSupplyModel @Inject constructor(
init { init {
checkIfYieldSupplyIsAvailable() checkIfYieldSupplyIsAvailable()
params.handleNavigation?.let { handle ->
if (handle) {
modelScope.launch {
delay(timeMillis = 1000)
bottomSheetNavigation.activate(Unit)
}
} else {
onStartEarningClick()
}
}
} }
private fun checkIfYieldSupplyIsAvailable() { private fun checkIfYieldSupplyIsAvailable() {
@ -185,7 +195,10 @@ internal class YieldSupplyModel @Inject constructor(
@Suppress("MaximumLineLength") @Suppress("MaximumLineLength")
private fun onCryptoCurrencyStatusUpdated(cryptoCurrencyStatus: CryptoCurrencyStatus) { private fun onCryptoCurrencyStatusUpdated(cryptoCurrencyStatus: CryptoCurrencyStatus) {
val yieldSupplyStatus = cryptoCurrencyStatus.value.yieldSupplyStatus val yieldSupplyStatus = cryptoCurrencyStatus.value.yieldSupplyStatus
val tokenProtocolStatus = yieldSupplyRepository.getTokenProtocolStatus(cryptoCurrency) val tokenProtocolStatus = yieldSupplyRepository.getTokenProtocolStatus(
userWallet.walletId,
cryptoCurrency,
)
val isActive = yieldSupplyStatus?.isActive == true val isActive = yieldSupplyStatus?.isActive == true
val isCryptoCurrencyStatusFromCache = cryptoCurrencyStatus.value.sources.networkSource != StatusSource.ACTUAL val isCryptoCurrencyStatusFromCache = cryptoCurrencyStatus.value.sources.networkSource != StatusSource.ACTUAL
val processing = uiState.value is YieldSupplyUM.Processing val processing = uiState.value is YieldSupplyUM.Processing
@ -298,7 +311,11 @@ internal class YieldSupplyModel @Inject constructor(
val address = cryptoCurrencyStatus.value.networkAddress?.defaultAddress?.value ?: return val address = cryptoCurrencyStatus.value.networkAddress?.defaultAddress?.value ?: return
modelScope.launch(dispatchers.default) { modelScope.launch(dispatchers.default) {
if (cryptoCurrencyStatus.value.yieldSupplyStatus?.isActive == true) { if (cryptoCurrencyStatus.value.yieldSupplyStatus?.isActive == true) {
yieldSupplyActivateUseCase(token, address).onRight { yieldSupplyActivateUseCase(
userWalletId = userWallet.walletId,
cryptoCurrency = token,
address = address,
).onRight {
lastYieldSupplyStatus = cryptoCurrencyStatus.value.yieldSupplyStatus lastYieldSupplyStatus = cryptoCurrencyStatus.value.yieldSupplyStatus
} }
} else { } else {

View file

@ -248,7 +248,7 @@ internal class YieldSupplyActiveModel @Inject constructor(
cryptoCurrencyStatus = cryptoStatus, cryptoCurrencyStatus = cryptoStatus,
appCurrency = appCurrency, appCurrency = appCurrency,
feeValue = currentFee, feeValue = currentFee,
maxNetworkFee = maxFee.second, maxNetworkFee = maxFee,
analyticsHandler = analyticsHandler, analyticsHandler = analyticsHandler,
), ),
) )

View file

@ -10,6 +10,7 @@ import com.tangem.core.ui.format.bigdecimal.format
import com.tangem.core.ui.extensions.TextReference import com.tangem.core.ui.extensions.TextReference
import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.appcurrency.model.AppCurrency
import com.tangem.domain.models.currency.CryptoCurrencyStatus import com.tangem.domain.models.currency.CryptoCurrencyStatus
import com.tangem.domain.yield.supply.models.YieldSupplyMaxFee
import com.tangem.features.yield.supply.api.analytics.YieldSupplyAnalytics import com.tangem.features.yield.supply.api.analytics.YieldSupplyAnalytics
import com.tangem.features.yield.supply.impl.R import com.tangem.features.yield.supply.impl.R
import com.tangem.features.yield.supply.impl.subcomponents.active.entity.YieldSupplyActiveContentUM import com.tangem.features.yield.supply.impl.subcomponents.active.entity.YieldSupplyActiveContentUM
@ -23,7 +24,7 @@ internal class YieldSupplyActiveFeeContentTransformer(
private val cryptoCurrencyStatus: CryptoCurrencyStatus, private val cryptoCurrencyStatus: CryptoCurrencyStatus,
private val appCurrency: AppCurrency, private val appCurrency: AppCurrency,
private val feeValue: BigDecimal, private val feeValue: BigDecimal,
private val maxNetworkFee: BigDecimal, private val maxNetworkFee: YieldSupplyMaxFee,
private val analyticsHandler: AnalyticsEventHandler, private val analyticsHandler: AnalyticsEventHandler,
) : Transformer<YieldSupplyActiveContentUM> { ) : Transformer<YieldSupplyActiveContentUM> {
@ -35,9 +36,12 @@ internal class YieldSupplyActiveFeeContentTransformer(
val tokenFiatFee = tokenFiatRate?.let(feeValue::multiply) val tokenFiatFee = tokenFiatRate?.let(feeValue::multiply)
val tokenFiatFeeValueText = tokenFiatFee.format { fiat(appCurrency.code, appCurrency.symbol) } val tokenFiatFeeValueText = tokenFiatFee.format { fiat(appCurrency.code, appCurrency.symbol) }
val maxFeeCryptoValueText = maxNetworkFee.format { crypto(cryptoCurrency) } val maxFeeCryptoValueText = maxNetworkFee.tokenMaxFee.format { crypto(cryptoCurrency) }
val maxFiatFee = tokenFiatRate?.let { rate -> maxNetworkFee.multiply(rate) } val maxFiatFeeValueText = maxNetworkFee.fiatMaxFee.format { fiat(
val maxFiatFeeValueText = maxFiatFee.format { fiat(appCurrency.code, appCurrency.symbol) } appCurrency.code,
appCurrency
.symbol,
) }
val feeNoteValue: TextReference = resourceReference( val feeNoteValue: TextReference = resourceReference(
id = R.string.yield_module_fee_policy_sheet_fee_note, id = R.string.yield_module_fee_policy_sheet_fee_note,
@ -49,7 +53,7 @@ internal class YieldSupplyActiveFeeContentTransformer(
), ),
) )
val isHighFee = feeValue > maxNetworkFee val isHighFee = feeValue > maxNetworkFee.tokenMaxFee
if (isHighFee) { if (isHighFee) {
analyticsHandler.send( analyticsHandler.send(

View file

@ -255,6 +255,7 @@ internal class YieldSupplyApproveModel @Inject constructor(
maxNetworkFeeFiatValue = TextReference.EMPTY, maxNetworkFeeFiatValue = TextReference.EMPTY,
minTopUpFiatValue = TextReference.EMPTY, minTopUpFiatValue = TextReference.EMPTY,
feeNoteValue = TextReference.EMPTY, feeNoteValue = TextReference.EMPTY,
estimatedFiatValue = TextReference.EMPTY,
), ),
) )
} }

View file

@ -86,7 +86,7 @@ internal fun YieldSupplyFeePolicyContent(
.padding(horizontal = 16.dp), .padding(horizontal = 16.dp),
) { ) {
val currentFee = when (yieldSupplyFeeUM) { val currentFee = when (yieldSupplyFeeUM) {
is YieldSupplyFeeUM.Content -> yieldSupplyFeeUM.tokenFeeFiatValue is YieldSupplyFeeUM.Content -> yieldSupplyFeeUM.estimatedFiatValue
YieldSupplyFeeUM.Error -> stringReference(StringsSigns.DASH_SIGN) YieldSupplyFeeUM.Error -> stringReference(StringsSigns.DASH_SIGN)
YieldSupplyFeeUM.Loading -> null YieldSupplyFeeUM.Loading -> null
} }
@ -184,6 +184,7 @@ private fun YieldSupplyFeePolicyContent_Preview() {
stringReference("2.46 USDT"), stringReference("2.46 USDT"),
), ),
), ),
estimatedFiatValue = stringReference("$8.50"),
), ),
tokenSymbol = "USDT", tokenSymbol = "USDT",
modifier = Modifier.background(TangemTheme.colors.background.primary), modifier = Modifier.background(TangemTheme.colors.background.primary),

View file

@ -63,6 +63,7 @@ internal class YieldSupplyStartEarningModel @Inject constructor(
private val yieldSupplyActivateUseCase: YieldSupplyActivateUseCase, private val yieldSupplyActivateUseCase: YieldSupplyActivateUseCase,
private val yieldSupplyMinAmountUseCase: YieldSupplyMinAmountUseCase, private val yieldSupplyMinAmountUseCase: YieldSupplyMinAmountUseCase,
private val yieldSupplyGetMaxFeeUseCase: YieldSupplyGetMaxFeeUseCase, private val yieldSupplyGetMaxFeeUseCase: YieldSupplyGetMaxFeeUseCase,
private val yieldSupplyGetCurrentFeeUseCase: YieldSupplyGetCurrentFeeUseCase,
private val yieldSupplyRepository: YieldSupplyRepository, private val yieldSupplyRepository: YieldSupplyRepository,
) : Model(), YieldSupplyNotificationsComponent.ModelCallback { ) : Model(), YieldSupplyNotificationsComponent.ModelCallback {
@ -130,10 +131,6 @@ internal class YieldSupplyStartEarningModel @Inject constructor(
} }
} }
private suspend fun getMaxFeePair(): Pair<BigDecimal, BigDecimal>? {
return yieldSupplyGetMaxFeeUseCase(userWallet, cryptoCurrencyStatus).getOrNull()
}
private suspend fun onLoadFee() { private suspend fun onLoadFee() {
if (cryptoCurrencyStatus.value is CryptoCurrencyStatus.Loading || uiState.value.isTransactionSending) return if (cryptoCurrencyStatus.value is CryptoCurrencyStatus.Loading || uiState.value.isTransactionSending) return
@ -141,15 +138,13 @@ internal class YieldSupplyStartEarningModel @Inject constructor(
it.copy(yieldSupplyFeeUM = YieldSupplyFeeUM.Loading) it.copy(yieldSupplyFeeUM = YieldSupplyFeeUM.Loading)
} }
val maxFeePair = getMaxFeePair() ?: return val maxFee = yieldSupplyGetMaxFeeUseCase(userWallet, cryptoCurrencyStatus).getOrNull() ?: return
val estimatedFee = yieldSupplyGetCurrentFeeUseCase(userWallet, cryptoCurrencyStatus).getOrNull() ?: return
val maxFee = maxFeePair.first
val maxFeeFiat = maxFeePair.second
val transactionListData = yieldSupplyStartEarningUseCase( val transactionListData = yieldSupplyStartEarningUseCase(
userWalletId = userWallet.walletId, userWalletId = userWallet.walletId,
cryptoCurrencyStatus = cryptoCurrencyStatus, cryptoCurrencyStatus = cryptoCurrencyStatus,
maxNetworkFee = maxFee, maxNetworkFee = maxFee.nativeMaxFee,
).getOrNull() ).getOrNull()
if (transactionListData == null) { if (transactionListData == null) {
@ -194,7 +189,7 @@ internal class YieldSupplyStartEarningModel @Inject constructor(
updatedTransactionList = updatedTransactionList, updatedTransactionList = updatedTransactionList,
feeValue = feeSum, feeValue = feeSum,
maxNetworkFee = maxFee, maxNetworkFee = maxFee,
maxNetworkFeeFiat = maxFeeFiat, estimatedFeeValue = estimatedFee,
minAmount = minAmount, minAmount = minAmount,
), ),
) )
@ -249,35 +244,52 @@ internal class YieldSupplyStartEarningModel @Inject constructor(
) )
}, },
ifRight = { ifRight = {
yieldSupplyRepository.saveTokenProtocolStatus(cryptoCurrency, YieldSupplyEnterStatus.Enter) onStartEarningTransactionSuccess(yieldSupplyFeeUM)
val event = AnalyticsParam.TxSentFrom.Earning(
blockchain = cryptoCurrency.network.name,
token = cryptoCurrency.symbol,
feeType = AnalyticsParam.FeeType.Normal,
)
analytics.send(YieldSupplyAnalytics.FundsEarned)
yieldSupplyFeeUM.transactionDataList.forEach {
analytics.send(
Basic.TransactionSent(
sentFrom = event,
memoType = Basic.TransactionSent.MemoType.Null,
),
)
}
val address = cryptoCurrencyStatus.value.networkAddress?.defaultAddress?.value
if (address != null) {
yieldSupplyActivateUseCase(cryptoCurrency, address)
}
modelScope.launch {
params.callback.onTransactionSent()
}
}, },
) )
} }
} }
private suspend fun onStartEarningTransactionSuccess(yieldSupplyFeeUM: YieldSupplyFeeUM.Content) {
yieldSupplyRepository.saveTokenProtocolStatus(
userWallet.walletId,
cryptoCurrency,
YieldSupplyEnterStatus.Enter,
)
val event = AnalyticsParam.TxSentFrom.Earning(
blockchain = cryptoCurrency.network.name,
token = cryptoCurrency.symbol,
feeType = AnalyticsParam.FeeType.Normal,
)
analytics.send(
YieldSupplyAnalytics.FundsEarned(
blockchain = cryptoCurrency.network.name,
token = cryptoCurrency.symbol,
),
)
yieldSupplyFeeUM.transactionDataList.forEach {
analytics.send(
Basic.TransactionSent(
sentFrom = event,
memoType = Basic.TransactionSent.MemoType.Null,
),
)
}
val address = cryptoCurrencyStatus.value.networkAddress?.defaultAddress?.value
if (address != null) {
yieldSupplyActivateUseCase(
userWalletId = userWallet.walletId,
cryptoCurrency = cryptoCurrency,
address = address,
)
}
modelScope.launch {
params.callback.onTransactionSent()
}
}
private fun subscribeOnCurrencyStatusUpdates() { private fun subscribeOnCurrencyStatusUpdates() {
modelScope.launch { modelScope.launch {
getUserWalletUseCase(params.userWalletId).fold( getUserWalletUseCase(params.userWalletId).fold(

View file

@ -9,6 +9,7 @@ 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.CryptoCurrencyStatus import com.tangem.domain.models.currency.CryptoCurrencyStatus
import com.tangem.domain.yield.supply.models.YieldSupplyMaxFee
import com.tangem.features.yield.supply.impl.R import com.tangem.features.yield.supply.impl.R
import com.tangem.features.yield.supply.impl.common.entity.YieldSupplyActionUM 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.entity.YieldSupplyFeeUM
@ -24,8 +25,8 @@ internal class YieldSupplyStartEarningFeeContentTransformer(
private val appCurrency: AppCurrency, private val appCurrency: AppCurrency,
private val updatedTransactionList: List<TransactionData.Uncompiled>, private val updatedTransactionList: List<TransactionData.Uncompiled>,
private val feeValue: BigDecimal, private val feeValue: BigDecimal,
private val maxNetworkFee: BigDecimal, private val estimatedFeeValue: BigDecimal,
private val maxNetworkFeeFiat: BigDecimal, private val maxNetworkFee: YieldSupplyMaxFee,
private val minAmount: BigDecimal, private val minAmount: BigDecimal,
) : Transformer<YieldSupplyActionUM> { ) : Transformer<YieldSupplyActionUM> {
override fun transform(prevState: YieldSupplyActionUM): YieldSupplyActionUM { override fun transform(prevState: YieldSupplyActionUM): YieldSupplyActionUM {
@ -36,15 +37,17 @@ internal class YieldSupplyStartEarningFeeContentTransformer(
val feeFiat = feeFiatRate?.let(feeValue::multiply) val feeFiat = feeFiatRate?.let(feeValue::multiply)
val feeFiatValueText = feeFiat.format { fiat(appCurrency.code, appCurrency.symbol) } val feeFiatValueText = feeFiat.format { fiat(appCurrency.code, appCurrency.symbol) }
val estimatedFeeFiat = estimatedFeeValue
val estimatedFeeFiatValueText = estimatedFeeFiat.format { fiat(appCurrency.code, appCurrency.symbol) }
val tokenCryptoFee = tokenFiatRate?.let { rate -> val tokenCryptoFee = tokenFiatRate?.let { rate ->
feeFiat?.divide(rate, cryptoCurrency.decimals, RoundingMode.HALF_UP) estimatedFeeFiat.divide(rate, cryptoCurrency.decimals, RoundingMode.HALF_UP)
} }
val tokenCryptoFeeValueText = tokenCryptoFee.format { crypto(cryptoCurrency) } val tokenCryptoFeeValueText = tokenCryptoFee.format { crypto(cryptoCurrency) }
val tokenFiatFee = feeFiat // same fiat amount val tokenFiatFeeValueText = estimatedFeeFiat.format { fiat(appCurrency.code, appCurrency.symbol) }
val tokenFiatFeeValueText = tokenFiatFee.format { fiat(appCurrency.code, appCurrency.symbol) }
val maxFeeCryptoValueText = maxNetworkFee.format { crypto(cryptoCurrency) } val maxFeeCryptoValueText = maxNetworkFee.tokenMaxFee.format { crypto(cryptoCurrency) }
val maxFiatFeeValueText = maxNetworkFeeFiat.format { fiat(appCurrency.code, appCurrency.symbol) } val maxFiatFeeValueText = maxNetworkFee.fiatMaxFee.format { fiat(appCurrency.code, appCurrency.symbol) }
val minAmountCryptoText = minAmount.format { crypto(cryptoCurrency) } val minAmountCryptoText = minAmount.format { crypto(cryptoCurrency) }
val minAmountFiat = tokenFiatRate?.let(minAmount::multiply) val minAmountFiat = tokenFiatRate?.let(minAmount::multiply)
@ -80,6 +83,7 @@ internal class YieldSupplyStartEarningFeeContentTransformer(
minTopUpFiatValue = stringReference(minAmountFiatText), minTopUpFiatValue = stringReference(minAmountFiatText),
feeNoteValue = feeNoteValue, feeNoteValue = feeNoteValue,
minFeeNoteValue = minFeeNoteValue, minFeeNoteValue = minFeeNoteValue,
estimatedFiatValue = stringReference(estimatedFeeFiatValueText),
), ),
) )
} }

View file

@ -165,37 +165,45 @@ internal class YieldSupplyStopEarningModel @Inject constructor(
) )
}, },
ifRight = { ifRight = {
yieldSupplyRepository.saveTokenProtocolStatus(cryptoCurrency, YieldSupplyEnterStatus.Exit) onStopEarningTransactionSuccess()
analytics.send(
YieldSupplyAnalytics.FundsWithdrawn(
token = cryptoCurrency.symbol,
blockchain = cryptoCurrency.network.name,
),
)
val event = AnalyticsParam.TxSentFrom.Earning(
blockchain = cryptoCurrency.network.name,
token = cryptoCurrency.symbol,
feeType = AnalyticsParam.FeeType.Normal,
)
analytics.send(
Basic.TransactionSent(
sentFrom = event,
memoType = Basic.TransactionSent.MemoType.Null,
),
)
val address = cryptoCurrencyStatus.value.networkAddress?.defaultAddress?.value
if (address != null) {
yieldSupplyDeactivateUseCase(cryptoCurrency, address)
}
modelScope.launch {
params.callback.onTransactionSent()
}
}, },
) )
} }
} }
private suspend fun onStopEarningTransactionSuccess() {
yieldSupplyRepository.saveTokenProtocolStatus(
userWallet.walletId,
cryptoCurrency,
YieldSupplyEnterStatus.Exit,
)
analytics.send(
YieldSupplyAnalytics.FundsWithdrawn(
token = cryptoCurrency.symbol,
blockchain = cryptoCurrency.network.name,
),
)
val event = AnalyticsParam.TxSentFrom.Earning(
blockchain = cryptoCurrency.network.name,
token = cryptoCurrency.symbol,
feeType = AnalyticsParam.FeeType.Normal,
)
analytics.send(
Basic.TransactionSent(
sentFrom = event,
memoType = Basic.TransactionSent.MemoType.Null,
),
)
val address = cryptoCurrencyStatus.value.networkAddress?.defaultAddress?.value
if (address != null) {
yieldSupplyDeactivateUseCase(cryptoCurrency, address)
}
modelScope.launch {
params.callback.onTransactionSent()
}
}
private fun subscribeOnCurrencyStatusUpdates() { private fun subscribeOnCurrencyStatusUpdates() {
modelScope.launch { modelScope.launch {
feeCryptoCurrencyStatusFlow.update { feeCryptoCurrencyStatusFlow.update {

View file

@ -38,6 +38,7 @@ internal class YieldSupplyStopEarningFeeContentTransformer(
maxNetworkFeeFiatValue = TextReference.EMPTY, maxNetworkFeeFiatValue = TextReference.EMPTY,
minTopUpFiatValue = TextReference.EMPTY, minTopUpFiatValue = TextReference.EMPTY,
feeNoteValue = TextReference.EMPTY, feeNoteValue = TextReference.EMPTY,
estimatedFiatValue = TextReference.EMPTY,
), ),
) )
} }