Updated on 2026-08-14

This commit is contained in:
Tangem 2025-07-15 16:11:50 +04:00
commit dbe235560b
428 changed files with 10165 additions and 3907 deletions

View file

@ -5,19 +5,14 @@ import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.arkivanov.essenty.lifecycle.subscribe
import com.tangem.common.routing.RoutingFeatureToggle
import com.tangem.core.decompose.context.AppComponentContext
import com.tangem.core.decompose.context.child
import com.tangem.core.decompose.model.getOrCreateModel
import com.tangem.core.deeplink.DeepLinksRegistry
import com.tangem.core.deeplink.global.BuyCurrencyDeepLink
import com.tangem.core.deeplink.utils.registerDeepLinks
import com.tangem.core.ui.components.NavigationBar3ButtonsScrim
import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.feature.tokendetails.presentation.tokendetails.model.TokenDetailsModel
import com.tangem.feature.tokendetails.presentation.tokendetails.ui.TokenDetailsScreen
import com.tangem.features.markets.token.block.TokenMarketBlockComponent
import com.tangem.features.onramp.OnrampFeatureToggles
import com.tangem.features.tokendetails.TokenDetailsComponent
import com.tangem.features.txhistory.component.TxHistoryComponent
import dagger.assisted.Assisted
@ -30,9 +25,6 @@ internal class DefaultTokenDetailsComponent @AssistedInject constructor(
@Assisted params: TokenDetailsComponent.Params,
tokenMarketBlockComponentFactory: TokenMarketBlockComponent.Factory,
txHistoryComponentFactory: TxHistoryComponent.Factory,
onrampFeatureToggles: OnrampFeatureToggles,
routingFeatureToggle: RoutingFeatureToggle,
deepLinksRegistry: DeepLinksRegistry,
) : TokenDetailsComponent, AppComponentContext by appComponentContext {
private val model: TokenDetailsModel = getOrCreateModel(params)
@ -50,23 +42,6 @@ internal class DefaultTokenDetailsComponent @AssistedInject constructor(
onPause = model::onPause,
onResume = model::onResume,
)
if (!routingFeatureToggle.isDeepLinkNavigationEnabled) {
val deeplinks = buildList {
if (!onrampFeatureToggles.isFeatureEnabled) {
add(
BuyCurrencyDeepLink(
onReceive = model::onBuyCurrencyDeepLink,
),
)
}
}
registerDeepLinks(
registry = deepLinksRegistry,
deepLinks = deeplinks,
)
}
}
private val tokenMarketBlockComponent = params.currency.toTokenMarketParam()?.let { tokenMarketParams ->

View file

@ -1,20 +1,29 @@
package com.tangem.feature.tokendetails.deeplink
import arrow.core.getOrElse
import com.tangem.common.routing.AppRoute
import com.tangem.common.routing.AppRouter
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
import com.tangem.domain.notifications.models.NotificationType
import com.tangem.domain.tokens.FetchCurrencyStatusUseCase
import com.tangem.domain.tokens.GetCryptoCurrenciesUseCase
import com.tangem.domain.tokens.*
import com.tangem.domain.tokens.wallet.WalletBalanceFetcher
import com.tangem.domain.wallets.models.UserWallet
import com.tangem.domain.wallets.models.UserWalletId
import com.tangem.domain.wallets.usecase.GetSelectedWalletSyncUseCase
import com.tangem.domain.wallets.models.isLocked
import com.tangem.domain.wallets.models.isMultiCurrency
import com.tangem.domain.wallets.usecase.GetUserWalletUseCase
import com.tangem.domain.wallets.usecase.SelectWalletUseCase
import com.tangem.features.pushnotifications.api.analytics.PushNotificationAnalyticEvents
import com.tangem.features.tokendetails.deeplink.TokenDetailsDeepLinkHandler
import com.tangem.features.wallet.deeplink.WalletDeepLinkActionTrigger
import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
@ -28,10 +37,17 @@ internal class DefaultTokenDetailsDeepLinkHandler @AssistedInject constructor(
@Assisted private val queryParams: Map<String, String>,
@Assisted private val isFromOnNewIntent: Boolean,
private val appRouter: AppRouter,
private val getSelectedWalletSyncUseCase: GetSelectedWalletSyncUseCase,
private val selectWalletUseCase: SelectWalletUseCase,
private val getCryptoCurrenciesUseCase: GetCryptoCurrenciesUseCase,
private val getCryptoCurrencyUseCase: GetCryptoCurrencyUseCase,
private val fetchCurrencyStatusUseCase: FetchCurrencyStatusUseCase,
private val tokenDetailsDeepLinkActionTrigger: TokenDetailsDeepLinkActionTrigger,
private val walletDeepLinkActionTrigger: WalletDeepLinkActionTrigger,
private val analyticsEventHandler: AnalyticsEventHandler,
private val getUserWalletUseCase: GetUserWalletUseCase,
private val tokensFeatureToggles: TokensFeatureToggles,
private val walletBalanceFetcher: WalletBalanceFetcher,
private val fetchCardTokenListUseCase: FetchCardTokenListUseCase,
) : TokenDetailsDeepLinkHandler {
init {
@ -42,27 +58,23 @@ internal class DefaultTokenDetailsDeepLinkHandler @AssistedInject constructor(
val networkId = queryParams[NETWORK_ID_KEY]
val tokenId = queryParams[TOKEN_ID_KEY]
val type = NotificationType.getType(queryParams[TYPE_KEY])
// It is okay here, we are navigating from outside, and there is no other way to getting UserWallet
val selectedUserWalletId = getSelectedWalletSyncUseCase().getOrNull()?.walletId
val walletId = queryParams[WALLET_ID_KEY]?.let(::UserWalletId) ?: selectedUserWalletId
// If selected user wallet is different than from deeplink - ignore deeplink
// If selected user wallet is null - ignore deeplink
if (walletId != selectedUserWalletId || selectedUserWalletId == null) {
Timber.e("Error on getting user wallet")
return
}
val transactionId = queryParams[TRANSACTION_ID_KEY]
val walletId = queryParams[WALLET_ID_KEY]
scope.launch {
val cryptoCurrency = getCryptoCurrenciesUseCase(userWalletId = selectedUserWalletId).getOrElse {
Timber.e("Error on getting crypto currency list")
val userWalletId = walletId?.let(::UserWalletId)
val userWallet = userWalletId?.let { getUserWalletUseCase(userWalletId) }?.getOrNull()
// If wallet to select is null or locked, ignore deeplink
if (userWallet == null || userWallet.isLocked) {
Timber.e("Error on getting user wallet")
return@launch
}.firstOrNull {
val isNetwork = it.network.backendId.equals(networkId, ignoreCase = true)
val isCurrency = it.id.rawCurrencyId?.value?.equals(tokenId, ignoreCase = true) == true
isNetwork && isCurrency
}
if (selectWalletUseCase(userWalletId).getOrNull() == null) {
Timber.e("Error on selecting user wallet")
return@launch
}
val cryptoCurrency = getCryptoCurrency(userWallet = userWallet, networkId = networkId, tokenId = tokenId)
if (cryptoCurrency == null) {
Timber.e(
@ -77,23 +89,78 @@ internal class DefaultTokenDetailsDeepLinkHandler @AssistedInject constructor(
analyticsEventHandler.send(PushNotificationAnalyticEvents.NotificationOpened(type.name))
appRouter.push(
AppRoute.CurrencyDetails(
userWalletId = selectedUserWalletId,
currency = cryptoCurrency,
),
)
if (isFromOnNewIntent) {
fetchCurrencyStatusUseCase.invoke(
userWalletId = selectedUserWalletId,
id = cryptoCurrency.id,
refresh = true,
if (userWallet.isMultiCurrency) {
appRouter.push(
route = AppRoute.CurrencyDetails(
userWalletId = userWallet.walletId,
currency = cryptoCurrency,
),
onComplete = { walletDeepLinkActionTrigger.selectWallet(userWallet.walletId) },
)
} else {
walletDeepLinkActionTrigger.selectWallet(userWallet.walletId)
}
if (transactionId != null) {
when (type) {
NotificationType.SwapStatus,
NotificationType.OnrampStatus,
-> tokenDetailsDeepLinkActionTrigger.trigger(transactionId)
NotificationType.Promo,
NotificationType.IncomeTransactions,
NotificationType.Unknown,
-> Unit
}
}
if (isFromOnNewIntent) fetchCurrency(userWallet, cryptoCurrency)
}
}
private suspend fun fetchCurrency(userWallet: UserWallet, cryptoCurrency: CryptoCurrency) {
val isMultiCurrency = userWallet.isMultiCurrency
// single-currency wallet with token (NODL)
val isSingleWalletWithToken = userWallet is UserWallet.Cold &&
userWallet.scanResponse.cardTypesResolver.isSingleWalletWithToken()
when {
isMultiCurrency -> fetchCurrencyStatusUseCase.invoke(
userWalletId = userWallet.walletId,
id = cryptoCurrency.id,
refresh = true,
)
!isMultiCurrency && tokensFeatureToggles.isWalletBalanceFetcherEnabled ->
walletBalanceFetcher(params = WalletBalanceFetcher.Params(userWalletId = userWallet.walletId))
// remove below after delete tokensFeatureToggles.isWalletBalanceFetcherEnabled
!isMultiCurrency && userWallet is UserWallet.Cold && isSingleWalletWithToken ->
fetchCardTokenListUseCase.invoke(
userWalletId = userWallet.walletId,
refresh = true,
)
!isMultiCurrency -> fetchCurrencyStatusUseCase.invoke(
userWalletId = userWallet.walletId,
refresh = true,
)
}
}
private suspend fun getCryptoCurrency(userWallet: UserWallet, networkId: String?, tokenId: String?) =
if (userWallet.isMultiCurrency) {
val derivationPath = queryParams[DERIVATION_PATH_KEY]
getCryptoCurrenciesUseCase(userWalletId = userWallet.walletId)
.getOrNull()
?.firstOrNull {
val isNetwork = it.network.backendId.equals(networkId, ignoreCase = true)
val isCurrency = it.id.rawCurrencyId?.value?.equals(tokenId, ignoreCase = true) == true
val isDefaultDerivation = it.network.derivationPath is Network.DerivationPath.Card
val isCustomDerivation = derivationPath?.equals(it.network.derivationPath.value) == true
val isCorrectDerivation = isDefaultDerivation || isCustomDerivation
isNetwork && isCurrency && isCorrectDerivation
}
} else {
getCryptoCurrencyUseCase(userWalletId = userWallet.walletId).getOrNull()
}
@AssistedFactory
interface Factory : TokenDetailsDeepLinkHandler.Factory {
override fun create(

View file

@ -0,0 +1,27 @@
package com.tangem.feature.tokendetails.deeplink
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.SharedFlow
import javax.inject.Inject
import javax.inject.Singleton
interface TokenDetailsDeepLinkActionTrigger {
suspend fun trigger(txId: String)
}
interface TokenDetailsDeepLinkActionListener {
val tokenDetailsActionFlow: SharedFlow<String>
}
@Singleton
internal class DefaultTokenDetailsDeepLinkActionTrigger @Inject constructor() :
TokenDetailsDeepLinkActionTrigger,
TokenDetailsDeepLinkActionListener {
override val tokenDetailsActionFlow: SharedFlow<String>
field = MutableSharedFlow<String>()
override suspend fun trigger(txId: String) {
tokenDetailsActionFlow.emit(txId)
}
}

View file

@ -1,6 +1,9 @@
package com.tangem.feature.tokendetails.deeplink.di
import com.tangem.feature.tokendetails.deeplink.DefaultTokenDetailsDeepLinkActionTrigger
import com.tangem.feature.tokendetails.deeplink.DefaultTokenDetailsDeepLinkHandler
import com.tangem.feature.tokendetails.deeplink.TokenDetailsDeepLinkActionListener
import com.tangem.feature.tokendetails.deeplink.TokenDetailsDeepLinkActionTrigger
import com.tangem.features.tokendetails.deeplink.TokenDetailsDeepLinkHandler
import dagger.Binds
import dagger.Module
@ -17,4 +20,16 @@ internal interface TokenDetailsDeepLinkModule {
fun bindWalletDeepLinkHandlerFactory(
impl: DefaultTokenDetailsDeepLinkHandler.Factory,
): TokenDetailsDeepLinkHandler.Factory
@Binds
@Singleton
fun bindTokenDetailsDeepLinkActionTrigger(
impl: DefaultTokenDetailsDeepLinkActionTrigger,
): TokenDetailsDeepLinkActionTrigger
@Binds
@Singleton
fun bindTokenDetailsDeepLinkActionListener(
impl: DefaultTokenDetailsDeepLinkActionTrigger,
): TokenDetailsDeepLinkActionListener
}

View file

@ -73,6 +73,7 @@ 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
import com.tangem.feature.tokendetails.presentation.router.InnerTokenDetailsRouter
import com.tangem.feature.tokendetails.presentation.tokendetails.analytics.TokenDetailsCurrencyStatusAnalyticsSender
import com.tangem.feature.tokendetails.presentation.tokendetails.analytics.TokenDetailsNotificationsAnalyticsSender
@ -80,7 +81,6 @@ import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenBala
import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenDetailsState
import com.tangem.feature.tokendetails.presentation.tokendetails.state.factory.TokenDetailsStateFactory
import com.tangem.feature.tokendetails.presentation.tokendetails.state.factory.express.ExpressStatusFactory
import com.tangem.features.onramp.OnrampFeatureToggles
import com.tangem.features.tokendetails.TokenDetailsComponent
import com.tangem.features.tokendetails.impl.R
import com.tangem.features.txhistory.entity.TxHistoryContentUpdateEmitter
@ -127,7 +127,6 @@ internal class TokenDetailsModel @Inject constructor(
private val analyticsEventsHandler: AnalyticsEventHandler,
private val vibratorHapticManager: VibratorHapticManager,
private val clipboardManager: ClipboardManager,
private val onrampFeatureToggles: OnrampFeatureToggles,
private val shareManager: ShareManager,
@GlobalUiMessageSender private val uiMessageSender: UiMessageSender,
private val txHistoryContentUpdateEmitter: TxHistoryContentUpdateEmitter,
@ -137,6 +136,7 @@ internal class TokenDetailsModel @Inject constructor(
getStakingIntegrationIdUseCase: GetStakingIntegrationIdUseCase,
private val appRouter: AppRouter,
private val router: InnerTokenDetailsRouter,
private val tokenDetailsDeepLinkActionListener: TokenDetailsDeepLinkActionListener,
private val analyticsExceptionHandler: AnalyticsExceptionHandler,
) : Model(), TokenDetailsClickIntents {
@ -157,6 +157,9 @@ internal class TokenDetailsModel @Inject constructor(
private var cryptoCurrencyStatus: CryptoCurrencyStatus? = null
private var expressTxStatusTaskScheduler = SingleTaskScheduler<PersistentList<ExpressTransactionStateUM>>()
/** Transaction id to check for status */
private val waitForFirstExpressStatusEmmit = MutableStateFlow(false)
private val stateFactory = TokenDetailsStateFactory(
currentStateProvider = Provider { uiState.value },
appCurrencyProvider = Provider(selectedAppCurrencyFlow::value),
@ -203,11 +206,7 @@ internal class TokenDetailsModel @Inject constructor(
initButtons()
updateContent()
handleBalanceHiding()
}
fun onBuyCurrencyDeepLink() {
val currency = cryptoCurrencyStatus?.currency ?: return
analyticsEventsHandler.send(TokenScreenAnalyticsEvent.Bought(currency.symbol))
checkForActionUpdates()
}
fun onResume() {
@ -335,6 +334,7 @@ internal class TokenDetailsModel @Inject constructor(
expressTxStatusTaskScheduler.cancelTask()
expressStatusFactory.getExpressStatuses()
.distinctUntilChanged()
.onEach { waitForFirstExpressStatusEmmit.value = true }
.onEach { expressTxs ->
internalUiState.value = expressStatusFactory.getStateWithUpdatedExpressTxs(
expressTxs = expressTxs,
@ -516,30 +516,14 @@ internal class TokenDetailsModel @Inject constructor(
}
val status = cryptoCurrencyStatus ?: return
if (onrampFeatureToggles.isFeatureEnabled) {
modelScope.launch(dispatchers.main) {
reduxStateHolder.dispatch(
TradeCryptoAction.Buy(
userWallet = userWallet,
source = OnrampSource.TOKEN_DETAILS,
cryptoCurrencyStatus = status,
appCurrencyCode = selectedAppCurrencyFlow.value.code,
),
)
}
} else {
showErrorIfDemoModeOrElse {
modelScope.launch(dispatchers.main) {
reduxStateHolder.dispatch(
TradeCryptoAction.Buy(
userWallet = userWallet,
source = OnrampSource.TOKEN_DETAILS,
cryptoCurrencyStatus = status,
appCurrencyCode = selectedAppCurrencyFlow.value.code,
),
)
}
}
modelScope.launch {
appRouter.push(
AppRoute.Onramp(
userWalletId = userWallet.walletId,
currency = status.currency,
source = OnrampSource.TOKEN_DETAILS,
),
)
}
}
@ -828,7 +812,8 @@ internal class TokenDetailsModel @Inject constructor(
}
override fun onExpressTransactionClick(txId: String) {
val expressTxState = internalUiState.value.expressTxsToDisplay.first { it.info.txId == txId }
val expressTxState = internalUiState.value.expressTxsToDisplay.firstOrNull { it.info.txId == txId }
?: return
internalUiState.value = expressStatusFactory.getStateWithExpressStatusBottomSheet(expressTxState)
}
@ -1069,6 +1054,15 @@ internal class TokenDetailsModel @Inject constructor(
}
}
private fun checkForActionUpdates() {
combine(
tokenDetailsDeepLinkActionListener.tokenDetailsActionFlow,
waitForFirstExpressStatusEmmit.filter { it },
) { transactionId, _ -> transactionId }
.onEach(::onExpressTransactionClick)
.launchIn(modelScope)
}
private companion object {
const val EXPRESS_STATUS_UPDATE_DELAY = 10_000L
}