Updated on 2026-08-14

This commit is contained in:
Tangem 2025-11-06 18:00:16 +05:00
parent 7e5a678d84
commit d442154b82
17 changed files with 137 additions and 6 deletions

View file

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

View file

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

View file

@ -62,6 +62,7 @@ class TokenItemStateConverter(
},
private val onItemClick: ((TokenItemState, CryptoCurrencyStatus) -> Unit)? = null,
private val onItemLongClick: ((TokenItemState, CryptoCurrencyStatus) -> Unit)? = null,
private val onApyLabelClick: ((TokenItemState, CryptoCurrencyStatus) -> Unit)? = null,
) : Converter<CryptoCurrencyStatus, TokenItemState> {
override fun convert(value: CryptoCurrencyStatus): TokenItemState {
@ -102,6 +103,9 @@ class TokenItemStateConverter(
onItemLongClick = onItemLongClick?.let { onItemLongClick ->
{ onItemLongClick(it, this) }
},
onApyLabelClick = onApyLabelClick?.let { onApyLabelClick ->
{ onApyLabelClick(it, this) }
},
)
}

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.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.STATE
import com.tangem.core.analytics.models.AnalyticsParam.Key.TOKEN_PARAM
/**
@ -108,6 +111,21 @@ sealed class MainScreenAnalyticsEvent(
event = "Hot Token Error",
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
companion object {

View file

@ -110,6 +110,7 @@ fun TokenItem(
TokenTitle(
state = state.titleState,
onApyClick = { state.onApyLabelClick?.invoke(state) },
modifier = Modifier
.layoutId(layoutId = LayoutId.TITLE)
.padding(end = TangemTheme.dimens.spacing8)

View file

@ -3,6 +3,7 @@ package com.tangem.core.ui.components.token.internal
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.*
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
@ -20,10 +21,10 @@ import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.components.token.state.TokenItemState.TitleState as TokenTitleState
@Composable
internal fun TokenTitle(state: TokenTitleState?, modifier: Modifier = Modifier) {
internal fun TokenTitle(state: TokenTitleState?, onApyClick: () -> Unit, modifier: Modifier = Modifier) {
when (state) {
is TokenTitleState.Content -> {
ContentTitle(state = state, modifier = modifier)
ContentTitle(state = state, modifier = modifier, onApyClick = onApyClick)
}
is TokenTitleState.Loading -> {
RectangleShimmer(modifier = modifier.placeholderSize(), radius = TangemTheme.dimens.radius4)
@ -36,7 +37,7 @@ internal fun TokenTitle(state: TokenTitleState?, modifier: Modifier = Modifier)
}
@Composable
private fun ContentTitle(state: TokenTitleState.Content, modifier: Modifier = Modifier) {
private fun ContentTitle(state: TokenTitleState.Content, onApyClick: () -> Unit, modifier: Modifier = Modifier) {
Row(
modifier = modifier,
horizontalArrangement = Arrangement.spacedBy(space = TangemTheme.dimens.spacing6),
@ -60,7 +61,11 @@ private fun ContentTitle(state: TokenTitleState.Content, modifier: Modifier = Mo
YieldSupplyApyLabel(
apy = state.earnApy,
isActive = state.earnApyIsActive,
modifier = Modifier.align(alignment = Alignment.CenterVertically),
modifier = Modifier
.align(alignment = Alignment.CenterVertically)
.clickable {
onApyClick()
},
)
}
}

View file

@ -40,6 +40,9 @@ sealed class TokenItemState {
/** Callback which will be called when an item is long clicked */
abstract val onItemLongClick: ((TokenItemState) -> Unit)?
/** Callback which will be called when an apy label is clicked */
abstract val onApyLabelClick: ((TokenItemState) -> Unit)?
/**
* Loading token state
*
@ -58,6 +61,7 @@ sealed class TokenItemState {
override val subtitle2State: Subtitle2State = Subtitle2State.Loading
override val onItemClick: ((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 onItemClick: ((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 onItemClick: ((TokenItemState) -> Unit)?,
override val onItemLongClick: ((TokenItemState) -> Unit)?,
override val onApyLabelClick: ((TokenItemState) -> Unit)? = null,
) : TokenItemState()
/**
@ -116,6 +122,7 @@ sealed class TokenItemState {
override val fiatAmountState: FiatAmountState? = null
override val onItemClick: ((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 titleState: TitleState,
override val subtitleState: SubtitleState? = null,
override val onApyLabelClick: ((TokenItemState) -> Unit)? = null,
override val onItemClick: ((TokenItemState) -> Unit)?,
override val onItemLongClick: ((TokenItemState) -> Unit)?,
) : TokenItemState() {
@ -159,6 +167,7 @@ sealed class TokenItemState {
override val fiatAmountState: FiatAmountState? = null
override val subtitle2State: Subtitle2State? = null
override val onItemClick: ((TokenItemState) -> Unit)? = null
override val onApyLabelClick: ((TokenItemState) -> Unit)? = null
}
@Immutable

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

@ -4,12 +4,14 @@ import com.tangem.core.decompose.factory.ComponentFactory
import com.tangem.core.ui.decompose.ComposableContentComponent
import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.domain.tokens.model.details.NavigationAction
interface TokenDetailsComponent : ComposableContentComponent {
data class Params(
val userWalletId: UserWalletId,
val currency: CryptoCurrency,
val navigationAction: NavigationAction? = null,
)
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.decompose.ComposableBottomSheetComponent
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.route.TokenDetailsBottomSheetConfig
import com.tangem.feature.tokendetails.presentation.tokendetails.ui.TokenDetailsScreen
@ -76,6 +77,8 @@ internal class DefaultTokenDetailsComponent @AssistedInject constructor(
params = YieldSupplyComponent.Params(
userWalletId = params.userWalletId,
cryptoCurrency = params.currency,
handleNavigation = (params.navigationAction as? NavigationAction.YieldSupply)
?.isActive,
),
)

View file

@ -62,6 +62,7 @@ import com.tangem.domain.tokens.model.TokenActionsState
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.DetailsScreenOpened.TokenBalance
import com.tangem.domain.tokens.model.details.NavigationAction
import com.tangem.domain.tokens.model.details.TokenAction
import com.tangem.domain.transaction.error.AssociateAssetError
import com.tangem.domain.transaction.error.IncompleteTransactionError
@ -218,6 +219,7 @@ internal class TokenDetailsModel @Inject constructor(
updateContent()
handleBalanceHiding()
checkForActionUpdates()
handleNavigationParam()
}
fun onResume() {
@ -1221,6 +1223,12 @@ internal class TokenDetailsModel @Inject constructor(
}
}
private fun handleNavigationParam() {
if (params.navigationAction is NavigationAction.Staking) {
openStaking()
}
}
private companion object {
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 com.tangem.common.ui.expressStatus.ExpressStatusBottomSheetConfig
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.domain.models.account.Account
import com.tangem.domain.models.currency.CryptoCurrency
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.UserWalletId
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.GetSingleCryptoCurrencyStatusUseCase
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.wallets.usecase.GetUserWalletUseCase
import com.tangem.feature.wallet.presentation.account.AccountDependencies
@ -45,6 +49,8 @@ internal interface WalletContentClickIntents {
fun onTokenItemLongClick(userWalletId: UserWalletId, cryptoCurrencyStatus: CryptoCurrencyStatus)
fun onApyLabelClick(userWalletId: UserWalletId, currencyStatus: CryptoCurrencyStatus)
fun onAccountExpandClick(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) {
val userWalletId = stateHolder.getSelectedWalletId()
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.redux.ReduxStateHolder
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.feature.wallet.navigation.WalletRoute
import com.tangem.feature.wallet.presentation.wallet.state.model.WalletDialogConfig
@ -66,13 +67,18 @@ internal class DefaultWalletRouter @Inject constructor(
urlOpener.openUrl(url)
}
override fun openTokenDetails(userWalletId: UserWalletId, currencyStatus: CryptoCurrencyStatus) {
override fun openTokenDetails(
userWalletId: UserWalletId,
currencyStatus: CryptoCurrencyStatus,
navigationAction: NavigationAction?,
) {
val networkAddress = currencyStatus.value.networkAddress
if (networkAddress != null && networkAddress.defaultAddress.value.isNotEmpty()) {
router.push(
AppRoute.CurrencyDetails(
userWalletId = userWalletId,
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.UserWalletId
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.feature.wallet.navigation.WalletRoute
import com.tangem.feature.wallet.presentation.wallet.state.model.WalletDialogConfig
@ -42,7 +43,11 @@ internal interface InnerWalletRouter {
fun openUrl(url: String)
/** Open token details screen */
fun openTokenDetails(userWalletId: UserWalletId, currencyStatus: CryptoCurrencyStatus)
fun openTokenDetails(
userWalletId: UserWalletId,
currencyStatus: CryptoCurrencyStatus,
navigationAction: NavigationAction? = null,
)
/** Open stories screen */
fun openStoriesScreen()

View file

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

View file

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

View file

@ -91,6 +91,16 @@ internal class YieldSupplyModel @Inject constructor(
init {
checkIfYieldSupplyIsAvailable()
if (params.handleNavigation != null) {
if (params.handleNavigation == true) {
modelScope.launch {
delay(timeMillis = 1000)
bottomSheetNavigation.activate(Unit)
}
} else {
onStartEarningClick()
}
}
}
private fun checkIfYieldSupplyIsAvailable() {