Updated on 2026-08-14

This commit is contained in:
Tangem 2025-07-14 16:11:08 +03:00
commit aed332c720
44 changed files with 514 additions and 351 deletions

View file

@ -224,4 +224,12 @@ internal object StakingDomainModule {
): CheckAccountInitializedUseCase {
return CheckAccountInitializedUseCase(walletManagersFacade)
}
@Provides
@Singleton
fun provideGetActionRequirementAmountUseCase(
stakingRepository: StakingRepository,
): GetActionRequirementAmountUseCase {
return GetActionRequirementAmountUseCase(stakingRepository)
}
}

View file

@ -22,7 +22,6 @@ internal class DefaultCardSettingsComponent @AssistedInject constructor(
@Composable
override fun Content(modifier: Modifier) {
val state by model.screenState.collectAsStateWithLifecycle()
CardSettingsScreen(modifier = modifier, state = state)
}

View file

@ -74,6 +74,8 @@ internal class CardSettingsModel @Inject constructor(
override fun onDestroy() {
super.onDestroy()
// Reset card scanned data
cardSettingsInteractor.clear()
// Restore the previous value of access code request policy
cardSdkConfigRepository.isBiometricsRequestPolicy = previousBiometricsRequestPolicy
}

View file

@ -33,10 +33,10 @@ data class OnrampQuoteResponse(
val providerId: String,
@Json(name = "minFromAmount")
val minFromAmount: String,
val minFromAmount: String?,
@Json(name = "maxFromAmount")
val maxFromAmount: String,
val maxFromAmount: String?,
@Json(name = "minToAmount")
val minToAmount: String?,

View file

@ -52,7 +52,6 @@ import kotlinx.coroutines.async
import kotlinx.coroutines.awaitAll
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.mapNotNull
import kotlinx.coroutines.withContext
import org.joda.time.DateTime
import timber.log.Timber
@ -303,8 +302,12 @@ internal class DefaultOnrampRepository(
OnrampQuote.Data(
fromAmount = fromOnrampAmount,
toAmount = convertToAmount(response.toAmount, cryptoCurrency),
minFromAmount = convertToAmount(response.minFromAmount, cryptoCurrency),
maxFromAmount = convertToAmount(response.maxFromAmount, cryptoCurrency),
minFromAmount = response.minFromAmount?.let {
convertToAmount(it, cryptoCurrency)
},
maxFromAmount = response.maxFromAmount?.let {
convertToAmount(it, cryptoCurrency)
},
paymentMethod = paymentMethod,
provider = provider,
countryCode = response.countryCode,

View file

@ -72,6 +72,7 @@ import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import kotlinx.coroutines.withTimeoutOrNull
import timber.log.Timber
import java.math.BigDecimal
import kotlin.time.Duration.Companion.seconds
@Suppress("LargeClass", "LongParameterList", "TooManyFunctions")
@ -119,7 +120,7 @@ internal class DefaultStakingRepository(
when (val stakingTokensWithYields = stakeKitApi.getEnabledYields(preferredValidatorsOnly = false)) {
is ApiResponse.Success -> stakingYieldsStore.store(
stakingTokensWithYields.data.data.filter {
it.isAvailable ?: false
it.isAvailable == true
},
)
else -> {
@ -650,15 +651,24 @@ internal class DefaultStakingRepository(
}
override suspend fun isAnyTokenStaked(userWalletId: UserWalletId): Boolean {
return withContext(dispatchers.io) {
stakingBalanceStore.getSyncOrNull(userWalletId)
?.let {
it.isNotEmpty() &&
it.any { yieldBalance ->
(yieldBalance as? YieldBalance.Data)?.balance?.items?.isNotEmpty() == true
}
return withContext(dispatchers.default) {
val balances = stakingBalanceStoreV2.getAllSyncOrNull(userWalletId) ?: return@withContext false
val hasDataYieldBalance by lazy {
balances.any { yieldBalance ->
(yieldBalance as? YieldBalance.Data)?.balance?.items?.isNotEmpty() == true
}
?: false
}
balances.isNotEmpty() && hasDataYieldBalance
}
}
override fun getActionRequirementAmount(integrationId: String, stakingActionType: StakingActionType): BigDecimal? {
return when {
stakingIdFactory.isPolygonIntegrationId(integrationId) &&
stakingActionType == StakingActionType.CLAIM_REWARDS -> BigDecimal.ONE
else -> null
}
}

View file

@ -1,5 +1,7 @@
package com.tangem.data.staking.single
import com.tangem.core.analytics.api.AnalyticsExceptionHandler
import com.tangem.core.analytics.models.ExceptionAnalyticsEvent
import com.tangem.data.staking.utils.StakingIdFactory
import com.tangem.domain.staking.model.StakingID
import com.tangem.domain.staking.model.stakekit.YieldBalance
@ -7,6 +9,7 @@ import com.tangem.domain.staking.multi.MultiYieldBalanceProducer
import com.tangem.domain.staking.multi.MultiYieldBalanceSupplier
import com.tangem.domain.staking.single.SingleYieldBalanceProducer
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import com.tangem.utils.extensions.indexOfFirstOrNull
import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
@ -14,6 +17,7 @@ import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.flow.mapNotNull
import timber.log.Timber
/**
* Default implementation of [SingleYieldBalanceProducer]
@ -29,6 +33,7 @@ internal class DefaultSingleYieldBalanceProducer @AssistedInject constructor(
@Assisted private val params: SingleYieldBalanceProducer.Params,
private val multiYieldBalanceSupplier: MultiYieldBalanceSupplier,
private val stakingIdFactory: StakingIdFactory,
private val analyticsExceptionHandler: AnalyticsExceptionHandler,
private val dispatchers: CoroutineDispatcherProvider,
) : SingleYieldBalanceProducer {
@ -48,8 +53,34 @@ internal class DefaultSingleYieldBalanceProducer @AssistedInject constructor(
.mapNotNull { balances ->
val currentStakingId = getStakingId() ?: return@mapNotNull YieldBalance.Unsupported
balances.firstOrNull { it.getStakingId() == currentStakingId }
?: YieldBalance.Unsupported
val currentBalances = balances.filter { it.getStakingId() == currentStakingId }
if (currentBalances.size > 1) {
analyticsExceptionHandler.sendException(
event = ExceptionAnalyticsEvent(
exception = IllegalStateException("Multiple balances found for staking ID"),
params = mapOf(
"stakingId" to currentStakingId.toString(),
"balances" to currentBalances.joinToString(",") { it.toString() },
),
),
)
Timber.w(
"Multiple balances found for staking ID $currentStakingId:\n%s",
currentBalances.joinToString("\n"),
)
val dataIndex = currentBalances.indexOfFirstOrNull { it is YieldBalance.Data }
if (dataIndex != null) {
currentBalances[dataIndex]
} else {
currentBalances.first()
}
} else {
currentBalances.firstOrNull() ?: YieldBalance.Unsupported
}
}
.distinctUntilChanged()
.flowOn(dispatchers.default)

View file

@ -109,8 +109,15 @@ internal class DefaultYieldsBalancesStore(
private suspend fun storeInPersistence(userWalletId: UserWalletId, values: Set<YieldBalanceWrapperDTO>) {
persistenceStore.updateData { current ->
current.toMutableMap().apply {
this[userWalletId.stringValue] = current[userWalletId.stringValue]
?.addOrReplace(items = values) { old, new -> old.getStakingId() == new.getStakingId() }
this[userWalletId.stringValue] = this[userWalletId.stringValue]
?.addOrReplace(items = values) { old, new ->
val oldId = old.getStakingId()
val newId = new.getStakingId()
if (oldId == null || newId == null) return@addOrReplace false
oldId == newId
}
?: values
}
}

View file

@ -35,6 +35,8 @@ internal class StakingIdFactory @Inject constructor(
return integrationIdMap[integrationKey]
}
fun isPolygonIntegrationId(integrationId: String): Boolean = integrationId == ETHEREUM_POLYGON_INTEGRATION_ID
@Suppress("UnusedPrivateMember", "unused")
companion object {

View file

@ -5,6 +5,7 @@ import com.tangem.blockchain.common.Blockchain
import com.tangem.common.test.data.staking.MockYieldBalanceWrapperDTOFactory
import com.tangem.common.test.domain.token.MockCryptoCurrencyFactory
import com.tangem.common.test.utils.getEmittedValues
import com.tangem.core.analytics.api.AnalyticsExceptionHandler
import com.tangem.data.staking.toDomain
import com.tangem.data.staking.utils.StakingIdFactory
import com.tangem.domain.staking.model.StakingID
@ -32,12 +33,14 @@ internal class DefaultSingleYieldBalanceProducerTest {
private val multiNetworkStatusSupplier = mockk<MultiYieldBalanceSupplier>()
private val stakingIdFactory = mockk<StakingIdFactory>()
private val analyticsExceptionHandler = mockk<AnalyticsExceptionHandler>(relaxUnitFun = true)
private val dispatchers = TestingCoroutineDispatcherProvider()
private val producer = DefaultSingleYieldBalanceProducer(
params = params,
stakingIdFactory = stakingIdFactory,
multiYieldBalanceSupplier = multiNetworkStatusSupplier,
analyticsExceptionHandler = analyticsExceptionHandler,
dispatchers = dispatchers,
)

View file

@ -97,31 +97,35 @@ internal class DefaultCurrenciesRepository(
}
}
override suspend fun addCurrencies(userWalletId: UserWalletId, currencies: List<CryptoCurrency>) {
withContext(dispatchers.io) {
val savedCurrencies = requireNotNull(
value = getSavedUserTokensResponseSync(key = userWalletId),
lazyMessage = { "Saved tokens empty. Can not perform add currencies action" },
)
override suspend fun addCurrencies(
userWalletId: UserWalletId,
currencies: List<CryptoCurrency>,
): List<CryptoCurrency> = withContext(dispatchers.io) {
val savedCurrencies = requireNotNull(
value = getSavedUserTokensResponseSync(key = userWalletId),
lazyMessage = { "Saved tokens empty. Can not perform add currencies action" },
)
val currenciesToAdd = populateCurrenciesWithMissedCoins(
currencies = currencies,
).let {
filterAlreadyAddedCurrencies(savedCurrencies.tokens, it)
}
val updatedResponse = savedCurrencies.copy(
tokens = savedCurrencies.tokens + currenciesToAdd.map(userTokensResponseFactory::createResponseToken),
)
userTokensSaver.storeAndPush(
userWalletId = userWalletId,
response = updatedResponse,
)
val currenciesToAdd = filterAlreadyAddedCurrencies(
savedCurrencies = savedCurrencies.tokens,
currenciesToAdd = populateCurrenciesWithMissedCoins(currencies = currencies),
)
fetchExpressAssetsByNetworkIds(
userWallet = userWalletsStore.getSyncStrict(key = userWalletId),
userTokens = updatedResponse,
)
}
val updatedResponse = savedCurrencies.copy(
tokens = savedCurrencies.tokens + currenciesToAdd.map(userTokensResponseFactory::createResponseToken),
)
userTokensSaver.storeAndPush(
userWalletId = userWalletId,
response = updatedResponse,
)
fetchExpressAssetsByNetworkIds(
userWallet = userWalletsStore.getSyncStrict(key = userWalletId),
userTokens = updatedResponse,
)
currenciesToAdd
}
private fun filterAlreadyAddedCurrencies(
@ -573,7 +577,7 @@ internal class DefaultCurrenciesRepository(
override fun isNetworkFeeZero(userWalletId: UserWalletId, network: Network): Boolean {
val blockchain = Blockchain.fromNetworkId(network.backendId)
return blockchain?.isNetworkFeeZero() ?: false
return blockchain?.isNetworkFeeZero() == true
}
override suspend fun syncTokens(userWalletId: UserWalletId) {

View file

@ -1,6 +1,7 @@
package com.tangem.domain.card
import arrow.core.Either
import arrow.core.right
import com.tangem.blockchain.common.Blockchain
import com.tangem.blockchainsdk.utils.toBlockchain
import com.tangem.common.extensions.ByteArrayKey
@ -22,16 +23,17 @@ class GetExtendedPublicKeyForCurrencyUseCase(
private val derivationsRepository: DerivationsRepository,
private val walletManagersFacade: WalletManagersFacade,
) {
suspend operator fun invoke(userWalletId: UserWalletId, network: Network): Either<Throwable, String> {
return Either.catch {
val userWallet = walletManagersFacade.getOrCreateWalletManager(userWalletId, network)
?: error("Wallet not found")
val walletManager = walletManagersFacade.getOrCreateWalletManager(userWalletId, network)
?: error("Wallet not found for userWalletId=$userWalletId and network=$network")
val blockchain = network.toBlockchain()
val isSecp256k1Blockchain = Blockchain.secp256k1Blockchains(network.isTestnet).contains(blockchain)
val hdKey = if (isSecp256k1Blockchain) {
userWallet.wallet.publicKey.derivationType?.hdKey ?: error("No derivation found")
walletManager.wallet.publicKey.derivationType?.hdKey ?: error("No derivation found")
} else {
error("No derivation found")
}
@ -50,7 +52,7 @@ class GetExtendedPublicKeyForCurrencyUseCase(
val pendingDerivations = getPendingDerivations(childKey, parentKey)
val derivedKeys = deriveKeys(
userWalletId = userWalletId,
seedKey = userWallet.wallet.publicKey.seedKey,
seedKey = walletManager.wallet.publicKey.seedKey,
paths = pendingDerivations,
)
@ -73,15 +75,17 @@ class GetExtendedPublicKeyForCurrencyUseCase(
/**
* @return true if xpub generation is supported, false otherwise
*/
suspend fun isSupported(userWalletId: UserWalletId, network: Network): Boolean {
val userWallet = walletManagersFacade.getOrCreateWalletManager(userWalletId, network)
?: error("Wallet not found")
suspend fun isSupported(userWalletId: UserWalletId, network: Network): Either<Throwable, Boolean> = Either.catch {
val walletManager = walletManagersFacade.getOrCreateWalletManager(userWalletId, network)
?: error("Wallet not found for user wallet $userWalletId and network ${network.id}")
val blockchain = network.toBlockchain()
val isSecp256k1Blockchain = Blockchain.secp256k1Blockchains(network.isTestnet).contains(blockchain)
val isHdKey = userWallet.wallet.publicKey.derivationType?.hdKey
val isHdKey = walletManager.wallet.publicKey.derivationType?.hdKey
return isSecp256k1Blockchain && isHdKey != null
val isSupported = isSecp256k1Blockchain && isHdKey != null
return isSupported.right()
}
private suspend fun deriveKeys(

View file

@ -41,16 +41,15 @@ class SaveMarketTokensUseCase(
removedNetworks: Set<TokenMarketInfo.Network>,
): Either<Throwable, Unit> = Either.catch {
if (removedNetworks.isNotEmpty()) {
currenciesRepository.removeCurrencies(
userWalletId = userWalletId,
currencies = removedNetworks.mapNotNull {
marketsTokenRepository.createCryptoCurrency(
userWalletId = userWalletId,
token = tokenMarketParams,
network = it,
)
},
)
val removedCurrencies = removedNetworks.mapNotNull {
marketsTokenRepository.createCryptoCurrency(
userWalletId = userWalletId,
token = tokenMarketParams,
network = it,
)
}
currenciesRepository.removeCurrencies(userWalletId = userWalletId, currencies = removedCurrencies)
}
if (addedNetworks.isNotEmpty()) {
@ -67,13 +66,16 @@ class SaveMarketTokensUseCase(
)
}
currenciesRepository.addCurrencies(userWalletId = userWalletId, currencies = addedCurrencies)
val savedCurrencies = currenciesRepository.addCurrencies(
userWalletId = userWalletId,
currencies = addedCurrencies,
)
refreshUpdatedNetworks(userWalletId, addedCurrencies)
refreshUpdatedNetworks(userWalletId, savedCurrencies)
refreshUpdatedYieldBalances(userWalletId, addedCurrencies)
refreshUpdatedYieldBalances(userWalletId, savedCurrencies)
refreshUpdatedQuotes(addedCurrencies)
refreshUpdatedQuotes(savedCurrencies)
}
}

View file

@ -15,8 +15,8 @@ sealed class OnrampQuote {
override val fromAmount: OnrampAmount,
override val countryCode: String,
val toAmount: OnrampAmount,
val minFromAmount: OnrampAmount,
val maxFromAmount: OnrampAmount,
val minFromAmount: OnrampAmount?,
val maxFromAmount: OnrampAmount?,
) : OnrampQuote()
data class AmountError(

View file

@ -0,0 +1,16 @@
package com.tangem.domain.staking
import arrow.core.Either
import com.tangem.domain.staking.model.stakekit.action.StakingActionType
import com.tangem.domain.staking.repositories.StakingRepository
import java.math.BigDecimal
class GetActionRequirementAmountUseCase(
private val stakingRepository: StakingRepository,
) {
operator fun invoke(integrationId: String, actionType: StakingActionType): Either<Throwable, BigDecimal?> =
Either.catch {
stakingRepository.getActionRequirementAmount(integrationId, actionType)
}
}

View file

@ -14,11 +14,13 @@ import com.tangem.domain.staking.model.stakekit.YieldBalance
import com.tangem.domain.staking.model.stakekit.YieldBalanceList
import com.tangem.domain.staking.model.stakekit.action.StakingAction
import com.tangem.domain.staking.model.stakekit.action.StakingActionStatus
import com.tangem.domain.staking.model.stakekit.action.StakingActionType
import com.tangem.domain.staking.model.stakekit.transaction.ActionParams
import com.tangem.domain.staking.model.stakekit.transaction.StakingGasEstimate
import com.tangem.domain.staking.model.stakekit.transaction.StakingTransaction
import com.tangem.domain.wallets.models.UserWalletId
import kotlinx.coroutines.flow.Flow
import java.math.BigDecimal
@Suppress("TooManyFunctions")
interface StakingRepository {
@ -100,4 +102,9 @@ interface StakingRepository {
fun getStakingApproval(cryptoCurrency: CryptoCurrency): StakingApproval
suspend fun isAnyTokenStaked(userWalletId: UserWalletId): Boolean
/**
* Return action requirement amount
*/
fun getActionRequirementAmount(integrationId: String, stakingActionType: StakingActionType): BigDecimal?
}

View file

@ -50,7 +50,7 @@ interface CurrenciesRepository {
* @throws DataError.UserWalletError.WrongUserWallet If single-currency user wallet
* ID provided.
*/
suspend fun addCurrencies(userWalletId: UserWalletId, currencies: List<CryptoCurrency>)
suspend fun addCurrencies(userWalletId: UserWalletId, currencies: List<CryptoCurrency>): List<CryptoCurrency>
/**
* Removes currency from a specific user wallet.

View file

@ -48,7 +48,10 @@ internal class MockCurrenciesRepository(
override suspend fun saveNewCurrenciesList(userWalletId: UserWalletId, currencies: List<CryptoCurrency>) = Unit
override suspend fun addCurrencies(userWalletId: UserWalletId, currencies: List<CryptoCurrency>) = Unit
override suspend fun addCurrencies(
userWalletId: UserWalletId,
currencies: List<CryptoCurrency>,
): List<CryptoCurrency> = emptyList()
override suspend fun removeCurrency(userWalletId: UserWalletId, currency: CryptoCurrency) {
removeCurrencyResult.onLeft { throw it }

View file

@ -188,6 +188,8 @@ class MockStakingRepository : StakingRepository {
override fun getStakingApproval(cryptoCurrency: CryptoCurrency): StakingApproval = StakingApproval.Empty
override suspend fun isAnyTokenStaked(userWalletId: UserWalletId): Boolean = false
override fun getActionRequirementAmount(integrationId: String, stakingActionType: StakingActionType): BigDecimal? =
null
private companion object {
val yield = Yield(

View file

@ -1,8 +1,8 @@
package com.tangem.features.nft.collections.ui
import androidx.activity.compose.BackHandler
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.*
import androidx.compose.material3.Scaffold
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import com.tangem.core.ui.components.appbar.AppBarWithBackButton
@ -17,31 +17,28 @@ import com.tangem.features.nft.impl.R
internal fun NFTCollections(state: NFTCollectionsStateUM, modifier: Modifier = Modifier) {
BackHandler(onBack = state.onBackClick)
Scaffold(
modifier = modifier,
containerColor = TangemTheme.colors.background.secondary,
topBar = {
AppBarWithBackButton(
modifier = Modifier.statusBarsPadding(),
onBackClick = state.onBackClick,
text = stringResourceSafe(id = R.string.nft_collections_title),
iconRes = R.drawable.ic_back_24,
)
},
content = { innerPadding ->
TangemPullToRefreshContainer(
config = state.pullToRefreshConfig,
modifier = Modifier
.padding(innerPadding)
.fillMaxSize(),
) {
when (val content = state.content) {
is NFTCollectionsUM.Content -> NFTCollectionsContent(content)
is NFTCollectionsUM.Empty -> NFTCollectionsEmpty(content)
is NFTCollectionsUM.Failed -> NFTCollectionsFailed(content)
is NFTCollectionsUM.Loading -> NFTCollectionsLoading(content)
}
Column(
modifier = modifier
.background(TangemTheme.colors.background.secondary),
) {
AppBarWithBackButton(
modifier = Modifier,
onBackClick = state.onBackClick,
text = stringResourceSafe(id = R.string.nft_collections_title),
iconRes = R.drawable.ic_back_24,
)
TangemPullToRefreshContainer(
config = state.pullToRefreshConfig,
modifier = Modifier
.fillMaxSize(),
) {
when (val content = state.content) {
is NFTCollectionsUM.Content -> NFTCollectionsContent(content)
is NFTCollectionsUM.Empty -> NFTCollectionsEmpty(content)
is NFTCollectionsUM.Failed -> NFTCollectionsFailed(content)
is NFTCollectionsUM.Loading -> NFTCollectionsLoading(content)
}
},
)
}
}
}

View file

@ -52,14 +52,14 @@ internal fun NFTCollectionsEmpty(state: NFTCollectionsUM.Empty, modifier: Modifi
color = TangemTheme.colors.text.secondary,
textAlign = TextAlign.Center,
)
PrimaryButton(
modifier = Modifier
.padding(top = TangemTheme.dimens.spacing48)
.widthIn(min = TangemTheme.dimens.size158),
text = stringResourceSafe(R.string.nft_collections_receive),
onClick = state.onReceiveClick,
)
}
PrimaryButton(
modifier = Modifier
.fillMaxWidth()
.align(Alignment.BottomCenter),
text = stringResourceSafe(R.string.nft_collections_receive),
onClick = state.onReceiveClick,
)
}
}

View file

@ -19,8 +19,8 @@ import com.tangem.features.nft.common.NFTRoute
@Composable
internal fun NFTContent(stackState: ChildStack<NFTRoute, ComposableContentComponent>) {
Column(
modifier = Modifier.Companion
.background(color = TangemTheme.colors.background.tertiary)
modifier = Modifier
.background(color = TangemTheme.colors.background.secondary)
.fillMaxSize()
.imePadding()
.systemBarsPadding(),

View file

@ -1,5 +1,6 @@
package com.tangem.features.nft.details.entity.transformer
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.extensions.stringReference
import com.tangem.core.ui.format.bigdecimal.crypto
import com.tangem.core.ui.format.bigdecimal.fiat
@ -8,6 +9,7 @@ import com.tangem.domain.appcurrency.model.AppCurrency
import com.tangem.domain.nft.models.NFTSalePrice
import com.tangem.features.nft.details.entity.NFTAssetUM
import com.tangem.features.nft.details.entity.NFTDetailsUM
import com.tangem.features.nft.impl.R
import com.tangem.utils.transformer.Transformer
internal class NFTPriceChangeTransformer(
@ -18,35 +20,50 @@ internal class NFTPriceChangeTransformer(
override fun transform(prevState: NFTDetailsUM): NFTDetailsUM {
val topInfo = prevState.nftAsset.topInfo as? NFTAssetUM.TopInfo.Content ?: return prevState
return prevState.copy(
nftAsset = prevState.nftAsset.copy(
topInfo = topInfo.copy(
salePrice = when (nftSalePrice) {
is NFTSalePrice.Empty,
is NFTSalePrice.Error,
-> NFTAssetUM.SalePrice.Empty
is NFTSalePrice.Loading -> NFTAssetUM.SalePrice.Loading
is NFTSalePrice.Value -> NFTAssetUM.SalePrice.Content(
isFlickering = false,
cryptoPrice = stringReference(
nftSalePrice.value.format {
crypto(
symbol = nftSalePrice.symbol,
decimals = nftSalePrice.decimals,
)
},
),
fiatPrice = stringReference(
nftSalePrice.fiatValue.format {
fiat(
fiatCurrencyCode = appCurrency.code,
fiatCurrencySymbol = appCurrency.symbol,
)
},
),
val salePrice = when (nftSalePrice) {
is NFTSalePrice.Empty,
is NFTSalePrice.Error,
-> NFTAssetUM.SalePrice.Empty
is NFTSalePrice.Loading -> NFTAssetUM.SalePrice.Loading
is NFTSalePrice.Value -> NFTAssetUM.SalePrice.Content(
isFlickering = false,
cryptoPrice = stringReference(
nftSalePrice.value.format {
crypto(
symbol = nftSalePrice.symbol,
decimals = nftSalePrice.decimals,
)
},
),
fiatPrice = stringReference(
nftSalePrice.fiatValue.format {
fiat(
fiatCurrencyCode = appCurrency.code,
fiatCurrencySymbol = appCurrency.symbol,
)
},
),
)
}
val hasSalePrice = salePrice !is NFTAssetUM.SalePrice.Empty
val newTopInfo = if (
topInfo.rarity is NFTAssetUM.Rarity.Empty &&
topInfo.description.isNullOrEmpty() &&
!hasSalePrice
) {
NFTAssetUM.TopInfo.Empty
} else {
topInfo.copy(
title = resourceReference(R.string.nft_details_last_sale_price).takeIf { hasSalePrice },
salePrice = salePrice,
)
}
return prevState.copy(
nftAsset = prevState.nftAsset.copy(
topInfo = newTopInfo,
),
)
}

View file

@ -1,13 +1,10 @@
package com.tangem.features.nft.details.ui
import androidx.activity.compose.BackHandler
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.statusBarsPadding
import androidx.compose.material3.FabPosition
import androidx.compose.material3.Scaffold
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.*
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import com.tangem.core.ui.components.PrimaryButton
import com.tangem.core.ui.components.appbar.TangemTopAppBar
@ -22,25 +19,25 @@ import com.tangem.features.nft.impl.R
internal fun NFTDetails(state: NFTDetailsUM, modifier: Modifier = Modifier) {
BackHandler(onBack = state.onBackClick)
Scaffold(
modifier = modifier,
containerColor = TangemTheme.colors.background.secondary,
topBar = {
Box(
modifier = modifier
.background(TangemTheme.colors.background.secondary),
) {
Column(
modifier = Modifier.fillMaxSize(),
) {
TangemTopAppBar(
modifier = Modifier.statusBarsPadding(),
modifier = Modifier,
startButton = TopAppBarButtonUM(
iconRes = R.drawable.ic_back_24,
onIconClicked = state.onBackClick,
),
title = state.nftAsset.name,
)
},
content = { innerPadding ->
TangemPullToRefreshContainer(
config = state.pullToRefreshConfig,
modifier = Modifier
.padding(innerPadding)
.fillMaxSize(),
modifier = Modifier.fillMaxSize(),
) {
NFTDetailsAsset(
state = state.nftAsset,
@ -49,16 +46,19 @@ internal fun NFTDetails(state: NFTDetailsUM, modifier: Modifier = Modifier) {
onExploreClick = state.onExploreClick,
)
}
},
floatingActionButtonPosition = FabPosition.Center,
floatingActionButton = {
PrimaryButton(
modifier = Modifier
.padding(horizontal = TangemTheme.dimens.spacing16)
.fillMaxWidth(),
text = stringResourceSafe(id = R.string.common_send),
onClick = state.onSendClick,
)
},
)
}
PrimaryButton(
modifier = Modifier
.align(Alignment.BottomCenter)
.padding(
start = TangemTheme.dimens.spacing16,
end = TangemTheme.dimens.spacing16,
bottom = TangemTheme.dimens.spacing16,
)
.fillMaxWidth(),
text = stringResourceSafe(id = R.string.common_send),
onClick = state.onSendClick,
)
}
}

View file

@ -173,8 +173,7 @@ internal fun NFTDetailsGroupBlock(
} else {
onBlockClick?.invoke()
}
}
.padding(top = TangemTheme.dimens.spacing4),
},
text = value.resolveReference(),
style = TangemTheme.typography.body1,
color = TangemTheme.colors.text.primary1,

View file

@ -1,8 +1,8 @@
package com.tangem.features.nft.receive.ui
import androidx.activity.compose.BackHandler
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.*
import androidx.compose.material3.Scaffold
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import com.tangem.common.ui.bottomsheet.receive.TokenReceiveBottomSheet
@ -22,41 +22,33 @@ import com.tangem.features.nft.receive.entity.NFTReceiveUM
internal fun NFTReceive(state: NFTReceiveUM, modifier: Modifier = Modifier) {
BackHandler(onBack = state.onBackClick)
Scaffold(
modifier = modifier,
containerColor = TangemTheme.colors.background.secondary,
topBar = {
TangemTopAppBar(
modifier = Modifier.statusBarsPadding(),
startButton = TopAppBarButtonUM(
iconRes = R.drawable.ic_close_24,
onIconClicked = state.onBackClick,
),
title = stringResourceSafe(id = R.string.nft_receive_title),
subtitle = state.appBarSubtitle.resolveReference(),
)
},
content = { innerPadding ->
Column(
modifier = Modifier
.padding(innerPadding)
.fillMaxSize(),
) {
SearchBar(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = TangemTheme.dimens.spacing16),
state = state.search,
colors = TangemSearchBarDefaults.secondaryTextFieldColors,
)
Column(
modifier = modifier
.background(TangemTheme.colors.background.secondary),
) {
TangemTopAppBar(
modifier = Modifier,
startButton = TopAppBarButtonUM(
iconRes = R.drawable.ic_close_24,
onIconClicked = state.onBackClick,
),
title = stringResourceSafe(id = R.string.nft_receive_title),
subtitle = state.appBarSubtitle.resolveReference(),
)
when (val networks = state.networks) {
is NFTReceiveUM.Networks.Content -> NFTReceiveNetworksContent(networks)
is NFTReceiveUM.Networks.Empty -> NFTReceiveNetworksEmpty()
}
}
},
)
SearchBar(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = TangemTheme.dimens.spacing16),
state = state.search,
colors = TangemSearchBarDefaults.secondaryTextFieldColors,
)
when (val networks = state.networks) {
is NFTReceiveUM.Networks.Content -> NFTReceiveNetworksContent(networks)
is NFTReceiveUM.Networks.Empty -> NFTReceiveNetworksEmpty()
}
}
ShowBottomSheet(state.bottomSheetConfig)
}

View file

@ -1,9 +1,8 @@
package com.tangem.features.nft.traits.ui
import androidx.activity.compose.BackHandler
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.statusBarsPadding
import androidx.compose.material3.Scaffold
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.*
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import com.tangem.core.ui.components.appbar.TangemTopAppBar
@ -17,25 +16,21 @@ import com.tangem.features.nft.traits.entity.NFTAssetTraitsUM
internal fun NFTAssetTraits(state: NFTAssetTraitsUM, modifier: Modifier = Modifier) {
BackHandler(onBack = state.onBackClick)
Scaffold(
modifier = modifier,
containerColor = TangemTheme.colors.background.secondary,
topBar = {
TangemTopAppBar(
modifier = Modifier.statusBarsPadding(),
startButton = TopAppBarButtonUM(
iconRes = R.drawable.ic_back_24,
onIconClicked = state.onBackClick,
),
title = stringResourceSafe(R.string.nft_traits_title),
)
},
content = { innerPadding ->
NFTAssetTraitsContent(
modifier = Modifier
.padding(innerPadding),
state = state,
)
},
)
Column(
modifier = modifier
.background(TangemTheme.colors.background.secondary),
) {
TangemTopAppBar(
modifier = Modifier,
startButton = TopAppBarButtonUM(
iconRes = R.drawable.ic_back_24,
onIconClicked = state.onBackClick,
),
title = stringResourceSafe(R.string.nft_traits_title),
)
NFTAssetTraitsContent(
state = state,
)
}
}

View file

@ -48,10 +48,14 @@ internal sealed class CommonSendAnalyticEvents(
data class TransactionError(
val categoryName: String,
val token: String,
val blockchain: String,
) : CommonSendAnalyticEvents(
category = categoryName,
event = "Error - Transaction Rejected",
params = mapOf(TOKEN_PARAM to token),
params = mapOf(
TOKEN_PARAM to token,
BLOCKCHAIN to blockchain,
),
)
/** Close button clicked */

View file

@ -404,6 +404,7 @@ internal class SendConfirmModel @Inject constructor(
CommonSendAnalyticEvents.TransactionError(
categoryName = analyticsCategoryName,
token = cryptoCurrency.symbol,
blockchain = cryptoCurrency.network.name,
),
)
},

View file

@ -312,6 +312,7 @@ internal class NFTSendConfirmModel @Inject constructor(
CommonSendAnalyticEvents.TransactionError(
categoryName = analyticsCategoryName,
token = cryptoCurrency.symbol,
blockchain = cryptoCurrency.network.name,
),
)
},

View file

@ -28,7 +28,6 @@ import com.tangem.domain.feedback.SaveBlockchainErrorUseCase
import com.tangem.domain.feedback.SendFeedbackEmailUseCase
import com.tangem.domain.feedback.models.BlockchainErrorInfo
import com.tangem.domain.feedback.models.FeedbackEmailType
import com.tangem.domain.models.StatusSource
import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.domain.staking.*
import com.tangem.domain.staking.analytics.StakeScreenSource
@ -37,6 +36,7 @@ import com.tangem.domain.staking.model.StakingApproval
import com.tangem.domain.staking.model.stakekit.*
import com.tangem.domain.staking.model.stakekit.action.StakingAction
import com.tangem.domain.staking.model.stakekit.action.StakingActionCommonType
import com.tangem.domain.staking.model.stakekit.action.StakingActionType
import com.tangem.domain.staking.model.stakekit.transaction.StakingTransaction
import com.tangem.domain.staking.utils.getValidatorsCount
import com.tangem.domain.tokens.*
@ -120,6 +120,7 @@ internal class StakingModel @Inject constructor(
private val getActionsUseCase: GetActionsUseCase,
private val getYieldUseCase: GetYieldUseCase,
private val checkAccountInitializedUseCase: CheckAccountInitializedUseCase,
private val getActionRequirementAmountUseCase: GetActionRequirementAmountUseCase,
private val paramsInterceptorHolder: ParamsInterceptorHolder,
private val shareManager: ShareManager,
@DelayedWork private val coroutineScope: CoroutineScope,
@ -527,9 +528,20 @@ internal class StakingModel @Inject constructor(
val rewardPendingActionConstraints = yieldBalance?.reward?.rewardConstraints
if (rewardBlockType == RewardBlockType.RewardsRequirementsError) {
val minimumAmount = rewardPendingActionConstraints?.amountArg?.minimum
// Temporary fix, until StakeKit adds minimum requirement amount to balance response
val minimumAmountValue = if (minimumAmount == null && yieldBalance.integrationId != null) {
getActionRequirementAmountUseCase.invoke(
integrationId = yieldBalance.integrationId,
actionType = StakingActionType.CLAIM_REWARDS,
).getOrNull()
} else {
minimumAmount
}
stakingEventFactory.createStakingRewardsMinimumRequirementsErrorAlert(
cryptoCurrencyName = cryptoCurrencyStatus.currency.name,
cryptoAmountValue = rewardPendingActionConstraints?.amountArg?.minimum?.format {
cryptoAmountValue = minimumAmountValue?.format {
crypto(cryptoCurrencyStatus.currency)
}.orEmpty(),
)
@ -931,11 +943,6 @@ internal class StakingModel @Inject constructor(
isSingleWalletWithTokens = false,
)
.conflate()
.filter {
val sources = it.getOrNull()?.value?.sources ?: return@filter true
sources.networkSource == StatusSource.ACTUAL && sources.yieldBalanceSource == StatusSource.ACTUAL
}
.distinctUntilChanged()
.filter { value.currentStep == StakingStep.InitialInfo }
.onEach { maybeStatus ->

View file

@ -9,6 +9,7 @@ import java.math.BigDecimal
@Immutable
internal sealed class InnerYieldBalanceState {
data class Data(
val integrationId: String?,
val reward: YieldReward,
val isActionable: Boolean,
val balances: ImmutableList<BalanceState>,

View file

@ -50,6 +50,7 @@ internal class YieldBalancesConverter(
?.firstOrNull { it.type == StakingActionType.CLAIM_REWARDS }
InnerYieldBalanceState.Data(
integrationId = yieldBalance?.integrationId,
reward = YieldReward(
rewardsCrypto = cryptoRewardsValue.format { crypto(cryptoCurrency) },
rewardsFiat = fiatRewardsValue.format {

View file

@ -64,7 +64,11 @@ internal class StakingBalanceUpdater @AssistedInject constructor(
coroutineScope {
listOf(
async {
fetchCurrencyStatus()
/*
* It is important to use NonCancellable here to ensure the update is not interrupted midway.
* For example, this can happen if the user enters and immediately leaves the screen.
*/
withContext(NonCancellable) { fetchCurrencyStatus() }
},
async {
updateStakingActions()

View file

@ -64,6 +64,7 @@ internal object InitialStakingStatePreview {
val stateWithYield = defaultState.copy(
yieldBalance = InnerYieldBalanceState.Data(
integrationId = null,
reward = YieldReward(
rewardsFiat = "100 $",
rewardsCrypto = "100 SOL",

View file

@ -3,6 +3,7 @@ package com.tangem.features.staking.impl.presentation.state.utils
import com.tangem.core.ui.R
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.domain.staking.model.stakekit.BalanceType
import com.tangem.domain.staking.model.stakekit.PendingAction
import com.tangem.domain.staking.model.stakekit.action.StakingActionType
import com.tangem.features.staking.impl.presentation.state.BalanceState
@ -44,16 +45,18 @@ internal fun isSingleAction(networkId: String, activeStake: BalanceState): Boole
return isSingleAction && !isRestake || isCompositePendingActions
}
internal fun withStubUnstakeAction(networkId: String, activeStake: BalanceState) = if (isStubUnstakeAction(networkId)) {
activeStake.pendingActions.plus(
PendingAction(
type = StakingActionType.UNSTAKE,
passthrough = "",
args = null,
),
).toPersistentList()
} else {
activeStake.pendingActions
internal fun withStubUnstakeAction(networkId: String, activeStake: BalanceState): ImmutableList<PendingAction> {
return if (isStubUnstakeAction(networkId) && activeStake.type != BalanceType.REWARDS) {
activeStake.pendingActions.plus(
PendingAction(
type = StakingActionType.UNSTAKE,
passthrough = "",
args = null,
),
).toPersistentList()
} else {
activeStake.pendingActions
}
}
internal fun isTronStakedBalance(networkId: String, pendingAction: PendingAction?): Boolean {

View file

@ -6,7 +6,7 @@ import com.tangem.datasource.local.preferences.AppPreferencesStore
import com.tangem.datasource.local.preferences.PreferencesKeys
import com.tangem.datasource.local.preferences.utils.getObjectList
import com.tangem.datasource.local.preferences.utils.getObjectListSync
import com.tangem.datasource.local.preferences.utils.getObjectMapSync
import com.tangem.datasource.local.preferences.utils.getObjectMap
import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.domain.wallets.models.UserWallet
import com.tangem.domain.wallets.models.UserWalletId
@ -17,9 +17,8 @@ import com.tangem.feature.swap.domain.models.domain.*
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import com.tangem.utils.extensions.addOrReplace
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.withContext
internal class DefaultSwapTransactionRepository(
private val appPreferencesStore: AppPreferencesStore,
@ -82,35 +81,35 @@ internal class DefaultSwapTransactionRepository(
}
}
override suspend fun getTransactions(
override fun getTransactions(
userWallet: UserWallet,
cryptoCurrencyId: CryptoCurrency.ID,
): Flow<List<SavedSwapTransactionListModel>?> {
return withContext(dispatchers.io) {
val txStatuses = appPreferencesStore.getObjectMapSync<ExchangeStatusModel>(
key = PreferencesKeys.SWAP_TRANSACTIONS_STATUSES_KEY,
)
appPreferencesStore.getObjectList<SavedSwapTransactionListModelInner>(
return combine(
flow = appPreferencesStore.getObjectList<SavedSwapTransactionListModelInner>(
key = PreferencesKeys.SWAP_TRANSACTIONS_KEY,
).map { savedTransactions ->
val currencyTxs = savedTransactions
?.filter {
it.userWalletId == userWallet.walletId.stringValue &&
(
it.toCryptoCurrencyId == cryptoCurrencyId.value ||
it.fromCryptoCurrencyId == cryptoCurrencyId.value
)
}
),
flow2 = appPreferencesStore.getObjectMap<ExchangeStatusModel>(
key = PreferencesKeys.SWAP_TRANSACTIONS_STATUSES_KEY,
),
) { savedTransactions, txStatuses ->
val currencyTxs = savedTransactions?.filter {
it.userWalletId == userWallet.walletId.stringValue &&
(
it.toCryptoCurrencyId == cryptoCurrencyId.value ||
it.fromCryptoCurrencyId == cryptoCurrencyId.value
)
}
currencyTxs?.mapNotNull {
converter.convertBack(
value = it,
scanResponse = userWallet.requireColdWallet().scanResponse, // TODO [REDACTED_TASK_KEY]
txStatuses = txStatuses,
)
}
}.flowOn(dispatchers.io)
currencyTxs?.mapNotNull {
converter.convertBack(
value = it,
scanResponse = userWallet.requireColdWallet().scanResponse, // TODO [REDACTED_TASK_KEY]
txStatuses = txStatuses,
)
}
}
.flowOn(dispatchers.default)
}
override suspend fun removeTransaction(

View file

@ -17,7 +17,7 @@ interface SwapTransactionRepository {
transaction: SavedSwapTransactionModel,
)
suspend fun getTransactions(
fun getTransactions(
userWallet: UserWallet,
cryptoCurrencyId: CryptoCurrency.ID,
): Flow<List<SavedSwapTransactionListModel>?>

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
@ -134,6 +137,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 +209,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()
@ -304,66 +308,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) {
@ -449,7 +448,7 @@ internal class TokenDetailsModel @Inject constructor(
network = cryptoCurrency.network,
).getOrElse { false }
val isSupported = getExtendedPublicKeyForCurrencyUseCase.isSupported(userWalletId, cryptoCurrency.network)
val isSupported = isXPUBSupported()
internalUiState.value = stateFactory.getStateWithUpdatedMenu(
cardTypesResolver = userWallet.scanResponse.cardTypesResolver,
@ -459,6 +458,34 @@ internal class TokenDetailsModel @Inject constructor(
}
}
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 ->

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()
}

View file

@ -26,6 +26,7 @@ import com.tangem.domain.models.scan.ScanResponse
import com.tangem.domain.nft.DisableWalletNFTUseCase
import com.tangem.domain.nft.EnableWalletNFTUseCase
import com.tangem.domain.nft.GetWalletNFTEnabledUseCase
import com.tangem.domain.notifications.GetApplicationIdUseCase
import com.tangem.domain.notifications.toggles.NotificationsFeatureToggles
import com.tangem.domain.settings.repositories.PermissionRepository
import com.tangem.domain.wallets.models.UserWallet
@ -45,6 +46,7 @@ import com.tangem.features.pushnotifications.api.analytics.PushNotificationAnaly
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import kotlinx.collections.immutable.PersistentList
import kotlinx.collections.immutable.persistentListOf
import kotlinx.coroutines.NonCancellable
import kotlinx.coroutines.flow.*
import kotlinx.coroutines.launch
import timber.log.Timber
@ -72,6 +74,8 @@ internal class WalletSettingsModel @Inject constructor(
private val setNotificationsEnabledUseCase: SetNotificationsEnabledUseCase,
private val settingsManager: SettingsManager,
private val permissionsRepository: PermissionRepository,
private val getApplicationIdUseCase: GetApplicationIdUseCase,
private val associateWalletsWithApplicationIdUseCase: AssociateWalletsWithApplicationIdUseCase,
) : Model() {
val params: WalletSettingsComponent.Params = paramsContainer.require()
@ -192,10 +196,20 @@ internal class WalletSettingsModel @Inject constructor(
if (hasUserWallets) {
router.pop()
} else {
clearWalletsAssociatedWithApplicationId()
router.replaceAll(AppRoute.Home)
}
}
private fun clearWalletsAssociatedWithApplicationId() = modelScope.launch(NonCancellable) {
getApplicationIdUseCase().onRight { applicationId ->
associateWalletsWithApplicationIdUseCase(applicationId, emptyList())
.onLeft {
Timber.e("Unable to associate empty wallets with application ID: $it")
}
}
}
private fun onLinkMoreCardsClick(scanResponse: ScanResponse) {
analyticsEventHandler.send(Settings.ButtonCreateBackup)
analyticsContextProxy.addContext(scanResponse)

View file

@ -58,11 +58,9 @@ internal abstract class BasicTokenListSubscriber(
}
.distinctUntilChanged()
.onEach { maybeTokenList ->
if (!routingFeatureToggle.isDeepLinkNavigationEnabled) {
coroutineScope.launch {
onTokenListReceived(maybeTokenList)
}.saveIn(onTokenListReceivedJobHolder)
}
coroutineScope.launch {
onTokenListReceived(maybeTokenList)
}.saveIn(onTokenListReceivedJobHolder)
coroutineScope.launch { startCheck(maybeTokenList) }
},
@ -77,8 +75,7 @@ internal abstract class BasicTokenListSubscriber(
ifLoading = { maybeContent ->
val isRefreshing = stateHolder.getWalletState(userWallet.walletId)
?.pullToRefreshConfig
?.isRefreshing
?: false
?.isRefreshing == true
maybeContent
?.takeIf { !isRefreshing }
@ -118,15 +115,17 @@ internal abstract class BasicTokenListSubscriber(
}
protected open suspend fun onTokenListReceived(maybeTokenList: Lce<TokenListError, TokenList>) {
/* no-op */
// Handling sell deeplink requires full content in order to correctly open Send screen
if (maybeTokenList.getOrNull(false) != null) {
deepLinksRegistry.triggerDelayedDeeplink(deepLinkClass = SellCurrencyDeepLink::class.java)
}
// Handling referral deeplink requires only selected wallet to be loaded
// This is temporary solution, will be removed with complete deeplink navigation overhaul
if (maybeTokenList.getOrNull(true) != null) {
deepLinksRegistry.triggerDelayedDeeplink(deepLinkClass = ReferralDeepLink::class.java)
if (!routingFeatureToggle.isDeepLinkNavigationEnabled) {
/* no-op */
// Handling sell deeplink requires full content in order to correctly open Send screen
if (maybeTokenList.getOrNull(false) != null) {
deepLinksRegistry.triggerDelayedDeeplink(deepLinkClass = SellCurrencyDeepLink::class.java)
}
// Handling referral deeplink requires only selected wallet to be loaded
// This is temporary solution, will be removed with complete deeplink navigation overhaul
if (maybeTokenList.getOrNull(true) != null) {
deepLinksRegistry.triggerDelayedDeeplink(deepLinkClass = ReferralDeepLink::class.java)
}
}
}

View file

@ -5,7 +5,7 @@
# https://github.com/tangem/tangem-sdk-android/
# https://github.com/tangem/vico
tangemBlockchainSdk = "develop-1104"
tangemBlockchainSdk = "develop-1110"
#tangemBlockchainSdk = "0.0.1" # Keep it! - used for local builds
tangemCardSdk = "develop-489"
#tangemCardSdk = "0.0.1" # Keep it! - used for local builds ^