Updated on 2026-08-14

This commit is contained in:
Tangem 2025-07-18 12:51:39 +03:00
commit a56bcff7dd
438 changed files with 9017 additions and 4452 deletions

View file

@ -2,13 +2,13 @@ package com.tangem.feature.tokendetails.deeplink
import com.tangem.common.routing.AppRoute
import com.tangem.common.routing.AppRouter
import com.tangem.common.routing.deeplink.DeeplinkConst.DERIVATION_PATH_KEY
import com.tangem.common.routing.deeplink.DeeplinkConst.NETWORK_ID_KEY
import com.tangem.common.routing.deeplink.DeeplinkConst.TOKEN_ID_KEY
import com.tangem.common.routing.deeplink.DeeplinkConst.TRANSACTION_ID_KEY
import com.tangem.common.routing.deeplink.DeeplinkConst.TYPE_KEY
import com.tangem.common.routing.deeplink.DeeplinkConst.WALLET_ID_KEY
import com.tangem.core.analytics.api.AnalyticsEventHandler
import com.tangem.core.deeplink.DeeplinkConst.DERIVATION_PATH_KEY
import com.tangem.core.deeplink.DeeplinkConst.NETWORK_ID_KEY
import com.tangem.core.deeplink.DeeplinkConst.TOKEN_ID_KEY
import com.tangem.core.deeplink.DeeplinkConst.TRANSACTION_ID_KEY
import com.tangem.core.deeplink.DeeplinkConst.TYPE_KEY
import com.tangem.core.deeplink.DeeplinkConst.WALLET_ID_KEY
import com.tangem.domain.common.util.cardTypesResolver
import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.domain.models.network.Network
@ -125,7 +125,6 @@ internal class DefaultTokenDetailsDeepLinkHandler @AssistedInject constructor(
isMultiCurrency -> fetchCurrencyStatusUseCase.invoke(
userWalletId = userWallet.walletId,
id = cryptoCurrency.id,
refresh = true,
)
!isMultiCurrency && tokensFeatureToggles.isWalletBalanceFetcherEnabled ->
walletBalanceFetcher(params = WalletBalanceFetcher.Params(userWalletId = userWallet.walletId))

View file

@ -3,6 +3,7 @@ package com.tangem.feature.tokendetails.presentation.tokendetails.model
import androidx.compose.runtime.Stable
import androidx.paging.cachedIn
import arrow.core.getOrElse
import arrow.core.merge
import com.tangem.blockchain.common.address.AddressType
import com.tangem.common.routing.AppRoute
import com.tangem.common.routing.AppRouter
@ -11,7 +12,9 @@ import com.tangem.common.ui.bottomsheet.receive.mapToAddressModels
import com.tangem.common.ui.expressStatus.ExpressStatusBottomSheetConfig
import com.tangem.common.ui.expressStatus.state.ExpressTransactionStateUM
import com.tangem.core.analytics.api.AnalyticsEventHandler
import com.tangem.core.analytics.api.AnalyticsExceptionHandler
import com.tangem.core.analytics.models.AnalyticsParam
import com.tangem.core.analytics.models.ExceptionAnalyticsEvent
import com.tangem.core.decompose.di.GlobalUiMessageSender
import com.tangem.core.decompose.di.ModelScoped
import com.tangem.core.decompose.model.Model
@ -67,7 +70,6 @@ import com.tangem.domain.txhistory.usecase.GetTxHistoryItemsCountUseCase
import com.tangem.domain.txhistory.usecase.GetTxHistoryItemsUseCase
import com.tangem.domain.wallets.models.UserWallet
import com.tangem.domain.wallets.models.UserWalletId
import com.tangem.domain.wallets.models.requireColdWallet
import com.tangem.domain.wallets.usecase.GetExploreUrlUseCase
import com.tangem.domain.wallets.usecase.GetUserWalletUseCase
import com.tangem.feature.tokendetails.deeplink.TokenDetailsDeepLinkActionListener
@ -134,6 +136,7 @@ internal class TokenDetailsModel @Inject constructor(
private val appRouter: AppRouter,
private val router: InnerTokenDetailsRouter,
private val tokenDetailsDeepLinkActionListener: TokenDetailsDeepLinkActionListener,
private val analyticsExceptionHandler: AnalyticsExceptionHandler,
) : Model(), TokenDetailsClickIntents {
private val params = paramsContainer.require<TokenDetailsComponent.Params>()
@ -205,15 +208,15 @@ internal class TokenDetailsModel @Inject constructor(
checkForActionUpdates()
}
fun onResume() {
subscribeOnExpressTransactionsUpdates()
}
fun onPause() {
expressTxStatusTaskScheduler.cancelTask()
expressTxJobHolder.cancel()
}
fun onResume() {
subscribeOnExpressTransactionsUpdates()
}
override fun onDestroy() {
expressTxStatusTaskScheduler.cancelTask()
expressTxJobHolder.cancel()
@ -252,7 +255,7 @@ internal class TokenDetailsModel @Inject constructor(
.launchIn(modelScope)
}
private suspend fun updateButtons(currencyStatus: CryptoCurrencyStatus) {
private fun updateButtons(currencyStatus: CryptoCurrencyStatus) {
getCryptoCurrencyActionsUseCase(
userWallet = userWallet,
cryptoCurrencyStatus = currencyStatus,
@ -304,66 +307,61 @@ internal class TokenDetailsModel @Inject constructor(
}
private fun subscribeOnCurrencyStatusUpdates() {
modelScope.launch(dispatchers.main) {
getSingleCryptoCurrencyStatusUseCase.invokeMultiWallet(
userWalletId = userWalletId,
currencyId = cryptoCurrency.id,
isSingleWalletWithTokens = userWallet is UserWallet.Cold &&
userWallet.scanResponse.cardTypesResolver.isSingleWalletWithToken(),
)
.distinctUntilChanged()
.onEach { maybeCurrencyStatus ->
internalUiState.value = stateFactory.getCurrencyLoadedBalanceState(maybeCurrencyStatus)
maybeCurrencyStatus.onRight { status ->
cryptoCurrencyStatus = status
updateButtons(currencyStatus = status)
updateWarnings(status)
subscribeOnUpdateStakingInfo(status)
}
currencyStatusAnalyticsSender.send(maybeCurrencyStatus)
getSingleCryptoCurrencyStatusUseCase.invokeMultiWallet(
userWalletId = userWalletId,
currencyId = cryptoCurrency.id,
isSingleWalletWithTokens = userWallet is UserWallet.Cold &&
userWallet.scanResponse.cardTypesResolver.isSingleWalletWithToken(),
)
.distinctUntilChanged()
.onEach { maybeCurrencyStatus ->
internalUiState.value = stateFactory.getCurrencyLoadedBalanceState(maybeCurrencyStatus)
maybeCurrencyStatus.onRight { status ->
cryptoCurrencyStatus = status
updateButtons(currencyStatus = status)
updateWarnings(status)
subscribeOnUpdateStakingInfo(status)
}
.flowOn(dispatchers.main)
.launchIn(modelScope)
.saveIn(marketPriceJobHolder)
}
currencyStatusAnalyticsSender.send(maybeCurrencyStatus)
}
.flowOn(dispatchers.main)
.launchIn(modelScope)
.saveIn(marketPriceJobHolder)
}
private fun subscribeOnExpressTransactionsUpdates() {
modelScope.launch(dispatchers.main) {
expressTxStatusTaskScheduler.cancelTask()
expressStatusFactory
.getExpressStatuses()
.distinctUntilChanged()
.onEach { waitForFirstExpressStatusEmmit.value = true }
.onEach { expressTxs ->
internalUiState.value = expressStatusFactory.getStateWithUpdatedExpressTxs(
expressTxs,
::updateNetworkToSwapBalance,
)
expressTxStatusTaskScheduler.scheduleTask(
modelScope,
PeriodicTask(
isDelayFirst = false,
delay = EXPRESS_STATUS_UPDATE_DELAY,
task = {
runCatching {
expressStatusFactory.getUpdatedExpressStatuses(internalUiState.value.expressTxs)
}
},
onSuccess = { updatedTxs ->
internalUiState.value = expressStatusFactory.getStateWithUpdatedExpressTxs(
updatedTxs,
::updateNetworkToSwapBalance,
)
},
onError = { /* no-op */ },
),
)
}
.flowOn(dispatchers.main)
.launchIn(modelScope)
.saveIn(expressTxJobHolder)
}
expressTxStatusTaskScheduler.cancelTask()
expressStatusFactory.getExpressStatuses()
.distinctUntilChanged()
.onEach { waitForFirstExpressStatusEmmit.value = true }
.onEach { expressTxs ->
internalUiState.value = expressStatusFactory.getStateWithUpdatedExpressTxs(
expressTxs = expressTxs,
updateBalance = ::updateNetworkToSwapBalance,
)
expressTxStatusTaskScheduler.scheduleTask(
scope = modelScope,
task = PeriodicTask(
isDelayFirst = false,
delay = EXPRESS_STATUS_UPDATE_DELAY,
task = {
runCatching {
expressStatusFactory.getUpdatedExpressStatuses(internalUiState.value.expressTxs)
}
},
onSuccess = { updatedTxs ->
internalUiState.value = expressStatusFactory.getStateWithUpdatedExpressTxs(
updatedTxs,
::updateNetworkToSwapBalance,
)
},
onError = { /* no-op */ },
),
)
}
.flowOn(dispatchers.main)
.launchIn(modelScope)
.saveIn(expressTxJobHolder)
}
private fun updateNetworkToSwapBalance(toCryptoCurrency: CryptoCurrency) {
@ -443,22 +441,49 @@ internal class TokenDetailsModel @Inject constructor(
private fun updateTopBarMenu() {
modelScope.launch(dispatchers.main) {
val hasDerivations =
networkHasDerivationUseCase(
scanResponse = userWallet.requireColdWallet().scanResponse, // TODO [REDACTED_TASK_KEY]
network = cryptoCurrency.network,
).getOrElse { false }
val hasDerivations = networkHasDerivationUseCase(
userWallet = userWallet,
network = cryptoCurrency.network,
).getOrElse { false }
val isSupported = getExtendedPublicKeyForCurrencyUseCase.isSupported(userWalletId, cryptoCurrency.network)
val isSupported = isXPUBSupported()
internalUiState.value = stateFactory.getStateWithUpdatedMenu(
cardTypesResolver = userWallet.scanResponse.cardTypesResolver,
userWallet = userWallet,
hasDerivations = hasDerivations,
isSupported = isSupported,
)
}
}
private suspend fun isXPUBSupported(): Boolean {
return getExtendedPublicKeyForCurrencyUseCase.isSupported(
userWalletId = userWalletId,
network = cryptoCurrency.network,
)
.mapLeft {
analyticsExceptionHandler.sendException(
event = ExceptionAnalyticsEvent(
exception = it,
params = mapOf(
"blockchainId" to cryptoCurrency.network.id.rawId.value,
"networkId" to cryptoCurrency.network.backendId,
),
),
)
Timber.e(
/* t = */ it,
/* message = */ "Unable to get wallet manager for user wallet %s and network %s",
/* ...args = */ userWalletId,
cryptoCurrency.network,
)
false
}
.merge()
}
private fun createSelectedAppCurrencyFlow(): StateFlow<AppCurrency> {
return getSelectedAppCurrencyUseCase()
.map { maybeAppCurrency ->
@ -750,13 +775,7 @@ internal class TokenDetailsModel @Inject constructor(
modelScope.launch(dispatchers.main) {
listOf(
async {
fetchCurrencyStatusUseCase(
userWalletId = userWalletId,
id = cryptoCurrency.id,
refresh = true,
)
},
async { fetchCurrencyStatusUseCase(userWalletId = userWalletId, id = cryptoCurrency.id) },
async {
updateTxHistory(
refresh = true,

View file

@ -14,7 +14,6 @@ import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.domain.models.network.Network
import com.tangem.domain.staking.GetStakingIntegrationIdUseCase
import com.tangem.domain.wallets.models.UserWalletId
import com.tangem.domain.wallets.models.requireColdWallet
import com.tangem.domain.wallets.usecase.GetUserWalletUseCase
import com.tangem.feature.tokendetails.presentation.tokendetails.model.TokenDetailsClickIntents
import com.tangem.feature.tokendetails.presentation.tokendetails.state.*
@ -101,7 +100,7 @@ internal class TokenDetailsSkeletonStateConverter(
val isBitcoin = isBitcoin(cryptoCurrency.network.rawId)
val hasDerivations = networkHasDerivationUseCase(
scanResponse = userWallet.requireColdWallet().scanResponse, // TODO [REDACTED_TASK_KEY]
userWallet = userWallet,
network = cryptoCurrency.network,
).getOrElse { false }

View file

@ -24,6 +24,7 @@ import com.tangem.lib.crypto.BlockchainUtils.isStakingRewardUnavailable
import com.tangem.utils.Provider
import com.tangem.utils.converter.Converter
import com.tangem.utils.isNullOrZero
import timber.log.Timber
import java.math.BigDecimal
internal class TokenDetailsStakingInfoConverter(
@ -45,6 +46,7 @@ internal class TokenDetailsStakingInfoConverter(
state: TokenDetailsState,
stakingAvailability: StakingAvailability,
): StakingBlockUM? {
Timber.i("Define staking block for [${status.currency.id.value}] with availability:\n$stakingAvailability")
return when (stakingAvailability) {
StakingAvailability.TemporaryUnavailable -> StakingBlockUM.TemporaryUnavailable
StakingAvailability.Unavailable -> null
@ -60,6 +62,15 @@ internal class TokenDetailsStakingInfoConverter(
val iconState = state.tokenInfoBlockState.iconState
Timber.i(
"""
getStakingInfoBlock:
yieldBalance: $yieldBalance
stakingCryptoAmount: $stakingCryptoAmount
stakingEntryInfo: $stakingEntryInfo
""".trimIndent(),
)
return when {
stakingCryptoAmount.isNullOrZero() && stakingEntryInfo != null -> {
if (pendingBalances.isEmpty()) {

View file

@ -14,7 +14,7 @@ import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.res.TangemTheme
import com.tangem.domain.appcurrency.model.AppCurrency
import com.tangem.domain.card.NetworkHasDerivationUseCase
import com.tangem.domain.common.CardTypesResolver
import com.tangem.domain.common.util.cardTypesResolver
import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.domain.models.network.Network
import com.tangem.domain.models.network.NetworkAddress
@ -29,6 +29,7 @@ import com.tangem.domain.tokens.model.TokenActionsState
import com.tangem.domain.tokens.model.warnings.CryptoCurrencyWarning
import com.tangem.domain.txhistory.models.TxHistoryListError
import com.tangem.domain.txhistory.models.TxHistoryStateError
import com.tangem.domain.wallets.models.UserWallet
import com.tangem.domain.wallets.models.UserWalletId
import com.tangem.domain.wallets.usecase.GetUserWalletUseCase
import com.tangem.feature.tokendetails.presentation.tokendetails.model.TokenDetailsClickIntents
@ -348,7 +349,7 @@ internal class TokenDetailsStateFactory(
}
fun getStateWithUpdatedMenu(
cardTypesResolver: CardTypesResolver,
userWallet: UserWallet,
hasDerivations: Boolean,
isSupported: Boolean,
): TokenDetailsState {
@ -356,7 +357,7 @@ internal class TokenDetailsStateFactory(
copy(
topAppBarConfig = topAppBarConfig.copy(
tokenDetailsAppBarMenuConfig = topAppBarConfig.tokenDetailsAppBarMenuConfig
?.updateMenu(cardTypesResolver, hasDerivations, isSupported),
?.updateMenu(userWallet, hasDerivations, isSupported),
),
)
}
@ -385,11 +386,16 @@ internal class TokenDetailsStateFactory(
}
private fun TokenDetailsAppBarMenuConfig.updateMenu(
cardTypesResolver: CardTypesResolver,
userWallet: UserWallet,
hasDerivations: Boolean,
isSupported: Boolean,
): TokenDetailsAppBarMenuConfig? {
if (cardTypesResolver.isSingleWalletWithToken()) return null
if (userWallet is UserWallet.Cold &&
userWallet.scanResponse.cardTypesResolver.isSingleWalletWithToken()
) {
return null
}
return copy(
items = buildList {
if (isSupported && hasDerivations) {

View file

@ -52,7 +52,7 @@ internal class ExchangeStatusFactory @AssistedInject constructor(
)
}
suspend operator fun invoke(): Flow<PersistentList<ExchangeUM>> {
operator fun invoke(): Flow<PersistentList<ExchangeUM>> {
return swapTransactionRepository.getTransactions(
userWallet = userWallet,
cryptoCurrencyId = cryptoCurrency.id,

View file

@ -65,14 +65,12 @@ internal class ExpressStatusFactory @AssistedInject constructor(
)
}
suspend fun getExpressStatuses(): Flow<PersistentList<ExpressTransactionStateUM>> = combine(
fun getExpressStatuses(): Flow<PersistentList<ExpressTransactionStateUM>> = combine(
flow = exchangeStatusFactory(),
flow2 = onrampStatusFactory(),
) { maybeExchange, maybeOnramp ->
persistentListOf(
maybeOnramp,
maybeExchange,
).flatten()
persistentListOf(maybeOnramp, maybeExchange)
.flatten()
.sortedByDescending { it.info.timestamp }
.toPersistentList()
}