Updated on 2026-08-14
This commit is contained in:
commit
a56bcff7dd
438 changed files with 9017 additions and 4452 deletions
|
|
@ -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))
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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 }
|
||||
|
||||
|
|
|
|||
|
|
@ -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()) {
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue