From f9cc3decfb4293616e24f8f3b6abaa1cde3d064c Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 27 Feb 2025 19:43:34 +0300 Subject: [PATCH 1/9] Updated on 2026-08-14 --- .../common/analytics/events/AnalyticsParam.kt | 2 +- .../exchangeServices/DefaultRampManager.kt | 36 +++-- .../common/ui/tokens/TokenActionsUtils.kt | 21 ++- .../core/analytics/models/AnalyticsParam.kt | 3 +- .../domain/exchange/ExchangeableState.kt | 9 ++ .../domain/exchange/RampStateManager.kt | 2 +- .../model/ScenarioUnavailabilityReason.kt | 26 ++++ .../analytics/TokenScreenAnalyticsEvent.kt | 142 +++++++++++++----- .../tokens/GetCryptoCurrencyActionsUseCase.kt | 84 ++++++++--- .../tokenlist/model/OnrampTokenListModel.kt | 14 +- .../viewmodels/TokenDetailsViewModel.kt | 126 ++++++++-------- .../WalletCurrencyActionsClickIntents.kt | 32 +++- 12 files changed, 353 insertions(+), 144 deletions(-) create mode 100644 domain/legacy/src/main/java/com/tangem/domain/exchange/ExchangeableState.kt diff --git a/app/src/main/java/com/tangem/tap/common/analytics/events/AnalyticsParam.kt b/app/src/main/java/com/tangem/tap/common/analytics/events/AnalyticsParam.kt index 8cfba8dd64..217260e00b 100644 --- a/app/src/main/java/com/tangem/tap/common/analytics/events/AnalyticsParam.kt +++ b/app/src/main/java/com/tangem/tap/common/analytics/events/AnalyticsParam.kt @@ -95,7 +95,7 @@ sealed class AnalyticsParam { } companion object Key { - const val BLOCKCHAIN = "blockchain" + const val BLOCKCHAIN = "Blockchain" const val TOKEN = "Token" const val SOURCE = "Source" const val BALANCE = "Balance" diff --git a/app/src/main/java/com/tangem/tap/network/exchangeServices/DefaultRampManager.kt b/app/src/main/java/com/tangem/tap/network/exchangeServices/DefaultRampManager.kt index f2d1e26685..8876d81ae9 100644 --- a/app/src/main/java/com/tangem/tap/network/exchangeServices/DefaultRampManager.kt +++ b/app/src/main/java/com/tangem/tap/network/exchangeServices/DefaultRampManager.kt @@ -10,6 +10,8 @@ import com.tangem.datasource.api.express.models.TangemExpressValues.EMPTY_CONTRA import com.tangem.datasource.api.express.models.response.Asset import com.tangem.datasource.exchangeservice.swap.ExpressServiceLoader import com.tangem.datasource.local.token.ExpressAssetsStore +import com.tangem.domain.core.lce.Lce +import com.tangem.domain.exchange.ExchangeableState import com.tangem.domain.exchange.RampStateManager import com.tangem.domain.models.scan.ScanResponse import com.tangem.domain.tokens.GetNetworkCoinStatusUseCase @@ -97,10 +99,11 @@ internal class DefaultRampManager( } } - override suspend fun availableForSwap(userWalletId: UserWalletId, cryptoCurrency: CryptoCurrency): Boolean { - return runCatching { getExchangeableFlag(userWalletId, cryptoCurrency) && !cryptoCurrency.isCustom } - .getOrNull() - ?: false + override suspend fun availableForSwap( + userWalletId: UserWalletId, + cryptoCurrency: CryptoCurrency, + ): ExchangeableState { + return runCatching { getExchangeableState(userWalletId, cryptoCurrency) }.getOrNull() ?: ExchangeableState.Error } override fun getBuyInitializationStatus(): Flow { @@ -127,14 +130,27 @@ internal class DefaultRampManager( return expressServiceLoader.getInitializationStatus(userWalletId) } - private suspend fun getExchangeableFlag(userWalletId: UserWalletId, cryptoCurrency: CryptoCurrency): Boolean { + private suspend fun getExchangeableState( + userWalletId: UserWalletId, + cryptoCurrency: CryptoCurrency, + ): ExchangeableState { return withContext(dispatchers.io) { - val asset = expressServiceLoader.getInitializationStatus(userWalletId) - .value - .getOrNull() - ?.find { cryptoCurrency.findAssetPredicate(it) } + when (val asset = expressServiceLoader.getInitializationStatus(userWalletId).value) { + is Lce.Error -> ExchangeableState.Error + is Lce.Loading -> ExchangeableState.Loading + is Lce.Content -> { + val foundAsset = asset.getOrNull()?.find { cryptoCurrency.findAssetPredicate(it) } + foundAsset?.exchangeAvailable?.toExchangeableState() ?: ExchangeableState.AssetNotFound + } + } + } + } - asset?.exchangeAvailable ?: false + private fun Boolean.toExchangeableState(): ExchangeableState { + return if (this) { + ExchangeableState.Exchangeable + } else { + ExchangeableState.NotExchangeable } } diff --git a/common/ui/src/main/java/com/tangem/common/ui/tokens/TokenActionsUtils.kt b/common/ui/src/main/java/com/tangem/common/ui/tokens/TokenActionsUtils.kt index baf5001db7..7337dccc5d 100644 --- a/common/ui/src/main/java/com/tangem/common/ui/tokens/TokenActionsUtils.kt +++ b/common/ui/src/main/java/com/tangem/common/ui/tokens/TokenActionsUtils.kt @@ -34,7 +34,11 @@ fun ScenarioUnavailabilityReason.getUnavailabilityReasonText(): TextReference { formatArgs = wrappedList(unavailabilityReason.cryptoCurrencyName), ) } - ScenarioUnavailabilityReason.Unreachable -> { + is ScenarioUnavailabilityReason.TokenNoQuotes, + ScenarioUnavailabilityReason.SingleWallet, + is ScenarioUnavailabilityReason.ExpressUnreachable, + ScenarioUnavailabilityReason.Unreachable, + -> { resourceReference( id = R.string.token_button_unavailability_generic_description, ) @@ -45,6 +49,21 @@ fun ScenarioUnavailabilityReason.getUnavailabilityReasonText(): TextReference { ScenarioUnavailabilityReason.UsedOutdatedData -> { resourceReference(id = R.string.token_button_unavailability_reason_out_of_date_balance) } + is ScenarioUnavailabilityReason.AssetNotFound -> resourceReference( + id = R.string.token_button_unavailability_reason_not_exchangeable, + formatArgs = wrappedList(unavailabilityReason.cryptoCurrencyName), + ) + is ScenarioUnavailabilityReason.CustomToken -> { + resourceReference( + id = R.string + .token_button_unavailability_reason_custom_token, + ) + } + is ScenarioUnavailabilityReason.DataLoading, + is ScenarioUnavailabilityReason.ExpressLoading, + -> { + resourceReference(id = R.string.token_button_unavailability_reason_loading) + } ScenarioUnavailabilityReason.None -> { throw IllegalArgumentException("The unavailability reason must be other than None") } diff --git a/core/analytics/models/src/main/java/com/tangem/core/analytics/models/AnalyticsParam.kt b/core/analytics/models/src/main/java/com/tangem/core/analytics/models/AnalyticsParam.kt index 38afb4a40f..31e79f809c 100644 --- a/core/analytics/models/src/main/java/com/tangem/core/analytics/models/AnalyticsParam.kt +++ b/core/analytics/models/src/main/java/com/tangem/core/analytics/models/AnalyticsParam.kt @@ -175,7 +175,7 @@ sealed class AnalyticsParam { } companion object Key { - const val BLOCKCHAIN = "blockchain" + const val BLOCKCHAIN = "Blockchain" const val TOKEN_PARAM = "Token" const val SOURCE = "Source" const val BALANCE = "Balance" @@ -210,5 +210,6 @@ sealed class AnalyticsParam { const val RESIDENCE = "Residence" const val PAYMENT_METHOD = "Payment Method" const val WATCHED = "Watched" + const val ACTION = "Action" } } \ No newline at end of file diff --git a/domain/legacy/src/main/java/com/tangem/domain/exchange/ExchangeableState.kt b/domain/legacy/src/main/java/com/tangem/domain/exchange/ExchangeableState.kt new file mode 100644 index 0000000000..9a4f140cf1 --- /dev/null +++ b/domain/legacy/src/main/java/com/tangem/domain/exchange/ExchangeableState.kt @@ -0,0 +1,9 @@ +package com.tangem.domain.exchange + +sealed interface ExchangeableState { + data object Exchangeable : ExchangeableState + data object NotExchangeable : ExchangeableState + data object Loading : ExchangeableState + data object Error : ExchangeableState + data object AssetNotFound : ExchangeableState +} \ No newline at end of file diff --git a/domain/legacy/src/main/java/com/tangem/domain/exchange/RampStateManager.kt b/domain/legacy/src/main/java/com/tangem/domain/exchange/RampStateManager.kt index 3e6ad8a4e8..180a8b16b5 100644 --- a/domain/legacy/src/main/java/com/tangem/domain/exchange/RampStateManager.kt +++ b/domain/legacy/src/main/java/com/tangem/domain/exchange/RampStateManager.kt @@ -31,7 +31,7 @@ interface RampStateManager { status: CryptoCurrencyStatus, ): Either - suspend fun availableForSwap(userWalletId: UserWalletId, cryptoCurrency: CryptoCurrency): Boolean + suspend fun availableForSwap(userWalletId: UserWalletId, cryptoCurrency: CryptoCurrency): ExchangeableState suspend fun fetchBuyServiceData() diff --git a/domain/tokens/models/src/main/java/com/tangem/domain/tokens/model/ScenarioUnavailabilityReason.kt b/domain/tokens/models/src/main/java/com/tangem/domain/tokens/model/ScenarioUnavailabilityReason.kt index adc1a8e4ca..02f95ce1e1 100644 --- a/domain/tokens/models/src/main/java/com/tangem/domain/tokens/model/ScenarioUnavailabilityReason.kt +++ b/domain/tokens/models/src/main/java/com/tangem/domain/tokens/model/ScenarioUnavailabilityReason.kt @@ -5,6 +5,8 @@ sealed class ScenarioUnavailabilityReason { data object UsedOutdatedData : ScenarioUnavailabilityReason() + data object DataLoading : ScenarioUnavailabilityReason() + // staking-specific data class StakingUnavailable(val cryptoCurrencyName: String) : ScenarioUnavailabilityReason() @@ -19,8 +21,32 @@ sealed class ScenarioUnavailabilityReason { data class BuyUnavailable(val cryptoCurrencyName: String) : ScenarioUnavailabilityReason() // swap-specific + /** + * False for this token in asset request + */ data class NotExchangeable(val cryptoCurrencyName: String) : ScenarioUnavailabilityReason() + data class CustomToken(val cryptoCurrencyName: String) : ScenarioUnavailabilityReason() + + data class TokenNoQuotes(val cryptoCurrencyName: String) : ScenarioUnavailabilityReason() + + data object SingleWallet : ScenarioUnavailabilityReason() + + /** + * Express request failed + */ + data class ExpressUnreachable(val cryptoCurrencyName: String) : ScenarioUnavailabilityReason() + + /** + * Assets still loading + */ + data class ExpressLoading(val cryptoCurrencyName: String) : ScenarioUnavailabilityReason() + + /** + * No token in asset list returned from express + */ + data class AssetNotFound(val cryptoCurrencyName: String) : ScenarioUnavailabilityReason() + // sell-specific data class NotSupportedBySellService(val cryptoCurrencyName: String) : ScenarioUnavailabilityReason() diff --git a/domain/tokens/models/src/main/java/com/tangem/domain/tokens/model/analytics/TokenScreenAnalyticsEvent.kt b/domain/tokens/models/src/main/java/com/tangem/domain/tokens/model/analytics/TokenScreenAnalyticsEvent.kt index dcea766313..e88a9c31ee 100644 --- a/domain/tokens/models/src/main/java/com/tangem/domain/tokens/model/analytics/TokenScreenAnalyticsEvent.kt +++ b/domain/tokens/models/src/main/java/com/tangem/domain/tokens/model/analytics/TokenScreenAnalyticsEvent.kt @@ -1,6 +1,10 @@ package com.tangem.domain.tokens.model.analytics import com.tangem.core.analytics.models.AnalyticsEvent +import com.tangem.core.analytics.models.AnalyticsParam.Key.ACTION +import com.tangem.core.analytics.models.AnalyticsParam.Key.BLOCKCHAIN +import com.tangem.core.analytics.models.AnalyticsParam.Key.STATUS +import com.tangem.core.analytics.models.AnalyticsParam.Key.TOKEN_PARAM import com.tangem.domain.tokens.model.ScenarioUnavailabilityReason /** @@ -34,29 +38,89 @@ sealed class TokenScreenAnalyticsEvent( params = mapOf("Token" to token), ) - class ButtonBuy(token: String) : TokenScreenAnalyticsEvent( - event = "Button - Buy", - params = mapOf("Token" to token), - ) + sealed class ButtonWithParams( + event: String, + token: String, + blockchain: String, + status: String?, + ) : TokenScreenAnalyticsEvent( + event = event, + params = buildMap { + put(TOKEN_PARAM, token) + put(BLOCKCHAIN, blockchain) + status?.let { put(STATUS, it) } + }, + ) { - class ButtonSell(token: String) : TokenScreenAnalyticsEvent( - event = "Button - Sell", - params = mapOf("Token" to token), - ) + class ButtonBuy( + token: String, + blockchain: String, + status: String?, + ) : ButtonWithParams( + event = "Button - Buy", + token = token, + status = status, + blockchain = blockchain, + ) - class ButtonExchange(token: String) : TokenScreenAnalyticsEvent( - event = "Button - Exchange", - params = mapOf("Token" to token), - ) + class ButtonSell( + token: String, + status: String, + blockchain: String, + ) : ButtonWithParams( + event = "Button - Sell", + token = token, + status = status, + blockchain = blockchain, + ) - class ButtonSend(token: String) : TokenScreenAnalyticsEvent( - event = "Button - Send", - params = mapOf("Token" to token), - ) + class ButtonExchange( + token: String, + status: String, + blockchain: String, + ) : ButtonWithParams( + event = "Button - Exchange", + token = token, + status = status, + blockchain = blockchain, + ) - class ButtonReceive(token: String) : TokenScreenAnalyticsEvent( - event = "Button - Receive", - params = mapOf("Token" to token), + class ButtonSend( + token: String, + status: String, + blockchain: String, + ) : ButtonWithParams( + event = "Button - Send", + token = token, + status = status, + blockchain = blockchain, + ) + + class ButtonReceive( + token: String, + status: String, + blockchain: String, + ) : ButtonWithParams( + event = "Button - Receive", + token = token, + status = status, + blockchain = blockchain, + ) + } + + class ActionButtonDisabled( + token: String, + status: String, + blockchain: String, + action: String, + ) : TokenScreenAnalyticsEvent( + event = "Action Button Disabled", + params = mapOf( + TOKEN_PARAM to token, + STATUS to status, + BLOCKCHAIN to blockchain, + ACTION to action, + ), ) class ButtonCopyAddress(token: String) : TokenScreenAnalyticsEvent( @@ -89,30 +153,20 @@ sealed class TokenScreenAnalyticsEvent( params = mapOf("Token" to token), ) - class NoticeActionInactive(token: String, tokenAction: TokenAction, reason: String) : TokenScreenAnalyticsEvent( - event = "Notice - Action Inactive", - params = buildMap { - put("Token", token) - put("Action", tokenAction.action) - if (reason.isNotEmpty()) { - put("Reason", reason) - } - }, - ) { - sealed class TokenAction(val action: String) { - data object BuyAction : TokenAction("Buy") - data object SellAction : TokenAction("Sell") - data object SwapAction : TokenAction("Swap") - data object SendAction : TokenAction("Send") - data object ReceiveAction : TokenAction("Receive") - } - } - companion object { + const val AVAILABLE = "Available" private const val UNAVAILABLE = "Unavailable" private const val EMPTY = "Empty" private const val PENDING = "Pending" private const val CACHING = "Caching" + private const val CUSTOM_TOKEN = "Custom Token" + private const val BLOCKCHAIN_UNREACHABLE = "Blockchain Unreachable" + private const val ASSET_NOT_FOUND = "Asset NotFound" + private const val ASSET_LOADING = "Assets Loading" + private const val ASSETS_ERROR = "Assets Error" + private const val NO_QUOTE = "Quotes Unavailable" + private const val SINGLE_WALLET = "Single Wallet" + private const val LOADING = "Loading" fun ScenarioUnavailabilityReason.toReasonAnalyticsText(): String { return when (this) { @@ -125,9 +179,15 @@ sealed class TokenScreenAnalyticsEvent( is ScenarioUnavailabilityReason.EmptyBalance -> EMPTY is ScenarioUnavailabilityReason.PendingTransaction -> PENDING ScenarioUnavailabilityReason.UsedOutdatedData -> CACHING - ScenarioUnavailabilityReason.None, - ScenarioUnavailabilityReason.Unreachable, - -> "" + ScenarioUnavailabilityReason.None -> AVAILABLE + ScenarioUnavailabilityReason.Unreachable -> BLOCKCHAIN_UNREACHABLE + is ScenarioUnavailabilityReason.AssetNotFound -> ASSET_NOT_FOUND + is ScenarioUnavailabilityReason.CustomToken -> CUSTOM_TOKEN + is ScenarioUnavailabilityReason.ExpressLoading -> ASSET_LOADING + is ScenarioUnavailabilityReason.ExpressUnreachable -> ASSETS_ERROR + ScenarioUnavailabilityReason.SingleWallet -> SINGLE_WALLET + is ScenarioUnavailabilityReason.TokenNoQuotes -> NO_QUOTE + ScenarioUnavailabilityReason.DataLoading -> LOADING } } } diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetCryptoCurrencyActionsUseCase.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetCryptoCurrencyActionsUseCase.kt index 680c0148e3..5e627d055b 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetCryptoCurrencyActionsUseCase.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetCryptoCurrencyActionsUseCase.kt @@ -1,6 +1,7 @@ package com.tangem.domain.tokens import com.tangem.domain.common.util.cardTypesResolver +import com.tangem.domain.exchange.ExchangeableState import com.tangem.domain.exchange.RampStateManager import com.tangem.domain.models.StatusSource import com.tangem.domain.promo.PromoRepository @@ -24,7 +25,7 @@ import kotlinx.coroutines.withTimeoutOrNull * * @property rampManager Ramp manager to check ramp availability */ -@Suppress("LongParameterList") +@Suppress("LongParameterList", "LargeClass") class GetCryptoCurrencyActionsUseCase( private val rampManager: RampStateManager, private val walletManagersFacade: WalletManagersFacade, @@ -181,26 +182,11 @@ class GetCryptoCurrencyActionsUseCase( } // swap - if (userWallet.isMultiCurrency) { - val isExchangeable = withTimeoutOrNull(REQUEST_EXCHANGE_DATA_TIMEOUT) { - rampManager.availableForSwap(userWallet.walletId, cryptoCurrency) - } ?: false - if (isExchangeable && cryptoCurrencyStatus.value !is CryptoCurrencyStatus.NoQuote) { - val swapStoriesEnabled = swapFeatureToggles.isPromoStoriesEnabled - activeList.add( - TokenActionsState.ActionState.Swap( - unavailabilityReason = ScenarioUnavailabilityReason.None, - showBadge = shouldShowSwapStories && swapStoriesEnabled, - ), - ) - } else { - disabledList.add( - TokenActionsState.ActionState.Swap( - unavailabilityReason = ScenarioUnavailabilityReason.NotExchangeable(cryptoCurrency.name), - showBadge = false, - ), - ) - } + val swapActionState = getSwapUnavailabilityReason(userWallet, cryptoCurrencyStatus, shouldShowSwapStories) + if (swapActionState.unavailabilityReason == ScenarioUnavailabilityReason.None) { + activeList.add(swapActionState) + } else { + disabledList.add(swapActionState) } // buy @@ -289,7 +275,8 @@ class GetCryptoCurrencyActionsUseCase( } // swap - val isSwapAvailable = with(cryptoCurrencyStatus.value.sources) { + val sources = cryptoCurrencyStatus.value.sources + val isSwapAvailable = with(sources) { quoteSource.isActual() && networkSource.isActual() } @@ -297,8 +284,11 @@ class GetCryptoCurrencyActionsUseCase( TokenActionsState.ActionState.Swap( unavailabilityReason = if (isSwapAvailable) { ScenarioUnavailabilityReason.None - } else { + } else if (sources.networkSource == StatusSource.ONLY_CACHE) { ScenarioUnavailabilityReason.UsedOutdatedData + } else { + // CACHE source always when loading + ScenarioUnavailabilityReason.DataLoading }, showBadge = false, ), @@ -376,6 +366,54 @@ class GetCryptoCurrencyActionsUseCase( } } + private suspend fun getSwapUnavailabilityReason( + userWallet: UserWallet, + cryptoCurrencyStatus: CryptoCurrencyStatus, + shouldShowSwapStories: Boolean, + ): TokenActionsState.ActionState { + val cryptoCurrency = cryptoCurrencyStatus.currency + return if (userWallet.isMultiCurrency) { + if (cryptoCurrency.isCustom) { + return TokenActionsState.ActionState.Swap( + unavailabilityReason = ScenarioUnavailabilityReason.CustomToken(cryptoCurrency.name), + showBadge = false, + ) + } + if (cryptoCurrencyStatus.value is CryptoCurrencyStatus.NoQuote) { + return TokenActionsState.ActionState.Swap( + unavailabilityReason = ScenarioUnavailabilityReason.TokenNoQuotes(cryptoCurrency.name), + showBadge = false, + ) + } + val exchangeableState = withTimeoutOrNull(REQUEST_EXCHANGE_DATA_TIMEOUT) { + rampManager.availableForSwap(userWallet.walletId, cryptoCurrency) + } ?: ExchangeableState.Loading + val swapStoriesEnabled = swapFeatureToggles.isPromoStoriesEnabled + val currencyName = cryptoCurrency.name + + val reason = when (exchangeableState) { + ExchangeableState.Exchangeable -> { + ScenarioUnavailabilityReason.None + } + ExchangeableState.AssetNotFound -> ScenarioUnavailabilityReason.AssetNotFound(currencyName) + ExchangeableState.Error -> ScenarioUnavailabilityReason.ExpressUnreachable(currencyName) + ExchangeableState.Loading -> ScenarioUnavailabilityReason.ExpressLoading(currencyName) + ExchangeableState.NotExchangeable -> ScenarioUnavailabilityReason.NotExchangeable(currencyName) + } + + val isShowBadge = reason == ScenarioUnavailabilityReason.None && shouldShowSwapStories && swapStoriesEnabled + TokenActionsState.ActionState.Swap( + unavailabilityReason = reason, + showBadge = isShowBadge, + ) + } else { + TokenActionsState.ActionState.Swap( + unavailabilityReason = ScenarioUnavailabilityReason.SingleWallet, + showBadge = false, + ) + } + } + private fun isAddressAvailable(networkAddress: NetworkAddress?): Boolean { return networkAddress != null && networkAddress.defaultAddress.value.isNotEmpty() } diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/tokenlist/model/OnrampTokenListModel.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/tokenlist/model/OnrampTokenListModel.kt index 62fa577e9d..76809b1485 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/tokenlist/model/OnrampTokenListModel.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/tokenlist/model/OnrampTokenListModel.kt @@ -10,6 +10,7 @@ import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.balancehiding.GetBalanceHidingSettingsUseCase import com.tangem.domain.core.lce.Lce import com.tangem.domain.core.utils.getOrElse +import com.tangem.domain.exchange.ExchangeableState import com.tangem.domain.exchange.RampStateManager import com.tangem.domain.settings.usercountry.GetUserCountryUseCase import com.tangem.domain.settings.usercountry.models.UserCountry @@ -241,10 +242,21 @@ internal class OnrampTokenListModel @Inject constructor( val isAvailable = rampStateManager.availableForSwap( userWalletId = params.userWalletId, cryptoCurrency = status.currency, - ) + ).isSwapAvailable() && !status.currency.isCustom isAvailable && status.value !is CryptoCurrencyStatus.NoQuote } } } + + private fun ExchangeableState.isSwapAvailable(): Boolean { + return when (this) { + ExchangeableState.AssetNotFound, + ExchangeableState.Error, + ExchangeableState.Loading, + ExchangeableState.NotExchangeable, + -> false + ExchangeableState.Exchangeable -> true + } + } } \ No newline at end of file diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/viewmodels/TokenDetailsViewModel.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/viewmodels/TokenDetailsViewModel.kt index 9b4d62818d..a20a2babfc 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/viewmodels/TokenDetailsViewModel.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/viewmodels/TokenDetailsViewModel.kt @@ -45,10 +45,7 @@ import com.tangem.domain.staking.GetYieldUseCase import com.tangem.domain.staking.model.StakingAvailability import com.tangem.domain.tokens.* import com.tangem.domain.tokens.legacy.TradeCryptoAction -import com.tangem.domain.tokens.model.CryptoCurrency -import com.tangem.domain.tokens.model.CryptoCurrencyStatus -import com.tangem.domain.tokens.model.NetworkAddress -import com.tangem.domain.tokens.model.ScenarioUnavailabilityReason +import com.tangem.domain.tokens.model.* import com.tangem.domain.tokens.model.analytics.TokenReceiveAnalyticsEvent import com.tangem.domain.tokens.model.analytics.TokenScreenAnalyticsEvent import com.tangem.domain.tokens.model.analytics.TokenScreenAnalyticsEvent.Companion.toReasonAnalyticsText @@ -281,6 +278,7 @@ internal class TokenDetailsViewModel @Inject constructor( .conflate() .distinctUntilChanged() .onEach { + sendButtonsEvents(it.states) internalUiState.value = stateFactory.getManageButtonsState(actions = it.states) } .flowOn(dispatchers.main) @@ -288,6 +286,21 @@ internal class TokenDetailsViewModel @Inject constructor( .saveIn(buttonsJobHolder) } + private fun sendButtonsEvents(states: List) { + // for now send only for swap + val swapState = states.firstOrNull { it is TokenActionsState.ActionState.Swap } ?: return + if (swapState.unavailabilityReason != ScenarioUnavailabilityReason.None) { + analyticsEventsHandler.send( + TokenScreenAnalyticsEvent.ActionButtonDisabled( + token = cryptoCurrency.symbol, + status = swapState.unavailabilityReason.toReasonAnalyticsText(), + blockchain = cryptoCurrency.network.name, + action = "Swap", + ), + ) + } + } + private fun updateWarnings(cryptoCurrencyStatus: CryptoCurrencyStatus) { viewModelScope.launch(dispatchers.main) { getCurrencyWarningsUseCase.invoke( @@ -472,14 +485,15 @@ internal class TokenDetailsViewModel @Inject constructor( } override fun onBuyClick(unavailabilityReason: ScenarioUnavailabilityReason) { - analyticsEventsHandler.send(TokenScreenAnalyticsEvent.ButtonBuy(cryptoCurrency.symbol)) + analyticsEventsHandler.send( + TokenScreenAnalyticsEvent.ButtonWithParams.ButtonBuy( + token = cryptoCurrency.symbol, + blockchain = cryptoCurrency.network.name, + status = unavailabilityReason.toReasonAnalyticsText(), + ), + ) - if (handleUnavailabilityReason( - unavailabilityReason = unavailabilityReason, - tokenSymbol = cryptoCurrency.symbol, - tokenAction = TokenScreenAnalyticsEvent.NoticeActionInactive.TokenAction.BuyAction, - ) - ) { + if (handleUnavailabilityReason(unavailabilityReason = unavailabilityReason)) { return } @@ -512,7 +526,13 @@ internal class TokenDetailsViewModel @Inject constructor( } override fun onBuyCoinClick(cryptoCurrency: CryptoCurrency) { - analyticsEventsHandler.send(TokenScreenAnalyticsEvent.ButtonBuy(cryptoCurrency.symbol)) + analyticsEventsHandler.send( + TokenScreenAnalyticsEvent.ButtonWithParams.ButtonBuy( + token = cryptoCurrency.symbol, + blockchain = cryptoCurrency.network.name, + status = null, + ), + ) router.openTokenDetails(userWalletId = userWalletId, currency = cryptoCurrency) } @@ -528,14 +548,15 @@ internal class TokenDetailsViewModel @Inject constructor( } override fun onSendClick(unavailabilityReason: ScenarioUnavailabilityReason) { - analyticsEventsHandler.send(TokenScreenAnalyticsEvent.ButtonSend(cryptoCurrency.symbol)) + analyticsEventsHandler.send( + TokenScreenAnalyticsEvent.ButtonWithParams.ButtonSend( + token = cryptoCurrency.symbol, + blockchain = cryptoCurrency.network.name, + status = unavailabilityReason.toReasonAnalyticsText(), + ), + ) - if (handleUnavailabilityReason( - unavailabilityReason = unavailabilityReason, - tokenSymbol = cryptoCurrency.symbol, - tokenAction = TokenScreenAnalyticsEvent.NoticeActionInactive.TokenAction.SendAction, - ) - ) { + if (handleUnavailabilityReason(unavailabilityReason = unavailabilityReason)) { return } @@ -554,17 +575,18 @@ internal class TokenDetailsViewModel @Inject constructor( override fun onReceiveClick(unavailabilityReason: ScenarioUnavailabilityReason) { val networkAddress = cryptoCurrencyStatus?.value?.networkAddress ?: return - if (handleUnavailabilityReason( - unavailabilityReason = unavailabilityReason, - tokenSymbol = cryptoCurrency.symbol, - tokenAction = TokenScreenAnalyticsEvent.NoticeActionInactive.TokenAction.ReceiveAction, - ) - ) { + if (handleUnavailabilityReason(unavailabilityReason = unavailabilityReason)) { return } viewModelScope.launch(dispatchers.main) { - analyticsEventsHandler.send(TokenScreenAnalyticsEvent.ButtonReceive(cryptoCurrency.symbol)) + analyticsEventsHandler.send( + TokenScreenAnalyticsEvent.ButtonWithParams.ButtonReceive( + token = cryptoCurrency.symbol, + blockchain = cryptoCurrency.network.name, + status = unavailabilityReason.toReasonAnalyticsText(), + ), + ) analyticsEventsHandler.send(TokenReceiveAnalyticsEvent.ReceiveScreenOpened(cryptoCurrency.symbol)) internalUiState.value = stateFactory.getStateWithReceiveBottomSheet( @@ -583,12 +605,7 @@ internal class TokenDetailsViewModel @Inject constructor( } override fun onStakeClick(unavailabilityReason: ScenarioUnavailabilityReason) { - if (handleUnavailabilityReason( - unavailabilityReason = unavailabilityReason, - tokenSymbol = cryptoCurrency.symbol, - tokenAction = null, - ) - ) { + if (handleUnavailabilityReason(unavailabilityReason = unavailabilityReason)) { return } @@ -619,14 +636,15 @@ internal class TokenDetailsViewModel @Inject constructor( } override fun onSellClick(unavailabilityReason: ScenarioUnavailabilityReason) { - analyticsEventsHandler.send(TokenScreenAnalyticsEvent.ButtonSell(cryptoCurrency.symbol)) + analyticsEventsHandler.send( + TokenScreenAnalyticsEvent.ButtonWithParams.ButtonSell( + token = cryptoCurrency.symbol, + blockchain = cryptoCurrency.network.name, + status = unavailabilityReason.toReasonAnalyticsText(), + ), + ) - if (handleUnavailabilityReason( - unavailabilityReason = unavailabilityReason, - tokenSymbol = cryptoCurrency.symbol, - tokenAction = TokenScreenAnalyticsEvent.NoticeActionInactive.TokenAction.SellAction, - ) - ) { + if (handleUnavailabilityReason(unavailabilityReason = unavailabilityReason)) { return } @@ -643,14 +661,15 @@ internal class TokenDetailsViewModel @Inject constructor( } override fun onSwapClick(unavailabilityReason: ScenarioUnavailabilityReason) { - analyticsEventsHandler.send(TokenScreenAnalyticsEvent.ButtonExchange(cryptoCurrency.symbol)) + analyticsEventsHandler.send( + TokenScreenAnalyticsEvent.ButtonWithParams.ButtonExchange( + token = cryptoCurrency.symbol, + blockchain = cryptoCurrency.network.name, + status = unavailabilityReason.toReasonAnalyticsText(), + ), + ) - if (handleUnavailabilityReason( - unavailabilityReason = unavailabilityReason, - tokenSymbol = cryptoCurrency.symbol, - tokenAction = TokenScreenAnalyticsEvent.NoticeActionInactive.TokenAction.SwapAction, - ) - ) { + if (handleUnavailabilityReason(unavailabilityReason = unavailabilityReason)) { return } @@ -972,21 +991,8 @@ internal class TokenDetailsViewModel @Inject constructor( internalUiState.value = stateFactory.getStateWithClosedBottomSheet() } - private fun handleUnavailabilityReason( - unavailabilityReason: ScenarioUnavailabilityReason, - tokenSymbol: String, - tokenAction: TokenScreenAnalyticsEvent.NoticeActionInactive.TokenAction?, - ): Boolean { + private fun handleUnavailabilityReason(unavailabilityReason: ScenarioUnavailabilityReason): Boolean { if (unavailabilityReason == ScenarioUnavailabilityReason.None) return false - if (tokenAction != null) { - analyticsEventsHandler.send( - TokenScreenAnalyticsEvent.NoticeActionInactive( - token = tokenSymbol, - tokenAction = tokenAction, - reason = unavailabilityReason.toReasonAnalyticsText(), - ), - ) - } internalUiState.value = stateFactory.getStateWithActionButtonErrorDialog(unavailabilityReason) diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/intents/WalletCurrencyActionsClickIntents.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/intents/WalletCurrencyActionsClickIntents.kt index b9647e9365..b5a1671df6 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/intents/WalletCurrencyActionsClickIntents.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/intents/WalletCurrencyActionsClickIntents.kt @@ -39,6 +39,8 @@ import com.tangem.domain.tokens.legacy.TradeCryptoAction import com.tangem.domain.tokens.model.* import com.tangem.domain.tokens.model.analytics.TokenReceiveAnalyticsEvent import com.tangem.domain.tokens.model.analytics.TokenScreenAnalyticsEvent +import com.tangem.domain.tokens.model.analytics.TokenScreenAnalyticsEvent.Companion.AVAILABLE +import com.tangem.domain.tokens.model.analytics.TokenScreenAnalyticsEvent.Companion.toReasonAnalyticsText import com.tangem.domain.walletmanager.WalletManagersFacade import com.tangem.domain.wallets.models.UserWalletId import com.tangem.domain.wallets.usecase.GetExploreUrlUseCase @@ -133,7 +135,11 @@ internal class WalletCurrencyActionsClickIntentsImplementor @Inject constructor( val userWallet = getSelectedWalletSyncUseCase.unwrap() ?: return analyticsEventHandler.send( - event = TokenScreenAnalyticsEvent.ButtonSend(cryptoCurrencyStatus.currency.symbol), + event = TokenScreenAnalyticsEvent.ButtonWithParams.ButtonSend( + token = cryptoCurrencyStatus.currency.symbol, + status = unavailabilityReason.toReasonAnalyticsText(), + blockchain = cryptoCurrencyStatus.currency.network.name, + ), ) if (handleUnavailabilityReason(unavailabilityReason)) return @@ -151,7 +157,11 @@ internal class WalletCurrencyActionsClickIntentsImplementor @Inject constructor( val userWalletId = stateHolder.getSelectedWalletId() analyticsEventHandler.send( - event = TokenScreenAnalyticsEvent.ButtonReceive(cryptoCurrencyStatus.currency.symbol), + event = TokenScreenAnalyticsEvent.ButtonWithParams.ButtonReceive( + token = cryptoCurrencyStatus.currency.symbol, + status = AVAILABLE, + blockchain = cryptoCurrencyStatus.currency.network.name, + ), ) analyticsEventHandler.send( @@ -290,7 +300,11 @@ internal class WalletCurrencyActionsClickIntentsImplementor @Inject constructor( unavailabilityReason: ScenarioUnavailabilityReason, ) { analyticsEventHandler.send( - event = TokenScreenAnalyticsEvent.ButtonSell(cryptoCurrencyStatus.currency.symbol), + event = TokenScreenAnalyticsEvent.ButtonWithParams.ButtonSell( + token = cryptoCurrencyStatus.currency.symbol, + status = unavailabilityReason.toReasonAnalyticsText(), + blockchain = cryptoCurrencyStatus.currency.network.name, + ), ) if (handleUnavailabilityReason(unavailabilityReason)) return @@ -314,7 +328,11 @@ internal class WalletCurrencyActionsClickIntentsImplementor @Inject constructor( val userWallet = getSelectedWalletSyncUseCase.unwrap() ?: return analyticsEventHandler.send( - event = TokenScreenAnalyticsEvent.ButtonBuy(cryptoCurrencyStatus.currency.symbol), + event = TokenScreenAnalyticsEvent.ButtonWithParams.ButtonBuy( + token = cryptoCurrencyStatus.currency.symbol, + status = unavailabilityReason.toReasonAnalyticsText(), + blockchain = cryptoCurrencyStatus.currency.network.name, + ), ) if (handleUnavailabilityReason(unavailabilityReason)) return @@ -349,7 +367,11 @@ internal class WalletCurrencyActionsClickIntentsImplementor @Inject constructor( unavailabilityReason: ScenarioUnavailabilityReason, ) { analyticsEventHandler.send( - event = TokenScreenAnalyticsEvent.ButtonExchange(cryptoCurrencyStatus.currency.symbol), + event = TokenScreenAnalyticsEvent.ButtonWithParams.ButtonExchange( + token = cryptoCurrencyStatus.currency.symbol, + status = unavailabilityReason.toReasonAnalyticsText(), + blockchain = cryptoCurrencyStatus.currency.network.name, + ), ) if (handleUnavailabilityReason(unavailabilityReason)) return From 08d3e18b3e7dbed970290a4370977a74c475f24d Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 11 Mar 2025 10:02:52 +0300 Subject: [PATCH 2/9] Updated on 2026-08-14 --- .../exchangeServices/DefaultRampManager.kt | 8 ++++---- .../analytics/TokenScreenAnalyticsEvent.kt | 4 +++- .../viewmodels/TokenDetailsViewModel.kt | 17 +++++++++-------- 3 files changed, 16 insertions(+), 13 deletions(-) diff --git a/app/src/main/java/com/tangem/tap/network/exchangeServices/DefaultRampManager.kt b/app/src/main/java/com/tangem/tap/network/exchangeServices/DefaultRampManager.kt index 8876d81ae9..ca9bb24e80 100644 --- a/app/src/main/java/com/tangem/tap/network/exchangeServices/DefaultRampManager.kt +++ b/app/src/main/java/com/tangem/tap/network/exchangeServices/DefaultRampManager.kt @@ -77,10 +77,6 @@ internal class DefaultRampManager( catch = { raise(ScenarioUnavailabilityReason.NotSupportedBySellService(status.currency.name)) }, ) - ensure(condition = sellSupportedByService) { - ScenarioUnavailabilityReason.NotSupportedBySellService(status.currency.name) - } - val reason = getSendUnavailabilityReason(userWalletId, status) ensure(condition = reason is ScenarioUnavailabilityReason.None) { @@ -95,6 +91,10 @@ internal class DefaultRampManager( } } + ensure(condition = sellSupportedByService) { + ScenarioUnavailabilityReason.NotSupportedBySellService(status.currency.name) + } + Unit.right() } } diff --git a/domain/tokens/models/src/main/java/com/tangem/domain/tokens/model/analytics/TokenScreenAnalyticsEvent.kt b/domain/tokens/models/src/main/java/com/tangem/domain/tokens/model/analytics/TokenScreenAnalyticsEvent.kt index e88a9c31ee..9e8a010f52 100644 --- a/domain/tokens/models/src/main/java/com/tangem/domain/tokens/model/analytics/TokenScreenAnalyticsEvent.kt +++ b/domain/tokens/models/src/main/java/com/tangem/domain/tokens/model/analytics/TokenScreenAnalyticsEvent.kt @@ -167,15 +167,17 @@ sealed class TokenScreenAnalyticsEvent( private const val NO_QUOTE = "Quotes Unavailable" private const val SINGLE_WALLET = "Single Wallet" private const val LOADING = "Loading" + private const val ASSET_REQUIREMENT = "AssetRequirement" + @Suppress("CyclomaticComplexMethod") fun ScenarioUnavailabilityReason.toReasonAnalyticsText(): String { return when (this) { is ScenarioUnavailabilityReason.BuyUnavailable, is ScenarioUnavailabilityReason.NotExchangeable, is ScenarioUnavailabilityReason.NotSupportedBySellService, is ScenarioUnavailabilityReason.StakingUnavailable, - ScenarioUnavailabilityReason.UnassociatedAsset, -> UNAVAILABLE + ScenarioUnavailabilityReason.UnassociatedAsset -> ASSET_REQUIREMENT is ScenarioUnavailabilityReason.EmptyBalance -> EMPTY is ScenarioUnavailabilityReason.PendingTransaction -> PENDING ScenarioUnavailabilityReason.UsedOutdatedData -> CACHING diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/viewmodels/TokenDetailsViewModel.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/viewmodels/TokenDetailsViewModel.kt index a20a2babfc..535d369528 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/viewmodels/TokenDetailsViewModel.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/viewmodels/TokenDetailsViewModel.kt @@ -575,18 +575,19 @@ internal class TokenDetailsViewModel @Inject constructor( override fun onReceiveClick(unavailabilityReason: ScenarioUnavailabilityReason) { val networkAddress = cryptoCurrencyStatus?.value?.networkAddress ?: return + analyticsEventsHandler.send( + TokenScreenAnalyticsEvent.ButtonWithParams.ButtonReceive( + token = cryptoCurrency.symbol, + blockchain = cryptoCurrency.network.name, + status = unavailabilityReason.toReasonAnalyticsText(), + ), + ) + if (handleUnavailabilityReason(unavailabilityReason = unavailabilityReason)) { return } - viewModelScope.launch(dispatchers.main) { - analyticsEventsHandler.send( - TokenScreenAnalyticsEvent.ButtonWithParams.ButtonReceive( - token = cryptoCurrency.symbol, - blockchain = cryptoCurrency.network.name, - status = unavailabilityReason.toReasonAnalyticsText(), - ), - ) + viewModelScope.launch { analyticsEventsHandler.send(TokenReceiveAnalyticsEvent.ReceiveScreenOpened(cryptoCurrency.symbol)) internalUiState.value = stateFactory.getStateWithReceiveBottomSheet( From 448b849b89c28d6482ad644abe9b759b73a6cfbc Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 11 Mar 2025 10:03:25 +0300 Subject: [PATCH 3/9] Updated on 2026-08-14 --- .../com/tangem/domain/managetokens/ValidateTokenFormUseCase.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/domain/manage-tokens/src/main/kotlin/com/tangem/domain/managetokens/ValidateTokenFormUseCase.kt b/domain/manage-tokens/src/main/kotlin/com/tangem/domain/managetokens/ValidateTokenFormUseCase.kt index 79a0481bc4..afa0efcf2b 100644 --- a/domain/manage-tokens/src/main/kotlin/com/tangem/domain/managetokens/ValidateTokenFormUseCase.kt +++ b/domain/manage-tokens/src/main/kotlin/com/tangem/domain/managetokens/ValidateTokenFormUseCase.kt @@ -77,7 +77,7 @@ class ValidateTokenFormUseCase( } companion object { - const val MIN_DECIMALS = 1 + const val MIN_DECIMALS = 0 const val MAX_DECIMALS = 30 } } \ No newline at end of file From 2d61d3d5210fa753a37deeb82aaedb3af17adc78 Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 11 Mar 2025 11:20:46 +0300 Subject: [PATCH 4/9] Updated on 2026-08-14 --- core/res/src/main/res/values-de/strings.xml | 10 +++++++++- core/res/src/main/res/values-es/strings.xml | 6 +++++- core/res/src/main/res/values-fr/strings.xml | 4 ++++ core/res/src/main/res/values-it/strings.xml | 1 + core/res/src/main/res/values-ja/strings.xml | 8 +++++++- core/res/src/main/res/values-ru/strings.xml | 8 +++++++- core/res/src/main/res/values-uk-rUA/strings.xml | 4 ++++ core/res/src/main/res/values-zh-rTW/strings.xml | 3 +++ core/res/src/main/res/values/strings.xml | 7 ++++++- 9 files changed, 46 insertions(+), 5 deletions(-) diff --git a/core/res/src/main/res/values-de/strings.xml b/core/res/src/main/res/values-de/strings.xml index 6edea905ea..c5b765d329 100644 --- a/core/res/src/main/res/values-de/strings.xml +++ b/core/res/src/main/res/values-de/strings.xml @@ -6,7 +6,7 @@ Einige Netzwerke sind derzeit nicht erreichbar. Bitte versuche es später erneut. Die Daten wurden noch nicht geladen. Die Aktion ist derzeit nicht verfügbar. Bitte versuche es später erneut oder aktualisiere die Daten, indem Du auf dem Bildschirm nach unten wischst. - Schaltfläche ist nicht verfügbar + Aktion ist nicht verfügbar Wähle den Token Du kannst Deinen Token nicht finden? Gehe zum Bereich „Markt“ auf der Hauptseite und fügen diesen zum tauschen im Portfolio hinzu. Es gibt keine verfügbaren Token, die mit dem ausgewählten Token getauscht werden können. Bitte wähle einen anderen. @@ -241,6 +241,7 @@ Dieser Mechanismus schützt vor Annäherungsangriffen auf eine Karte oder einen Ring. Er erzwingt eine Verzögerung zwischen dem Empfang und der Ausführung eines Befehls. Passcode Bevor du einen Befehl ausführst, der eine Änderung des Kartenstatus zur Folge hat, musst du den Passcode eingeben. + NFT Empfehlungsprogramm Flippe den Bildschirm deines Geräts nach unten, um Salden schnell ein- und auszublenden %s Hasch @@ -536,6 +537,7 @@ Möchtest du den Aktivierungsprozess abbrechen? Erste Schritte Für die Karte oder Ring, die du hinzufügen möchtest, wurde bereits eine andere Wallets erstellt. Wenn du Guthaben auf dieser Wallets hast, hebe es bitte ab, setze diese Karte oder Ring zurück und füge sie als Backup hinzu. + Sicher deine Wallet Backups anlegen Lese mehr über die Seed-Phrase @@ -668,6 +670,7 @@ Nutzung biometrischer Daten zulassen Bei Interaktionen mit deiner Wallet werden anstelle des Zugangscodes biometrische Daten abgefragt Zugangscode + Nicht zulassen Es sieht so aus, als ob du die biometrische Authentifizierung deaktiviert hast. Diese ist notwendig, um Wallets zu speichern Biometrische Autorisierung aktivieren Möchtest du Biometrie nutzen? @@ -778,6 +781,7 @@ %s geschätzter Profit Marktbewertung Metriken + Gemäß den Netzwerkregeln %1$s sind Ansprüche ab %2$s möglich. Die unten aufgeführten Beträge werden Deinem Konto nach der Aufhebung des Stakings gutgeschrieben. Mindestanforderungen Keine Belohnungen verfügbar Belohnungen beanspruchen @@ -824,6 +828,7 @@ Reinvestiert Deine verdienten Prämien in Deinen Einsatzbetrag und erhöht so den potenziellen Gewinn. Mit Restake kannst Du Dein Guthaben von einem Validator zu einem anderen verschieben, ohne dass Du den Stake aufheben musst. Du bist dabei, Dein gesamtes Guthaben zu staken. Wir empfehlen, einen kleinen Betrag übrig zu lassen, um die Netzwerkgebühren für die Aufhebung des Stakes oder das Einfordern von Prämien abzudecken. + Um mit dem Staking bei TON zu beginnen, führe zunächst eine ausgehende Transaktion in beliebiger Höhe durch – dadurch wird Deine Wallet aktiviert. Entsperre dein Geld, um es aus dem Staking-Prozess abzuheben. Das Freischalten nimmt %s. Nach Ablauf der 21-tägigen Bindungsfrist kannst Du über Dein Guthaben verfügen. Die Prämie wird zusammen mit dem ungestaketen Guthaben abgehoben. Deine Assets stehen Dir nach Ablauf der Frist für die Aufhebung der Bindung %s zur Verfügung. @@ -923,8 +928,10 @@ Rückgängig machen Dieser Vorgang ist derzeit nicht verfügbar. Bitte versuche es später noch einmal. Der Kauf von %s wird von aktuellen Anbietern nicht unterstützt, aber wir arbeiten daran, weitere Optionen hinzuzufügen. + Die Aktion ist für benutzerdefinierte Token nicht verfügbar. Du hast kein Guthaben um dies zu Verkaufen. Lade dein Konto auf, um Geld von dort zu verkaufen. Du hast kein Guthaben zum Versenden. Lade dein Konto auf, um Geld von dort aus senden zu können. + Die Daten wurden noch nicht geladen. Dies kann einige Sekunden dauern. Bitte versuche es später erneut. Das Tauschen von %s wird von aktuellen Anbietern nicht unterstützt, aber wir arbeiten daran, weitere Optionen hinzuzufügen. Der angezeigte Kontostand kann aufgrund der Zwischenspeicherung veraltet sein. Der Verkauf von Geldmitteln ist möglich, sobald die ausstehende(n) Transaktion(en) im %s -Netzwerk abgeschlossen ist/sind. @@ -973,6 +980,7 @@ Tangem Twin Diese Aktion ist unumkehrbar. Du hast keinen Zugriff mehr auf die alte Wallet. Tippe auf die Doppelkarte oder Ring mit der Nummer %s und entferne sie erst am Ende des Vorgangs. + Es ist ein Fehler aufgetreten. Fehlercode: %s. Bitte kontaktiere unseren Support. Verwende %s oder scanne eine Karte oder Ring, um Zugriff auf deine Wallet zu erhalten. Verbindung fehlgeschlagen: Diese dApp verwendet Wallet Connect Version 1.0, die nicht unterstützt wird. Bitte stelle sicher, dass die dApp Wallet Connect Version 2.0 unterstützt, um eine erfolgreiche Verbindung herzustellen. Bleib auf dem Laufenden mit den neuesten Funktionen und Neuigkeiten diff --git a/core/res/src/main/res/values-es/strings.xml b/core/res/src/main/res/values-es/strings.xml index ada8a5bd4d..bd31c5f775 100644 --- a/core/res/src/main/res/values-es/strings.xml +++ b/core/res/src/main/res/values-es/strings.xml @@ -241,6 +241,7 @@ Este mecanismo protege contra los ataques de proximidad a una tarjeta o anillo. Aplicará un retardo entre la recepción y la ejecución de un comando. Contraseña Antes de ejecutar un comando que cambie el estado de la tarjeta, deberá ingresar una contraseña. + NFT Programa de referidos Gire la pantalla de su dispositivo hacia abajo para ocultar y mostrar rápidamente los saldos %s hashes @@ -534,6 +535,7 @@ ¿Quiere salir del proceso de activación? Inicializando Ya se ha creado otra billetera en la tarjeta que está intentando agregar. Si tiene fondos en esta billetera, por favor retírelos y luego reinicie esta tarjeta y agréguela como backup. + Guardar su billetera Creando un backup Leer más sobre seed phrase @@ -666,6 +668,7 @@ Permitir el uso de biometría Se solicitará la biometría en lugar del código de acceso para interactuar con su billetera Código de acceso + No autorizar Parece que tiene la autenticación biométrica deshabilitada, es necesaria para guardar billeteras Activar la autorización biométrica ¿Le gustaría usar la biometría? @@ -917,6 +920,7 @@ Comprar %s no está disponible con los proveedores actuales pero estamos trabajando para agregar más opciones. No tiene fondos para vender. Recargue su cuenta para poder vender fondos desde ella. No tienes fondos para enviar. Recarga tu cuenta para poder enviar fondos desde ella. + Los datos aún no se han cargado. Esto puede tardar unos segundos. Por favor, inténtelo de nuevo más tarde. El servicio de intercambio %s no está compatible con los proveedores actuales pero estamos trabajando para agregar más opciones. La venta de fondos estará disponible una vez que la(s) transacción(es) pendiente(s) en la red %s se complete El envío de fondos estará disponible una vez que se completen las transacciones pendientes en la red %s. @@ -936,7 +940,7 @@ Cambie este token por otro por una tarifa de servicio de %1$s del %2$s al %3$s de febrero. Intercambio con Changelly, %s tarifa Intercambie ahora - Crypto-monnaies populaire 🔥 + Criptomonedas populares 🔥 No disponible para compra No disponible para vender No disponible para cambio desde %s diff --git a/core/res/src/main/res/values-fr/strings.xml b/core/res/src/main/res/values-fr/strings.xml index 529de205c8..8adef0ab59 100644 --- a/core/res/src/main/res/values-fr/strings.xml +++ b/core/res/src/main/res/values-fr/strings.xml @@ -241,6 +241,7 @@ Ce mécanisme protège contre les attaques sans contact sur la carte. Il y a un délai entre la réception et l\'exécution de la commande. Après la première transaction signée, ce téléphone sera associé à la carte et les transactions seront signées immédiatement. Mot de passe Avant d\'exécuter une commande qui modifie l\'état de la carte, vous devrez entrer un mot de passe. + NFT Programme de parrainage Retournez l\'écran de votre appareil vers le bas pour masquer et afficher rapidement les soldes %s hashes @@ -536,6 +537,7 @@ Voulez-vous quitter le processus d\'activation ? Initialisation en cours Un autre portefeuille a déjà été créé sur la carte que vous essayez d\'ajouter. Si vous avez des fonds dans ce portefeuille, veuillez les retirer, puis réinitialiser cette carte et l\'ajouter comme sauvegarde. + Enregistrer votre portefeuille Sauvegarde en cours En savoir plus sur les seed phrases @@ -668,6 +670,7 @@ Autoriser l\'utilisation de la biométrie La biométrie sera demandée à la place du code d\'accès pour les interactions avec votre portefeuille Code d\'accès + Ne pas autoriser Il semble que vous ayez désactivé l\'authentification biométrique, elle est nécessaire pour sauvegarder les portefeuilles Activer l\'autorisation biométrique Souhaitez-vous utiliser la biométrie ? @@ -919,6 +922,7 @@ L\'achat de %s n\'est pas pris en charge par les fournisseurs actuels mais nous travaillons à ajouter davantage d\'options. Vous n\'avez pas de fonds à vendre. Renflouez votre compte pour pouvoir vendre des fonds à partir de celui-ci. Vous n\'avez pas de fonds à envoyer. Renflouez votre compte pour pouvoir envoyer des fonds à partir de celui-ci. + Die Daten wurden noch nicht geladen. Dies kann einige Sekunden dauern. Bitte versuchen Sie es später noch einmal. Le service d\'échange %s n\'est pas pris en charge par les fournisseurs actuels mais nous travaillons à ajouter plus d\'options. La vente de fonds sera disponible une fois que la ou les transactions en attente dans le réseau %s seront terminées L\'envoi de fonds sera disponible une fois la ou les transactions en attente dans le réseau %s terminées. diff --git a/core/res/src/main/res/values-it/strings.xml b/core/res/src/main/res/values-it/strings.xml index 900ef94aa5..9b25886a73 100644 --- a/core/res/src/main/res/values-it/strings.xml +++ b/core/res/src/main/res/values-it/strings.xml @@ -19,6 +19,7 @@ Questo meccanismo protegge dagli avvicinamenti senza contatto sulla carta. Attiva un ritardo tra la ricezione e l\'esecuzione di un comando. Dopo la prima transazione firmata, questo telefono verrà associato alla carta e le transazioni verranno firmate immediatamente. Password Dovrai inserire una password prima di eseguire qualsiasi comando che modifichi lo stato della carta. + NFT Hash %s ID carta Valuta dell\'applicazione diff --git a/core/res/src/main/res/values-ja/strings.xml b/core/res/src/main/res/values-ja/strings.xml index aedabcb589..2ff3e6be5f 100644 --- a/core/res/src/main/res/values-ja/strings.xml +++ b/core/res/src/main/res/values-ja/strings.xml @@ -238,6 +238,7 @@ このメカニズムは、カードまたはリングに対する近接攻撃から保護します。コマンドの受信と実行の間に遅延を強制します。 パスコード カードの状態の変更を伴うコマンドを実行する前に、パスコードを入力する必要があります。 + NFT 紹介プログラム デバイスの画面を下に向けると、残高をすばやく非表示にしたり表示したりできます。 %sハッシュ @@ -530,6 +531,7 @@ アクティベーション処理を途中で終了しますか? スタート 追加しようとしているカードには、すでに別のウォレットが作成されています。このウォレットに資金がある場合は、それを引き出してからこのカードをリセットし、バックアップとして追加してください。 + ウォレットを保存する バックアップの作成 シードフレーズについてもっと読む @@ -658,6 +660,7 @@ 生体認証の使用を許可する ウォレットの操作には、アクセスコードの代わりに生体認証が要求されます。 アクセスコード + 許可しない 生体認証が無効になっているようです。ウォレットを保存する必要があります。 生体認証を有効にする 生体認証を使用しますか? @@ -768,6 +771,7 @@ %s 推定利益 市場評価 指標 + %1$sネットワークルールによれば、 %2$sからの請求が可能です。以下の金額は、ステーキング解除時にアカウントに入金されます。 最低要件 報酬はありません 請求中の報酬 @@ -814,6 +818,7 @@ 獲得した報酬をステーキングに再投資し、潜在的な収益を増やします。 再ステーキングを使うと、ステーキングを解除することなく、あるバリデータから別のバリデータに資金を移動できます。 残高のすべてをステーキングしようとしています。ステーキング解除や報酬請求にかかるネットワーク手数料をカバーするために、少額を残しておくことをお勧めします。 + TONでステーキングを開始するには、まず任意の金額の送金を行ってください。これにより、ウォレットがアクティブになります。 資金をステーキングから引き出すには、ロックを解除してください。ロック解除には%sかかります。 資金は、21日間のロック解除期間後に使用可能になります。報酬は、ロック解除後の資金とともに引き出されます。 %sのステーキング解除期間後、資金はすぐ利用可能となります。 @@ -916,7 +921,7 @@ カスタムトークンでは使用できません。 売却できる資金がありません。アカウントに入金して、売却できるようにしてください。 送金する資金がありません。アカウントに入金して、送金できるようにしてください。 - データはまだ読み込まれていません。数秒かかる場合があります。しばらくしてからもう一度お試しください。 + 現在データを読み込んでいます。これには数秒かかる場合があります。しばらくしてからもう一度お試しください。 %sのスワップは、現在のプロバイダーではサポートされていませんが、より多くのオプションを追加できるよう取り組んでいます。 表示残高はキャッシュにより古くなっている可能性があります。 %sネットワーク上の保留中の取引が完了すると、資金の売却が可能になります。 @@ -965,6 +970,7 @@ Tangemツイン この操作は元に戻せません。古いウォレットにはアクセスできなくなります。 番号%sのツインカードをタップし、操作が終了するまで取り外さないでください。 + エラーが発生しました。エラー コード:%s。サポートにお問い合わせください。 %sを使用するか、カード / リングをスキャンしてウォレットにアクセスしてください 接続に失敗しました:このdAppは、サポートされていないWallet Connectバージョン1.0を使用しています。正常に接続するには、dAppがWallet Connectバージョン2.0をサポートしていることを確認してください。 最新の機能とニュースをお届けします diff --git a/core/res/src/main/res/values-ru/strings.xml b/core/res/src/main/res/values-ru/strings.xml index 8793740014..b46553bb3b 100644 --- a/core/res/src/main/res/values-ru/strings.xml +++ b/core/res/src/main/res/values-ru/strings.xml @@ -247,6 +247,7 @@ Этот механизм защищает карту или кольцо от бесконтактных атак. Между сканированием и выполнением команды будет добавлена задержка. Пароль Перед выполнением любой команды, влекущей за собой изменение состояния карты, вам необходимо будет ввести пароль. + NFT Реферальная программа Переверните экран вашего устройства вниз, чтобы быстро скрыть и отобразить балансы %s хэшей @@ -546,6 +547,7 @@ Вы хотите выйти из процесса активации? Подготовка Другой кошелек уже был создан на карте, которую вы пытаетесь добавить. Если на нем есть средства, пожалуйста сначала выведите их, а затем сделайте сброс до заводских настроек и используйте как резервную. + Сохраните ваш кошелек Резервная копия Прочитать о seed-фразе @@ -639,7 +641,7 @@ %1$s (%2$s) в сети %3$s Использование другой сети может привести к утрате средств. Отправляйте только %s на этот адрес. Использование другой сети может привести к утрате средств. - Отправляйте только %1$s в сети %2$s + Отправляйте только %1$s в сети %2$s Переводите средства с любого кошелька или биржи Участвовать Не удалось загрузить информацию по реферальной программе. Пожалуйста, попробуйте позже. @@ -686,6 +688,7 @@ Использовать биометрию Для операций с вашим кошельком будет запрашиваться биометрия вместо кода доступа карты Код доступа + Не использовать Похоже, что у вас отключена биометрическая аутентификация, она необходима для сохранения кошельков Включите биометрическую аутентификацию Вы хотите использовать биометрию? @@ -838,6 +841,7 @@ Реинвестируйте свои заработанные награды в вашу застейканную сумму, увеличивая потенциальный доход Рестейк позволяет вам переместить средства из одного валидатора в другого без необходимости выхода из стейкинга. Вы собираетесь застейкать весь баланс, рекомендуем оставить небольшую сумму для оплаты комиссии сети при выходе из стейкинга или получении награды. + Чтобы начать стейкинг в TON, сначала осуществите исходящую транзакцию на любую сумму — это активирует ваш кошелек. Разблокируйте свои средства, чтобы вывести их из стейкинга. Разблокировка займёт %s. Ваши средства будут доступны для использования после 21-дневного периода отзыва. Награда будет выведена вместе с вашими выводими средствами. Ваши средства будут доступны после %s периода отзыва. @@ -938,6 +942,7 @@ Покупка %s в данный момент не поддерживается ни одним провайдером. Мы работаем над добавлением новых возможностей. Следите за нашими новостями. У вас нет средств для продажи. Пополните счет, чтобы иметь возможность продать с него средства. У вас нет средств для отправки. Пополните счет, чтобы иметь возможность отправить с него средства. + Данные в процессе загрузки. Это может занять несколько секунд. Пожалуйста, попробуйте позже. Обмен %s в данный момент не поддерживается ни одним провайдером. Мы работаем над добавлением новых возможностей. Следите за нашими новостями. Продажа средств станет доступной после завершения транзакции(-ий) в сети %s Отправка средств станет доступной после завершения транзакции(-ий) в сети %s @@ -985,6 +990,7 @@ Tangem Twin Это действие необратимо. У вас не будет доступа к старому кошельку. Приложите twin-карту с номером %s и не убирайте до окончания операции + Произошла ошибка. Код ошибки: %s. Попробуйте, пожалуйста, снова. Если проблема будет продолжать возникать — обратитесь в службу поддержки. Используйте %s или отсканируйте карту/кольцо, чтобы получить доступ к своему кошельку Соединение не удалось: это dApp использует Wallet Connect версии 1.0, которая не поддерживается. Убедитесь, что dApp поддерживает Wallet Connect версии 2.0 для успешного подключения. Будьте в курсе новых функций и новостей diff --git a/core/res/src/main/res/values-uk-rUA/strings.xml b/core/res/src/main/res/values-uk-rUA/strings.xml index 91701bc5c1..1d760fbe95 100644 --- a/core/res/src/main/res/values-uk-rUA/strings.xml +++ b/core/res/src/main/res/values-uk-rUA/strings.xml @@ -247,6 +247,7 @@ Цей механізм захищає картку або кільце від безконтактних атак. Між скануванням картки та виконанням команди буде додана затримка. Пароль Перед виконанням будь-якої команди, що тягне за собою зміну стану картки, вам необхідно буде ввести пароль. + NFT Реферальна програма Переверніть екран пристрою вниз, щоб швидко приховати та відобразити баланси %s хешів @@ -546,6 +547,7 @@ Ви хочете вийти з процесу активації? Підготовка На картці, яку ви намагаєтеся додати, вже створено інший гаманець. Якщо у вас є кошти на цьому гаманці, будь ласка, виведіть їх, а потім скиньте цю картку до заводських налаштувань і додайте її як резервну. + Збережіть ваш гаманець Резервна копія Дізнатися більше про seed-фразу @@ -686,6 +688,7 @@ Використовувати біометрію Для взаємодії з гаманцем будуть запитуватися біометричні дані замість коду доступу Код доступу + Не дозволяти Схоже, що у вас відключена біометрична автентифікація, вона необхідна для збереження гаманців Увімкніть біометричну автентифікацію Бажаєте використовувати біометрію? @@ -937,6 +940,7 @@ Наразі купівля монети %s недоступна. Слідкуйте за нашими оновленнями. У вас немає коштів для продажу. Поповніть рахунок, щоб мати змогу продати з нього кошти. У вас немає коштів для відправлення. Поповніть рахунок, щоб мати змогу надіслати з нього кошти. + Дані ще не завантажені. Це може зайняти кілька секунд. Будь ласка, спробуйте пізніше. Наразі обмін монети %s недоступний. Слідкуйте за нашими оновленнями. Продаж коштів стане доступним після завершення транзакції(-ій) в мережі %s Надсилання коштів стане доступним після завершення транзакції(-ій) в мережі %s diff --git a/core/res/src/main/res/values-zh-rTW/strings.xml b/core/res/src/main/res/values-zh-rTW/strings.xml index aaa494dbeb..d4ab1bc4e2 100644 --- a/core/res/src/main/res/values-zh-rTW/strings.xml +++ b/core/res/src/main/res/values-zh-rTW/strings.xml @@ -108,6 +108,7 @@ 此機制可防止對卡進行鄰近攻擊。它將強制執行命令的接收和執行之間的延遲。 密碼 在執行任何需要更改卡狀態的命令之前,您必須輸入密碼 + NFT 推薦計畫 %s hashes 卡號 @@ -176,6 +177,7 @@ 這此情況,您必須要重新開始 您想要離開啟用程序嗎? 開始 + 儲存你的錢包 創建備份 閱讀更多關於助記詞的訊息 @@ -241,6 +243,7 @@ 允許使用生物辨識 將要求生物識別而不是訪問密碼來與您的錢包進行交互 訪問密碼 + 不允許 您好像禁用了生物識別,有必要保存錢包 啟用生物識別授權 您想使用生物識別技術嗎? diff --git a/core/res/src/main/res/values/strings.xml b/core/res/src/main/res/values/strings.xml index 9b4f9524be..f4e57a45b5 100644 --- a/core/res/src/main/res/values/strings.xml +++ b/core/res/src/main/res/values/strings.xml @@ -537,6 +537,7 @@ Do you want to exit the activation process? Getting started Another wallet has already been created on the card you\'re trying to add. If you have funds in this wallet, please withdraw it and then reset this card and add it as a backup. + Save your wallet Creating a backup Read more about seed phrase @@ -669,6 +670,7 @@ Allow to use biometrics Biometrics will be requested instead of the access code for interactions with your wallet Access code + Don\'t allow It looks like you have biometric authentication disabled, it is necessary to save wallets Enable biometric authorization Would you like to use biometrics? @@ -779,6 +781,7 @@ %s est. profit Market rating Metrics + According to %1$s network rules, claims are possible from %2$s. Amounts below will be credited to your account upon unstaking. Minimum Requirement No rewards available Reward claiming @@ -825,6 +828,7 @@ Reinvests your earned rewards in your staked amount, increasing potential earnings. Restake lets you move your funds from one validator to another without the need to unstake You’re about to stake your entire balance. We recommend leaving a small amount to cover network fees for unstaking or claiming rewards. + To start staking in TON, first make an outgoing transaction of any amount — this will activate your wallet. Unlock your money to withdraw it from staking process. Unlocking takes %s. Your funds will be available for use after the 21-day unbonding period. Reward will be withdrawn along with your unstaking funds. Your funds will be available for use after the %s unbonding period. @@ -927,7 +931,7 @@ Action is not available for custom tokens. You do not have funds to sell. Top up your account to be able to sell funds from it. You do not have funds to send. Top up your account to be able to send funds from it. - The data has not loaded yet. This may take a few seconds. Please try again later. + The data is currently loading. This may take a few seconds. Please try again later. Swapping %s is not supported by current providers, but we are working to add more options. The displayed balance might be outdated due to caching. Selling funds will be available once the pending transaction(s) on the %s network is complete. @@ -976,6 +980,7 @@ Tangem Twin This action is irreversible. You will not have access to the old wallet. Tap the twin card with number %s and do not remove until the end of the operation + We\'ve encountered an error. Error code: %s. Please contact our support. Use %s or scan a card/ring to have access to your wallet Connection failed: This dApp uses Wallet Connect version 1.0, which is not supported. Please ensure the dApp supports Wallet Connect version 2.0 to connect successfully. Stay up to date with the latest features and news From 068cb45cb400cb83037a91a78ddd67b3f674339b Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 11 Mar 2025 12:48:32 +0300 Subject: [PATCH 5/9] Updated on 2026-08-14 --- .../tokens/GetCryptoCurrencyActionsUseCase.kt | 128 +++++++++++------- 1 file changed, 80 insertions(+), 48 deletions(-) diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetCryptoCurrencyActionsUseCase.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetCryptoCurrencyActionsUseCase.kt index 5e627d055b..981a56de13 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetCryptoCurrencyActionsUseCase.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetCryptoCurrencyActionsUseCase.kt @@ -124,7 +124,7 @@ class GetCryptoCurrencyActionsUseCase( } if (cryptoCurrencyStatus.value.sources.total != StatusSource.ACTUAL) { - return getActionsForOutdatedData(userWallet, cryptoCurrencyStatus, requirements) + return getActionsForOutdatedData(userWallet, cryptoCurrencyStatus, requirements, isStakingAvailable) } val activeList = mutableListOf() @@ -148,27 +148,7 @@ class GetCryptoCurrencyActionsUseCase( } // staking - if (isStakingAvailable) { - val yield = kotlin.runCatching { - stakingRepository.getYield( - cryptoCurrencyId = cryptoCurrency.id, - symbol = cryptoCurrency.symbol, - ) - }.getOrNull() - activeList.add( - TokenActionsState.ActionState.Stake( - unavailabilityReason = ScenarioUnavailabilityReason.None, - yield = yield, - ), - ) - } else { - disabledList.add( - TokenActionsState.ActionState.Stake( - unavailabilityReason = ScenarioUnavailabilityReason.StakingUnavailable(cryptoCurrency.name), - yield = null, - ), - ) - } + addStakingActions(cryptoCurrency, isStakingAvailable, activeList, disabledList) // send val sendUnavailabilityReason = getSendUnavailabilityReason( @@ -217,6 +197,35 @@ class GetCryptoCurrencyActionsUseCase( return activeList + disabledList } + private suspend fun addStakingActions( + cryptoCurrency: CryptoCurrency, + isStakingAvailable: Boolean, + activeList: MutableList, + disabledList: MutableList, + ) { + if (isStakingAvailable) { + val yield = kotlin.runCatching { + stakingRepository.getYield( + cryptoCurrencyId = cryptoCurrency.id, + symbol = cryptoCurrency.symbol, + ) + }.getOrNull() + activeList.add( + TokenActionsState.ActionState.Stake( + unavailabilityReason = ScenarioUnavailabilityReason.None, + yield = yield, + ), + ) + } else { + disabledList.add( + TokenActionsState.ActionState.Stake( + unavailabilityReason = ScenarioUnavailabilityReason.StakingUnavailable(cryptoCurrency.name), + yield = null, + ), + ) + } + } + private suspend fun getActionsForUnreachableCurrency( userWallet: UserWallet, cryptoCurrencyStatus: CryptoCurrencyStatus, @@ -256,12 +265,16 @@ class GetCryptoCurrencyActionsUseCase( return actionsList } + @Suppress("LongMethod") private suspend fun getActionsForOutdatedData( userWallet: UserWallet, cryptoCurrencyStatus: CryptoCurrencyStatus, requirements: AssetRequirementsCondition?, + isStakingAvailable: Boolean, ): List { val activeList = mutableListOf() + val disabledList = mutableListOf() + val cryptoCurrency = cryptoCurrencyStatus.currency // copy address if (isAddressAvailable(cryptoCurrencyStatus.value.networkAddress)) { @@ -271,7 +284,12 @@ class GetCryptoCurrencyActionsUseCase( // receive if (isAddressAvailable(cryptoCurrencyStatus.value.networkAddress)) { val scenario = getReceiveScenario(requirements) - activeList.add(TokenActionsState.ActionState.Receive(scenario)) + val action = TokenActionsState.ActionState.Receive(scenario) + if (scenario == ScenarioUnavailabilityReason.None) { + activeList.add(action) + } else { + disabledList.add(action) + } } // swap @@ -280,25 +298,28 @@ class GetCryptoCurrencyActionsUseCase( quoteSource.isActual() && networkSource.isActual() } - activeList.add( - TokenActionsState.ActionState.Swap( - unavailabilityReason = if (isSwapAvailable) { - ScenarioUnavailabilityReason.None - } else if (sources.networkSource == StatusSource.ONLY_CACHE) { - ScenarioUnavailabilityReason.UsedOutdatedData - } else { - // CACHE source always when loading - ScenarioUnavailabilityReason.DataLoading - }, - showBadge = false, - ), + val swapAction = TokenActionsState.ActionState.Swap( + unavailabilityReason = if (isSwapAvailable) { + ScenarioUnavailabilityReason.None + } else if (sources.networkSource == StatusSource.ONLY_CACHE) { + ScenarioUnavailabilityReason.UsedOutdatedData + } else { + // CACHE source always when loading + ScenarioUnavailabilityReason.DataLoading + }, + showBadge = false, ) + if (swapAction.unavailabilityReason == ScenarioUnavailabilityReason.None) { + activeList.add(swapAction) + } else { + disabledList.add(swapAction) + } // buy - if (rampManager.availableForBuy(userWallet.scanResponse, userWallet.walletId, cryptoCurrencyStatus.currency)) { + if (rampManager.availableForBuy(userWallet.scanResponse, userWallet.walletId, cryptoCurrency)) { activeList.add(TokenActionsState.ActionState.Buy(ScenarioUnavailabilityReason.None)) } else { - activeList.add( + disabledList.add( TokenActionsState.ActionState.Buy( ScenarioUnavailabilityReason.BuyUnavailable( cryptoCurrencyName = cryptoCurrencyStatus.currency.name, @@ -308,29 +329,40 @@ class GetCryptoCurrencyActionsUseCase( } // staking - activeList.add(TokenActionsState.ActionState.Stake(ScenarioUnavailabilityReason.UsedOutdatedData, null)) + if (cryptoCurrencyStatus.value.sources.networkSource.isActual()) { + addStakingActions(cryptoCurrency, isStakingAvailable, activeList, disabledList) + } else { + disabledList.add(TokenActionsState.ActionState.Stake(ScenarioUnavailabilityReason.UsedOutdatedData, null)) + } // send val isSendAvailable = cryptoCurrencyStatus.value.sources.networkSource.isActual() - activeList.add( - TokenActionsState.ActionState.Send( - unavailabilityReason = if (isSendAvailable) { - ScenarioUnavailabilityReason.None - } else { - ScenarioUnavailabilityReason.UsedOutdatedData - }, - ), + val sendAction = TokenActionsState.ActionState.Send( + unavailabilityReason = if (isSendAvailable) { + ScenarioUnavailabilityReason.None + } else { + ScenarioUnavailabilityReason.UsedOutdatedData + }, ) + if (sendAction.unavailabilityReason == ScenarioUnavailabilityReason.None) { + activeList.add(sendAction) + } else { + disabledList.add(sendAction) + } // region sell - activeList.add(TokenActionsState.ActionState.Sell(ScenarioUnavailabilityReason.UsedOutdatedData)) + if (isSendAvailable) { + activeList.add(TokenActionsState.ActionState.Sell(ScenarioUnavailabilityReason.None)) + } else { + disabledList.add(TokenActionsState.ActionState.Sell(ScenarioUnavailabilityReason.UsedOutdatedData)) + } // endregion // hide activeList.add(TokenActionsState.ActionState.HideToken(ScenarioUnavailabilityReason.None)) - return activeList + return activeList + disabledList } private fun getReceiveScenario(requirements: AssetRequirementsCondition?): ScenarioUnavailabilityReason { From 19fdd19139b1e0cca0fecb0b4b15da34d797b40f Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 11 Mar 2025 13:38:00 +0300 Subject: [PATCH 6/9] Updated on 2026-08-14 --- .../tokens/operations/CachedCurrenciesStatusesOperations.kt | 2 +- .../domain/tokens/operations/CurrenciesStatusesOperations.kt | 3 +++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/CachedCurrenciesStatusesOperations.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/CachedCurrenciesStatusesOperations.kt index c06a0481bd..be0fdb52a2 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/CachedCurrenciesStatusesOperations.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/CachedCurrenciesStatusesOperations.kt @@ -251,6 +251,6 @@ class CachedCurrenciesStatusesOperations( } companion object { - private const val RETRY_DELAY = 2000L + internal const val RETRY_DELAY = 2000L } } \ No newline at end of file diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/CurrenciesStatusesOperations.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/CurrenciesStatusesOperations.kt index e71d7a55f4..eeeb5b4e59 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/CurrenciesStatusesOperations.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/CurrenciesStatusesOperations.kt @@ -9,10 +9,12 @@ import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.domain.tokens.model.Network import com.tangem.domain.tokens.model.NetworkStatus import com.tangem.domain.tokens.model.Quote +import com.tangem.domain.tokens.operations.CachedCurrenciesStatusesOperations.Companion.RETRY_DELAY import com.tangem.domain.tokens.repository.CurrenciesRepository import com.tangem.domain.tokens.repository.NetworksRepository import com.tangem.domain.tokens.repository.QuotesRepository import com.tangem.domain.wallets.models.UserWalletId +import kotlinx.coroutines.delay import kotlinx.coroutines.flow.* class CurrenciesStatusesOperations( @@ -29,6 +31,7 @@ class CurrenciesStatusesOperations( } .retryWhen { cause, _ -> emit(Error.DataError(cause).left()) + delay(RETRY_DELAY) true } } From 078e215ad7f69ff64d93568fecc9af30bbcec6fe Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 11 Mar 2025 14:03:37 +0400 Subject: [PATCH 7/9] Updated on 2026-08-14 --- .../local/token/DefaultStakingBalanceStore.kt | 152 ++++++++++++------ .../local/token/StakingBalanceStore.kt | 29 ++-- .../token/converter/YieldBalanceConverter.kt | 10 +- .../data/staking/DefaultStakingRepository.kt | 115 ++++++++----- .../CachedCurrenciesStatusesOperations.kt | 8 +- .../impl/presentation/model/StakingModel.kt | 65 +++++--- .../state/converters/BalanceItemConverter.kt | 3 +- .../RewardsValidatorStateConverter.kt | 4 +- .../converters/YieldBalancesConverter.kt | 6 +- .../state/helpers/StakingBalanceUpdater.kt | 2 +- .../SetConfirmationStateAssentTransformer.kt | 5 +- ...etConfirmationStateCompletedTransformer.kt | 11 +- .../SetConfirmationStateInitTransformer.kt | 4 +- ...ConfirmationStateResetAssentTransformer.kt | 9 +- .../SetInitialDataStateTransformer.kt | 37 +++-- ...firmationStateAssentApprovalTransformer.kt | 5 +- .../AddStakingNotificationsTransformer.kt | 6 +- ...TokenDetailsBalanceSelectStateConverter.kt | 7 +- 18 files changed, 318 insertions(+), 160 deletions(-) diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/token/DefaultStakingBalanceStore.kt b/core/datasource/src/main/java/com/tangem/datasource/local/token/DefaultStakingBalanceStore.kt index fbe1a1435b..15acc9d99b 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/local/token/DefaultStakingBalanceStore.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/local/token/DefaultStakingBalanceStore.kt @@ -3,6 +3,7 @@ package com.tangem.datasource.local.token import androidx.datastore.core.DataStore import com.tangem.datasource.api.stakekit.models.response.model.YieldBalanceWrapperDTO import com.tangem.datasource.local.datastore.RuntimeSharedStore +import com.tangem.datasource.local.token.StakingBalanceStore.StakingID import com.tangem.datasource.local.token.converter.YieldBalanceConverter import com.tangem.domain.models.StatusSource import com.tangem.domain.staking.model.stakekit.YieldBalance @@ -26,10 +27,16 @@ internal class DefaultStakingBalanceStore( private val runtimeStore: RuntimeSharedStore, ) : StakingBalanceStore { - override fun get(userWalletId: UserWalletId): Flow> = channelFlow { + override fun get(userWalletId: UserWalletId, stakingIds: List): Flow> = channelFlow { val cachedBalances = persistenceStore.data .map { val wrappers = it[userWalletId.stringValue].orEmpty() + .filter { wrapper -> + stakingIds.any { id -> + id.address == wrapper.addresses.address && id.integrationId == wrapper.integrationId + } + } + YieldBalanceConverter(isCached = true).convertSet(input = wrappers) } .firstOrNull() @@ -40,33 +47,64 @@ internal class DefaultStakingBalanceStore( } runtimeStore.get() - .map { it[userWalletId].orEmpty() } + .map { + it[userWalletId].orEmpty().filter { balance -> + stakingIds.any { id -> + id.address == balance.address && id.integrationId == balance.integrationId + } + } + .toSet() + } .onEach { - val mergedBalances = mergeYieldBalances(cachedBalances = cachedBalances, runtimeBalances = it) + val mergedBalances = mergeYieldBalances( + stakingIds = stakingIds, + cachedBalances = cachedBalances, + runtimeBalances = it, + ) send(mergedBalances) } .launchIn(scope = this) } - override fun get(userWalletId: UserWalletId, address: String, integrationId: String): Flow { - return get(userWalletId).map { balances -> - balances.getBalance(address = address, integrationId = integrationId) + override fun get(userWalletId: UserWalletId, stakingID: StakingID): Flow { + return get(userWalletId = userWalletId, stakingIds = listOf(stakingID)).map { balances -> + balances.getBalance(stakingID = stakingID) } } override suspend fun getSyncOrNull(userWalletId: UserWalletId): Set? { - return runtimeStore.getSyncOrNull()?.getValue(userWalletId) + val runtimeBalances = runtimeStore.getSyncOrNull()?.getValue(userWalletId).orEmpty() + val cachedBalances = persistenceStore.data.firstOrNull()?.get(userWalletId.stringValue).orEmpty() + + if (runtimeBalances.isEmpty() && cachedBalances.isEmpty()) return null + + return cachedBalances.mapTo(hashSetOf()) { + val cached = YieldBalanceConverter(source = StatusSource.ONLY_CACHE).convert(value = it) + val runtime = runtimeBalances.getBalance(address = cached.address, integrationId = cached.integrationId) + + if (runtime == null || runtime is YieldBalance.Error) cached else runtime + } } - override suspend fun getSyncOrNull( - userWalletId: UserWalletId, - address: String, - integrationId: String, - ): YieldBalance? { - val balances = getSyncOrNull(userWalletId) ?: return null + override suspend fun getSyncOrNull(userWalletId: UserWalletId, stakingIds: List): Set? { + val runtime = runtimeStore.getSyncOrNull()?.getValue(userWalletId) + val cached = persistenceStore.data.firstOrNull()?.get(userWalletId.stringValue) - return balances.getBalance(address, integrationId) + if (runtime.isNullOrEmpty() && cached.isNullOrEmpty()) return null + + return mergeYieldBalances( + cachedBalances = YieldBalanceConverter(source = StatusSource.ONLY_CACHE) + .convertSet(input = cached.orEmpty()), + runtimeBalances = runtime.orEmpty(), + stakingIds = stakingIds, + ) + } + + override suspend fun getSyncOrNull(userWalletId: UserWalletId, stakingID: StakingID): YieldBalance? { + val balances = getSyncOrNull(userWalletId = userWalletId, stakingIds = listOf(stakingID)) ?: return null + + return balances.getBalance(stakingID = stakingID) } override suspend fun store(userWalletId: UserWalletId, items: Set) { @@ -88,34 +126,52 @@ internal class DefaultStakingBalanceStore( } } - override suspend fun refresh(userWalletId: UserWalletId, addressWithIntegrationIdMap: Map) { + override suspend fun refresh(userWalletId: UserWalletId, stakingIds: List) { updateRuntimeStore(userWalletId = userWalletId) { saved -> - saved.mapTo(hashSetOf()) { balance -> - val refreshIntegrationId = addressWithIntegrationIdMap[balance.address] + saved.mapTo(hashSetOf()) { + val yieldBalance = it.takeIf { balance -> + stakingIds.any { id -> balance.integrationId == id.integrationId && balance.address == id.address } + } - if (balance.integrationId == refreshIntegrationId) { - when (balance) { - is YieldBalance.Data -> balance.copy(source = StatusSource.CACHE) - is YieldBalance.Empty -> balance.copy(source = StatusSource.CACHE) - is YieldBalance.Error -> balance + if (yieldBalance != null) { + when (yieldBalance) { + is YieldBalance.Data -> yieldBalance.copy(source = StatusSource.CACHE) + is YieldBalance.Empty -> yieldBalance.copy(source = StatusSource.CACHE) + is YieldBalance.Error -> yieldBalance } } else { - balance + it } } } } - override suspend fun store( - userWalletId: UserWalletId, - integrationId: String, - address: String, - item: YieldBalanceWrapperDTO, - ) { + override suspend fun store(userWalletId: UserWalletId, stakingID: StakingID, item: YieldBalanceWrapperDTO) { coroutineScope { launch { - storeInRuntimeStore(userWalletId, integrationId, address, item) - storeInPersistenceStore(userWalletId, integrationId, address, item) + storeInRuntimeStore( + userWalletId = userWalletId, + integrationId = stakingID.integrationId, + address = stakingID.address, + item = item, + ) + + storeInPersistenceStore( + userWalletId = userWalletId, + integrationId = stakingID.integrationId, + address = stakingID.address, + item = item, + ) + } + } + } + + override suspend fun storeSingleYieldBalance(userWalletId: UserWalletId, item: YieldBalance) { + runtimeStore.update(default = emptyMap()) { saved -> + saved.toMutableMap().apply { + this[userWalletId] = saved[userWalletId] + ?.addOrReplace(item) { it.integrationId == item.integrationId && it.address == item.address } + ?: setOf(item) } } } @@ -178,28 +234,22 @@ internal class DefaultStakingBalanceStore( private fun mergeYieldBalances( cachedBalances: Set, runtimeBalances: Set, + stakingIds: List, ): Set { - if (runtimeBalances.isEmpty()) { - return cachedBalances.mapNotNullTo(hashSetOf()) { - when (it) { - is YieldBalance.Data -> it.copy(source = StatusSource.ONLY_CACHE) - is YieldBalance.Empty -> it.copy(source = StatusSource.ONLY_CACHE) - is YieldBalance.Error -> null - } + return stakingIds.mapTo(hashSetOf()) { id -> + val runtime = runtimeBalances.getBalance(stakingID = id) + + if (runtime == null || runtime is YieldBalance.Error) { + getCachedBalanceIfPossible(cachedBalances = cachedBalances, stakingID = id) + } else { + runtime } } - - return runtimeBalances - .map { runtime -> - runtime.takeIf { runtime !is YieldBalance.Error } - ?: getCachedBalanceIfPossible(cachedBalances, runtime) - } - .toSet() } - private fun getCachedBalanceIfPossible(cachedBalances: Set, runtime: YieldBalance): YieldBalance { - val cached = cachedBalances.getBalance(address = runtime.address, integrationId = runtime.integrationId) - ?: return runtime + private fun getCachedBalanceIfPossible(cachedBalances: Set, stakingID: StakingID): YieldBalance { + val cached = cachedBalances.getBalance(stakingID) + ?: return YieldBalance.Error(integrationId = stakingID.address, address = stakingID.integrationId) val updatedCached = when (cached) { is YieldBalance.Data -> cached.copy(source = StatusSource.ONLY_CACHE) @@ -207,7 +257,11 @@ internal class DefaultStakingBalanceStore( is YieldBalance.Error -> null } - return updatedCached ?: runtime + return updatedCached ?: YieldBalance.Error(integrationId = stakingID.address, address = stakingID.integrationId) + } + + private fun Set.getBalance(stakingID: StakingID): YieldBalance? { + return getBalance(address = stakingID.address, integrationId = stakingID.integrationId) } private fun Set.getBalance(address: String?, integrationId: String?): YieldBalance? { diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/token/StakingBalanceStore.kt b/core/datasource/src/main/java/com/tangem/datasource/local/token/StakingBalanceStore.kt index 7722a21c10..779536d430 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/local/token/StakingBalanceStore.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/local/token/StakingBalanceStore.kt @@ -9,23 +9,32 @@ import kotlinx.coroutines.flow.Flow /** Staking balance store */ interface StakingBalanceStore { - /** Get flow of [YieldBalanceList] by [userWalletId] */ - fun get(userWalletId: UserWalletId): Flow> + /** Get flow of [YieldBalanceList] by [userWalletId] and [stakingIds] */ + fun get(userWalletId: UserWalletId, stakingIds: List): Flow> - /** Get flow of [YieldBalance] by [userWalletId], [address] and [integrationId] */ - fun get(userWalletId: UserWalletId, address: String, integrationId: String): Flow + /** Get flow of [YieldBalance] by [userWalletId] and [stakingID] */ + fun get(userWalletId: UserWalletId, stakingID: StakingID): Flow - /** Get [YieldBalanceList] synchronously or null by [userWalletId] */ + /** Get all [YieldBalance] synchronously or null by [userWalletId] */ suspend fun getSyncOrNull(userWalletId: UserWalletId): Set? - /** Get [YieldBalance] synchronously or null by [userWalletId], [address] and [integrationId] */ - suspend fun getSyncOrNull(userWalletId: UserWalletId, address: String, integrationId: String): YieldBalance? + /** Get [YieldBalanceList] synchronously or null by [userWalletId] and [stakingIds] */ + suspend fun getSyncOrNull(userWalletId: UserWalletId, stakingIds: List): Set? + + /** Get [YieldBalance] synchronously or null by [userWalletId] and [stakingID] */ + suspend fun getSyncOrNull(userWalletId: UserWalletId, stakingID: StakingID): YieldBalance? /** Store [items] by [userWalletId] */ suspend fun store(userWalletId: UserWalletId, items: Set) - /** Store [item] by [userWalletId], [integrationId] and [address] */ - suspend fun store(userWalletId: UserWalletId, integrationId: String, address: String, item: YieldBalanceWrapperDTO) + /** Store [item] by [userWalletId] and [stakingID] */ + suspend fun store(userWalletId: UserWalletId, stakingID: StakingID, item: YieldBalanceWrapperDTO) - suspend fun refresh(userWalletId: UserWalletId, addressWithIntegrationIdMap: Map) + /** Store [item] by [userWalletId] */ + suspend fun storeSingleYieldBalance(userWalletId: UserWalletId, item: YieldBalance) + + /** Refresh balances of [stakingIds] by [userWalletId] */ + suspend fun refresh(userWalletId: UserWalletId, stakingIds: List) + + data class StakingID(val integrationId: String, val address: String) } \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/token/converter/YieldBalanceConverter.kt b/core/datasource/src/main/java/com/tangem/datasource/local/token/converter/YieldBalanceConverter.kt index 9df25f3294..6fc465801a 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/local/token/converter/YieldBalanceConverter.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/local/token/converter/YieldBalanceConverter.kt @@ -7,14 +7,18 @@ import com.tangem.domain.staking.model.stakekit.YieldBalance import com.tangem.domain.staking.model.stakekit.YieldBalanceItem import com.tangem.utils.converter.Converter -internal class YieldBalanceConverter(private val isCached: Boolean) : Converter { +internal class YieldBalanceConverter( + private val source: StatusSource, +) : Converter { + + constructor(isCached: Boolean) : this(source = if (isCached) StatusSource.CACHE else StatusSource.ACTUAL) override fun convert(value: YieldBalanceWrapperDTO): YieldBalance { return if (value.balances.isEmpty()) { YieldBalance.Empty( integrationId = value.integrationId, address = value.addresses.address, - source = if (isCached) StatusSource.CACHE else StatusSource.ACTUAL, + source = source, ) } else { YieldBalance.Data( @@ -40,7 +44,7 @@ internal class YieldBalanceConverter(private val isCached: Boolean) : Converter< .sortedWith(compareBy({ it.type }, { it.amount })), integrationId = value.integrationId, ), - source = if (isCached) StatusSource.CACHE else StatusSource.ACTUAL, + source = source, ) } } diff --git a/data/staking/src/main/java/com/tangem/data/staking/DefaultStakingRepository.kt b/data/staking/src/main/java/com/tangem/data/staking/DefaultStakingRepository.kt index 8482f9a190..876e516350 100644 --- a/data/staking/src/main/java/com/tangem/data/staking/DefaultStakingRepository.kt +++ b/data/staking/src/main/java/com/tangem/data/staking/DefaultStakingRepository.kt @@ -372,20 +372,33 @@ internal class DefaultStakingRepository( } val requestBody = getBalanceRequestData(address, integrationId) - val result = stakeKitApi.getSingleYieldBalance( - integrationId = requestBody.integrationId, - body = requestBody, - ).getOrThrow() - stakingBalanceStore.store( - userWalletId = userWalletId, - integrationId = requestBody.integrationId, - address = address, - item = YieldBalanceWrapperDTO( - balances = result, - integrationId = requestBody.integrationId, - addresses = requestBody.addresses, - ), + safeApiCall( + call = { + val result = stakeKitApi.getSingleYieldBalance( + integrationId = requestBody.integrationId, + body = requestBody, + ).bind() + + stakingBalanceStore.store( + userWalletId = userWalletId, + stakingID = StakingBalanceStore.StakingID( + integrationId = requestBody.integrationId, + address = address, + ), + item = YieldBalanceWrapperDTO( + balances = result, + integrationId = requestBody.integrationId, + addresses = requestBody.addresses, + ), + ) + }, + onError = { + stakingBalanceStore.storeSingleYieldBalance( + userWalletId = userWalletId, + item = YieldBalance.Error(integrationId = requestBody.integrationId, address = address), + ) + }, ) }, ) @@ -399,7 +412,11 @@ internal class DefaultStakingRepository( val address = walletManagersFacade.getDefaultAddress(userWalletId, cryptoCurrency.network).orEmpty() val integrationId = integrationIdMap[getIntegrationKey(cryptoCurrency.id)] ?: error("Could not get integrationId") - stakingBalanceStore.get(userWalletId, address, integrationId) + + stakingBalanceStore.get( + userWalletId = userWalletId, + stakingID = StakingBalanceStore.StakingID(integrationId = integrationId, address = address), + ) .distinctUntilChanged() .collectLatest { if (it != null) { @@ -413,10 +430,7 @@ internal class DefaultStakingRepository( } withContext(dispatchers.io) { - fetchSingleYieldBalance( - userWalletId, - cryptoCurrency, - ) + fetchSingleYieldBalance(userWalletId = userWalletId, cryptoCurrency = cryptoCurrency) } }.cancellable() @@ -431,7 +445,10 @@ internal class DefaultStakingRepository( val integrationId = integrationIdMap[getIntegrationKey(cryptoCurrency.id)] ?: error("Could not get integrationId") - stakingBalanceStore.getSyncOrNull(userWalletId, address, integrationId) + stakingBalanceStore.getSyncOrNull( + userWalletId = userWalletId, + stakingID = StakingBalanceStore.StakingID(integrationId = integrationId, address = address), + ) ?: YieldBalance.Error(integrationId, address) } @@ -444,21 +461,7 @@ internal class DefaultStakingRepository( if (refresh) { stakingBalanceStore.refresh( userWalletId = userWalletId, - addressWithIntegrationIdMap = cryptoCurrencies - .mapNotNull { currency -> - val addresses = walletManagersFacade.getAddresses(userWalletId, currency.network) - val integrationId = integrationIdMap[getIntegrationKey(currency.id)] - - if (integrationId != null) { - addresses to integrationId - } else { - null - } - } - .flatMap { (addresses, integrationId) -> - addresses.map { address -> integrationId to address.value } - } - .toMap(), + stakingIds = cryptoCurrencies.mapStakingId(userWalletId), ) } @@ -523,20 +526,52 @@ internal class DefaultStakingRepository( ) } + private suspend fun List.mapStakingId( + userWalletId: UserWalletId, + ): List { + return this + .mapNotNull { currency -> + val addresses = walletManagersFacade.getAddresses(userWalletId, currency.network) + val integrationId = integrationIdMap[getIntegrationKey(currency.id)] + + if (integrationId != null) { + addresses to integrationId + } else { + null + } + } + .flatMap { (addresses, integrationId) -> + addresses.map { address -> + StakingBalanceStore.StakingID( + integrationId = integrationId, + address = address.value, + ) + } + } + } + override fun getMultiYieldBalanceUpdates( userWalletId: UserWalletId, cryptoCurrencies: List, ): Flow { - return stakingBalanceStore.get(userWalletId) - .map(YieldBalanceListConverter::convert) - .flowOn(dispatchers.io) + return flow { + stakingBalanceStore.get( + userWalletId = userWalletId, + stakingIds = cryptoCurrencies.mapStakingId(userWalletId), + ) + .map(YieldBalanceListConverter::convert) + .collect { emit(it) } + } } override fun getMultiYieldBalanceUpdatesLegacy( userWalletId: UserWalletId, cryptoCurrencies: List, ): Flow = channelFlow { - stakingBalanceStore.get(userWalletId) + stakingBalanceStore.get( + userWalletId = userWalletId, + stakingIds = cryptoCurrencies.mapStakingId(userWalletId), + ) .onEach { val balances = YieldBalanceListConverter.convert(it) send(balances) @@ -554,7 +589,8 @@ internal class DefaultStakingRepository( ): YieldBalanceList = withContext(dispatchers.io) { fetchMultiYieldBalance(userWalletId, cryptoCurrencies) - stakingBalanceStore.getSyncOrNull(userWalletId)?.let(YieldBalanceListConverter::convert) + stakingBalanceStore.getSyncOrNull(userWalletId, cryptoCurrencies.mapStakingId(userWalletId)) + ?.let(YieldBalanceListConverter::convert) ?: YieldBalanceList.Error } @@ -697,6 +733,7 @@ internal class DefaultStakingRepository( } } + @Suppress("unused") private companion object { const val YIELDS_STORE_KEY = "yields" diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/CachedCurrenciesStatusesOperations.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/CachedCurrenciesStatusesOperations.kt index be0fdb52a2..739ad63738 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/CachedCurrenciesStatusesOperations.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/CachedCurrenciesStatusesOperations.kt @@ -112,7 +112,13 @@ class CachedCurrenciesStatusesOperations( val rawCurrenciesIds = currenciesIds.mapNotNullTo(mutableSetOf()) { it.rawCurrencyId } quotesRepository.fetchQuotes(rawCurrenciesIds) }, - async { stakingRepository.fetchMultiYieldBalance(userWalletId, currencies) }, + async { + if (currencies.size == 1) { + stakingRepository.fetchSingleYieldBalance(userWalletId, currencies.first()) + } else { + stakingRepository.fetchMultiYieldBalance(userWalletId, currencies) + } + }, ) } .map { } diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/model/StakingModel.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/model/StakingModel.kt index 01d1f570c7..3a4f7fe8ba 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/model/StakingModel.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/model/StakingModel.kt @@ -122,7 +122,7 @@ internal class StakingModel @Inject constructor( private val shareManager: ShareManager, @DelayedWork private val coroutineScope: CoroutineScope, private val innerRouter: InnerStakingRouter, - private val appRouter: AppRouter, + appRouter: AppRouter, ) : Model(), StakingClickIntents { val uiState: StateFlow = stateController.uiState @@ -207,6 +207,7 @@ internal class StakingModel @Inject constructor( private val transactionsInProgress: CopyOnWriteArrayList = CopyOnWriteArrayList() + private var actionsJobHolder: JobHolder = JobHolder() private var approvalJobHolder: JobHolder = JobHolder() private var feeJobHolder: JobHolder = JobHolder() private var sendTransactionJobHolder = JobHolder() @@ -274,6 +275,7 @@ internal class StakingModel @Inject constructor( appCurrencyProvider = Provider { appCurrency }, feeCryptoCurrencyStatus = feeCryptoCurrencyStatus, fee = gasEstimate, + cryptoCurrencyStatus = cryptoCurrencyStatus, ), ) updateNotifications() @@ -293,6 +295,7 @@ internal class StakingModel @Inject constructor( appCurrencyProvider = Provider { appCurrency }, feeCryptoCurrencyStatus = feeCryptoCurrencyStatus, fee = fee, + cryptoCurrencyStatus = cryptoCurrencyStatus, ), ) updateNotifications() @@ -312,25 +315,26 @@ internal class StakingModel @Inject constructor( }, onConstructError = { error -> stakingEventFactory.createStakingErrorAlert(error) - stateController.update(SetConfirmationStateResetAssentTransformer) + stateController.update(SetConfirmationStateResetAssentTransformer(cryptoCurrencyStatus)) }, onSendSuccess = { txUrl -> stakingAnalyticSender.sendTransactionStakingAnalytics(stateController.value) transactionsInProgress.clear() - stateController.update(SetConfirmationStateCompletedTransformer(txUrl)) + stateController.update(SetConfirmationStateCompletedTransformer(txUrl, cryptoCurrencyStatus)) }, onSendError = { error -> analyticsEventHandler.send(StakingAnalyticsEvent.TransactionError) stakingEventFactory.createSendTransactionErrorAlert(error) - stateController.update(SetConfirmationStateResetAssentTransformer) + stateController.update(SetConfirmationStateResetAssentTransformer(cryptoCurrencyStatus)) }, onFeeIncreased = { increasedFee -> stateController.updateAll( - SetConfirmationStateResetAssentTransformer, + SetConfirmationStateResetAssentTransformer(cryptoCurrencyStatus), SetConfirmationStateAssentTransformer( appCurrencyProvider = Provider { appCurrency }, feeCryptoCurrencyStatus = feeCryptoCurrencyStatus, fee = increasedFee, + cryptoCurrencyStatus = cryptoCurrencyStatus, ), ) stateController.updateEvent( @@ -355,7 +359,9 @@ internal class StakingModel @Inject constructor( } if (!isApprovalInProgress) { stakingStateRouter.onPrevClick() - stateController.update(SetConfirmationStateResetAssentTransformer) + stateController.update( + SetConfirmationStateResetAssentTransformer(cryptoCurrencyStatus = cryptoCurrencyStatus), + ) } } null, @@ -545,6 +551,7 @@ internal class StakingModel @Inject constructor( stateController.update(SetApprovalBottomSheetTypeChangeTransformer(approveType)) } + @Suppress("LongMethod") override fun onApprovalClick() { modelScope.launch { stateController.update( @@ -581,10 +588,13 @@ internal class StakingModel @Inject constructor( appCurrencyProvider = Provider { appCurrency }, feeCryptoCurrencyStatus = feeCryptoCurrencyStatus, fee = TransactionFee.Single(fee), + cryptoCurrencyStatus = cryptoCurrencyStatus, ), ) stakingEventFactory.createGenericErrorAlert(error.message ?: error.toString()) - stateController.update(SetConfirmationStateResetAssentTransformer) + stateController.update( + SetConfirmationStateResetAssentTransformer(cryptoCurrencyStatus = cryptoCurrencyStatus), + ) return@launch }, ifRight = { it }, @@ -603,10 +613,13 @@ internal class StakingModel @Inject constructor( appCurrencyProvider = Provider { appCurrency }, feeCryptoCurrencyStatus = feeCryptoCurrencyStatus, fee = TransactionFee.Single(fee), + cryptoCurrencyStatus = cryptoCurrencyStatus, ), ) stakingEventFactory.createSendTransactionErrorAlert(error) - stateController.update(SetConfirmationStateResetAssentTransformer) + stateController.update( + SetConfirmationStateResetAssentTransformer(cryptoCurrencyStatus = cryptoCurrencyStatus), + ) }, ifRight = { stakingAnalyticSender.sendTransactionApprovalAnalytics(tokenCryptoCurrency) @@ -851,10 +864,16 @@ internal class StakingModel @Inject constructor( }, ifLeft = { stakingEventFactory.createGenericErrorAlert(it.toString()) - stateController.update(SetConfirmationStateResetAssentTransformer) + stateController.update( + SetConfirmationStateResetAssentTransformer(cryptoCurrencyStatus = cryptoCurrencyStatus), + ) }, ) - getCurrencyStatusUpdatesUseCase(userWalletId, cryptoCurrencyId, false) + getCurrencyStatusUpdatesUseCase( + userWalletId = userWalletId, + currencyId = cryptoCurrencyId, + isSingleWalletWithTokens = false, + ) .conflate() .distinctUntilChangedBy { it.getOrNull()?.value?.yieldBalance } .filter { value.currentStep == StakingStep.InitialInfo } @@ -888,12 +907,14 @@ internal class StakingModel @Inject constructor( setupApprovalNeeded() setupIsAnyTokenStaked() checkIfSubtractAvailable() - subscribeOnActionsUpdates() - subscribeOnStepChanges() + subscribeOnActionsUpdates(status) + subscribeOnStepChanges(status) }, ifLeft = { error -> stakingEventFactory.createGenericErrorAlert(error.toString()) - stateController.update(SetConfirmationStateResetAssentTransformer) + stateController.update( + SetConfirmationStateResetAssentTransformer(cryptoCurrencyStatus = cryptoCurrencyStatus), + ) }, ) } @@ -923,13 +944,13 @@ internal class StakingModel @Inject constructor( .launchIn(modelScope) } - private fun subscribeOnStepChanges() { + private fun subscribeOnStepChanges(status: CryptoCurrencyStatus) { uiState .distinctUntilChangedBy { it.currentStep } .onEach { when { isInitState() -> { - updateInitialData() + updateInitialData(status) balanceUpdater.initialUpdate() } isAssentState() -> { @@ -950,32 +971,30 @@ internal class StakingModel @Inject constructor( .saveIn(stepChangesJobHolder) } - private fun subscribeOnActionsUpdates() { - getActionsUseCase( - userWalletId = userWalletId, - cryptoCurrencyId = cryptoCurrencyId, - ) + private fun subscribeOnActionsUpdates(status: CryptoCurrencyStatus) { + getActionsUseCase(userWalletId = userWalletId, cryptoCurrencyId = cryptoCurrencyId) .conflate() .distinctUntilChanged() .onEach { result -> result.getOrNull()?.let { actions -> processingActions = actions if (isInitState()) { - updateInitialData() + updateInitialData(status) } } } .flowOn(dispatchers.main) .launchIn(modelScope) + .saveIn(actionsJobHolder) } - private fun updateInitialData() { + private fun updateInitialData(status: CryptoCurrencyStatus) { stateController.updateAll( SetInitialDataStateTransformer( clickIntents = this@StakingModel, yield = yield, isAnyTokenStaked = isAnyTokenStaked, - cryptoCurrencyStatusProvider = Provider { cryptoCurrencyStatus }, + cryptoCurrencyStatus = status, userWalletProvider = Provider { userWallet }, appCurrencyProvider = Provider { appCurrency }, balancesToShowProvider = Provider { balancesToShow }, diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/converters/BalanceItemConverter.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/converters/BalanceItemConverter.kt index 8051fb9e54..1449a49895 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/converters/BalanceItemConverter.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/converters/BalanceItemConverter.kt @@ -21,13 +21,12 @@ import org.joda.time.DateTime import java.util.Calendar internal class BalanceItemConverter( - private val cryptoCurrencyStatusProvider: Provider, + private val cryptoCurrencyStatus: CryptoCurrencyStatus, private val appCurrencyProvider: Provider, private val yield: Yield, ) : Converter { override fun convert(value: BalanceItem): BalanceState? { - val cryptoCurrencyStatus = cryptoCurrencyStatusProvider() val appCurrency = appCurrencyProvider() val cryptoCurrency = cryptoCurrencyStatus.currency diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/converters/RewardsValidatorStateConverter.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/converters/RewardsValidatorStateConverter.kt index d9996e2ded..d7a73fa6f9 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/converters/RewardsValidatorStateConverter.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/converters/RewardsValidatorStateConverter.kt @@ -20,13 +20,11 @@ import org.joda.time.DateTime import java.math.BigDecimal internal class RewardsValidatorStateConverter( - private val cryptoCurrencyStatusProvider: Provider, + private val cryptoCurrencyStatus: CryptoCurrencyStatus, private val appCurrencyProvider: Provider, private val yield: Yield, ) : Converter { override fun convert(value: Unit): StakingStates.RewardsValidatorsState { - val cryptoCurrencyStatus = cryptoCurrencyStatusProvider() - val yieldBalance = cryptoCurrencyStatus.value.yieldBalance return if (yieldBalance is YieldBalance.Data) { val balances = yieldBalance.balance.items diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/converters/YieldBalancesConverter.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/converters/YieldBalancesConverter.kt index 41c9881df8..ed5cb2cdad 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/converters/YieldBalancesConverter.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/converters/YieldBalancesConverter.kt @@ -15,18 +15,17 @@ import com.tangem.utils.converter.Converter import kotlinx.collections.immutable.toPersistentList internal class YieldBalancesConverter( - private val cryptoCurrencyStatusProvider: Provider, + private val cryptoCurrencyStatus: CryptoCurrencyStatus, private val appCurrencyProvider: Provider, private val balancesToShowProvider: Provider>, private val yield: Yield, ) : Converter { private val balanceItemConverter by lazy(LazyThreadSafetyMode.NONE) { - BalanceItemConverter(cryptoCurrencyStatusProvider, appCurrencyProvider, yield) + BalanceItemConverter(cryptoCurrencyStatus, appCurrencyProvider, yield) } override fun convert(value: Unit): InnerYieldBalanceState { - val cryptoCurrencyStatus = cryptoCurrencyStatusProvider() val appCurrency = appCurrencyProvider() val cryptoCurrency = cryptoCurrencyStatus.currency @@ -68,7 +67,6 @@ internal class YieldBalancesConverter( .toPersistentList() private fun getRewardBlockType(): Pair { - val cryptoCurrencyStatus = cryptoCurrencyStatusProvider() val blockchainId = cryptoCurrencyStatus.currency.network.id.value val yieldBalance = cryptoCurrencyStatus.value.yieldBalance as? YieldBalance.Data val rewards = yieldBalance?.balance?.items diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/helpers/StakingBalanceUpdater.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/helpers/StakingBalanceUpdater.kt index c84db7f3ea..b1058db489 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/helpers/StakingBalanceUpdater.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/helpers/StakingBalanceUpdater.kt @@ -1,7 +1,7 @@ package com.tangem.features.staking.impl.presentation.state.helpers -import com.tangem.domain.staking.FetchStakingYieldBalanceUseCase import com.tangem.domain.staking.FetchActionsUseCase +import com.tangem.domain.staking.FetchStakingYieldBalanceUseCase import com.tangem.domain.staking.model.stakekit.Yield import com.tangem.domain.staking.model.stakekit.action.StakingActionStatus import com.tangem.domain.tokens.FetchPendingTransactionsUseCase diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetConfirmationStateAssentTransformer.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetConfirmationStateAssentTransformer.kt index 85443b4096..56e01d3a83 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetConfirmationStateAssentTransformer.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetConfirmationStateAssentTransformer.kt @@ -13,6 +13,7 @@ internal class SetConfirmationStateAssentTransformer( private val appCurrencyProvider: Provider, private val feeCryptoCurrencyStatus: CryptoCurrencyStatus?, private val fee: Fee, + private val cryptoCurrencyStatus: CryptoCurrencyStatus, ) : Transformer { override fun transform(prevState: StakingUiState): StakingUiState { @@ -32,7 +33,9 @@ internal class SetConfirmationStateAssentTransformer( appCurrency = appCurrencyProvider(), isFeeApproximate = false, ), - isPrimaryButtonEnabled = true, + isPrimaryButtonEnabled = with(cryptoCurrencyStatus.value) { + sources.yieldBalanceSource.isActual() && sources.networkSource.isActual() + }, ) } else { return this diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetConfirmationStateCompletedTransformer.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetConfirmationStateCompletedTransformer.kt index 4ecbf95344..7190ad5ae1 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetConfirmationStateCompletedTransformer.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetConfirmationStateCompletedTransformer.kt @@ -1,12 +1,17 @@ package com.tangem.features.staking.impl.presentation.state.transformers import com.tangem.core.ui.extensions.TextReference -import com.tangem.features.staking.impl.presentation.state.* +import com.tangem.domain.tokens.model.CryptoCurrencyStatus +import com.tangem.features.staking.impl.presentation.state.InnerConfirmationStakingState +import com.tangem.features.staking.impl.presentation.state.StakingStates +import com.tangem.features.staking.impl.presentation.state.StakingUiState +import com.tangem.features.staking.impl.presentation.state.TransactionDoneState import com.tangem.utils.transformer.Transformer import kotlinx.collections.immutable.persistentListOf internal class SetConfirmationStateCompletedTransformer( private val txUrl: String, + private val cryptoCurrencyStatus: CryptoCurrencyStatus, ) : Transformer { override fun transform(prevState: StakingUiState): StakingUiState { @@ -18,7 +23,9 @@ internal class SetConfirmationStateCompletedTransformer( private fun StakingStates.ConfirmationState.copyWrapped(): StakingStates.ConfirmationState { return if (this is StakingStates.ConfirmationState.Data) { copy( - isPrimaryButtonEnabled = true, + isPrimaryButtonEnabled = with(cryptoCurrencyStatus.value) { + sources.yieldBalanceSource.isActual() && sources.networkSource.isActual() + }, innerState = InnerConfirmationStakingState.COMPLETED, footerText = TextReference.EMPTY, notifications = persistentListOf(), diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetConfirmationStateInitTransformer.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetConfirmationStateInitTransformer.kt index c55fb7a485..b3694b4fdf 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetConfirmationStateInitTransformer.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetConfirmationStateInitTransformer.kt @@ -63,7 +63,9 @@ internal class SetConfirmationStateInitTransformer( actionType = actionType, balanceState = balanceState, confirmationState = StakingStates.ConfirmationState.Data( - isPrimaryButtonEnabled = false, + isPrimaryButtonEnabled = with(cryptoCurrencyStatus.value) { + sources.yieldBalanceSource.isActual() && sources.networkSource.isActual() + }, innerState = InnerConfirmationStakingState.ASSENT, feeState = FeeState.Loading, notifications = persistentListOf(), diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetConfirmationStateResetAssentTransformer.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetConfirmationStateResetAssentTransformer.kt index 1c1c7103dc..327ec4ae8e 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetConfirmationStateResetAssentTransformer.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetConfirmationStateResetAssentTransformer.kt @@ -1,17 +1,22 @@ package com.tangem.features.staking.impl.presentation.state.transformers +import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.features.staking.impl.presentation.state.InnerConfirmationStakingState import com.tangem.features.staking.impl.presentation.state.StakingStates import com.tangem.features.staking.impl.presentation.state.StakingUiState import com.tangem.utils.transformer.Transformer -internal object SetConfirmationStateResetAssentTransformer : Transformer { +internal class SetConfirmationStateResetAssentTransformer( + private val cryptoCurrencyStatus: CryptoCurrencyStatus, +) : Transformer { override fun transform(prevState: StakingUiState): StakingUiState { val confirmationState = prevState.confirmationState return prevState.copy( confirmationState = if (confirmationState is StakingStates.ConfirmationState.Data) { confirmationState.copy( - isPrimaryButtonEnabled = true, + isPrimaryButtonEnabled = with(cryptoCurrencyStatus.value) { + sources.yieldBalanceSource.isActual() && sources.networkSource.isActual() + }, innerState = InnerConfirmationStakingState.ASSENT, ) } else { diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetInitialDataStateTransformer.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetInitialDataStateTransformer.kt index 873626c210..67eef70501 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetInitialDataStateTransformer.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetInitialDataStateTransformer.kt @@ -5,25 +5,28 @@ import com.tangem.common.ui.amountScreen.converters.AmountStateConverter import com.tangem.common.ui.amountScreen.models.AmountParameters import com.tangem.common.ui.amountScreen.models.AmountState import com.tangem.common.ui.amountScreen.models.EnterAmountBoundary +import com.tangem.core.ui.components.containers.pullToRefresh.PullToRefreshConfig import com.tangem.core.ui.components.currency.icon.converter.CryptoCurrencyToIconStateConverter import com.tangem.core.ui.components.list.RoundedListWithDividersItemData import com.tangem.core.ui.extensions.* import com.tangem.core.ui.format.bigdecimal.crypto import com.tangem.core.ui.format.bigdecimal.format import com.tangem.core.ui.format.bigdecimal.percent -import com.tangem.core.ui.components.containers.pullToRefresh.PullToRefreshConfig import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.staking.model.stakekit.BalanceItem import com.tangem.domain.staking.model.stakekit.Yield import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.domain.wallets.models.UserWallet import com.tangem.features.staking.impl.R -import com.tangem.features.staking.impl.presentation.state.* +import com.tangem.features.staking.impl.presentation.model.StakingClickIntents +import com.tangem.features.staking.impl.presentation.state.InnerYieldBalanceState +import com.tangem.features.staking.impl.presentation.state.StakingStates +import com.tangem.features.staking.impl.presentation.state.StakingStep +import com.tangem.features.staking.impl.presentation.state.StakingUiState import com.tangem.features.staking.impl.presentation.state.bottomsheet.InfoType import com.tangem.features.staking.impl.presentation.state.converters.RewardsValidatorStateConverter import com.tangem.features.staking.impl.presentation.state.converters.YieldBalancesConverter import com.tangem.features.staking.impl.presentation.state.utils.getRewardScheduleText -import com.tangem.features.staking.impl.presentation.model.StakingClickIntents import com.tangem.lib.crypto.BlockchainUtils.isPolkadot import com.tangem.utils.Provider import com.tangem.utils.isNullOrZero @@ -37,7 +40,7 @@ internal class SetInitialDataStateTransformer( private val clickIntents: StakingClickIntents, private val yield: Yield, private val isAnyTokenStaked: Boolean, - private val cryptoCurrencyStatusProvider: Provider, + private val cryptoCurrencyStatus: CryptoCurrencyStatus, private val userWalletProvider: Provider, private val appCurrencyProvider: Provider, private val balancesToShowProvider: Provider>, @@ -46,20 +49,20 @@ internal class SetInitialDataStateTransformer( private val iconStateConverter by lazy(::CryptoCurrencyToIconStateConverter) private val rewardsValidatorStateConverter by lazy(LazyThreadSafetyMode.NONE) { - RewardsValidatorStateConverter(cryptoCurrencyStatusProvider, appCurrencyProvider, yield) + RewardsValidatorStateConverter(cryptoCurrencyStatus, appCurrencyProvider, yield) } private val yieldBalancesConverter by lazy(LazyThreadSafetyMode.NONE) { YieldBalancesConverter( - cryptoCurrencyStatusProvider, - appCurrencyProvider, - balancesToShowProvider, - yield, + cryptoCurrencyStatus = cryptoCurrencyStatus, + appCurrencyProvider = appCurrencyProvider, + balancesToShowProvider = balancesToShowProvider, + yield = yield, ) } override fun transform(prevState: StakingUiState): StakingUiState { - val cryptoCurrency = cryptoCurrencyStatusProvider().currency + val cryptoCurrency = cryptoCurrencyStatus.currency return prevState.copy( title = TextReference.EMPTY, cryptoCurrencyName = cryptoCurrency.name, @@ -77,8 +80,12 @@ internal class SetInitialDataStateTransformer( private fun createInitialInfoState(): StakingStates.InitialInfoState.Data { val yieldBalance = yieldBalancesConverter.convert(Unit) + + val status = cryptoCurrencyStatus.value return StakingStates.InitialInfoState.Data( - isPrimaryButtonEnabled = !cryptoCurrencyStatusProvider().value.amount.isNullOrZero(), + isPrimaryButtonEnabled = with(status) { + !amount.isNullOrZero() && sources.yieldBalanceSource.isActual() && sources.networkSource.isActual() + }, showBanner = !isAnyTokenStaked && yieldBalance == InnerYieldBalanceState.Empty, aprRange = getAprRange(yield.preferredValidators), infoItems = getInfoItems(), @@ -92,8 +99,6 @@ internal class SetInitialDataStateTransformer( } private fun getInfoItems(): PersistentList { - val cryptoCurrencyStatus = cryptoCurrencyStatusProvider() - return listOfNotNull( createAnnualPercentageRateItem(), createAvailableItem(cryptoCurrencyStatus), @@ -187,7 +192,7 @@ internal class SetInitialDataStateTransformer( private fun createRewardScheduleItem(): RoundedListWithDividersItemData? { val endTextReference = getRewardScheduleText( rewardSchedule = yield.metadata.rewardSchedule, - networkId = cryptoCurrencyStatusProvider().currency.network.id.value, + networkId = cryptoCurrencyStatus.currency.network.id.value, decapitalize = false, ) ?: return null @@ -200,7 +205,7 @@ internal class SetInitialDataStateTransformer( } private fun createInitialAmountState(): AmountState { - val cryptoBalanceValue = cryptoCurrencyStatusProvider().value + val cryptoBalanceValue = cryptoCurrencyStatus.value val maxEnterAmount = EnterAmountBoundary( amount = cryptoBalanceValue.amount, fiatAmount = cryptoBalanceValue.fiatAmount, @@ -208,7 +213,7 @@ internal class SetInitialDataStateTransformer( ) return AmountStateConverter( clickIntents = clickIntents, - cryptoCurrencyStatusProvider = cryptoCurrencyStatusProvider, + cryptoCurrencyStatusProvider = Provider { cryptoCurrencyStatus }, appCurrencyProvider = appCurrencyProvider, iconStateConverter = iconStateConverter, maxEnterAmount = maxEnterAmount, diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/approval/SetConfirmationStateAssentApprovalTransformer.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/approval/SetConfirmationStateAssentApprovalTransformer.kt index 6f331795e4..0729792529 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/approval/SetConfirmationStateAssentApprovalTransformer.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/approval/SetConfirmationStateAssentApprovalTransformer.kt @@ -14,6 +14,7 @@ internal class SetConfirmationStateAssentApprovalTransformer( private val appCurrencyProvider: Provider, private val feeCryptoCurrencyStatus: CryptoCurrencyStatus?, private val fee: TransactionFee, + private val cryptoCurrencyStatus: CryptoCurrencyStatus, ) : Transformer { override fun transform(prevState: StakingUiState): StakingUiState { @@ -35,7 +36,9 @@ internal class SetConfirmationStateAssentApprovalTransformer( appCurrency = appCurrencyProvider(), isFeeApproximate = false, ), - isPrimaryButtonEnabled = true, + isPrimaryButtonEnabled = with(cryptoCurrencyStatus.value) { + sources.yieldBalanceSource.isActual() && sources.networkSource.isActual() + }, isApprovalNeeded = true, ) } else { diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/notifications/AddStakingNotificationsTransformer.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/notifications/AddStakingNotificationsTransformer.kt index 6aeb94cc74..40dbd696c0 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/notifications/AddStakingNotificationsTransformer.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/notifications/AddStakingNotificationsTransformer.kt @@ -114,6 +114,10 @@ internal class AddStakingNotificationsTransformer( ) }.toImmutableList() + val isActualSources = with(cryptoCurrencyStatus.value) { + sources.yieldBalanceSource.isActual() && sources.networkSource.isActual() + } + return prevState.copy( confirmationState = confirmationState.copy( notifications = notifications.toImmutableList(), @@ -122,7 +126,7 @@ internal class AddStakingNotificationsTransformer( it is NotificationUM.Error || it is NotificationUM.Warning.NetworkFeeUnreachable || it is StakingNotification.Warning.TransactionInProgress - }, + } && isActualSources, ), ) } diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsBalanceSelectStateConverter.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsBalanceSelectStateConverter.kt index 4a76d6f1c9..b08ab69246 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsBalanceSelectStateConverter.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsBalanceSelectStateConverter.kt @@ -20,9 +20,14 @@ internal class TokenDetailsBalanceSelectStateConverter( override fun convert(value: TokenBalanceSegmentedButtonConfig): TokenDetailsState { return with(currentStateProvider()) { - if (stakingBlocksState !is StakingBlockUM.Staked) return this val cryptoCurrencyStatus = cryptoCurrencyStatusProvider() ?: return this + if (stakingBlocksState !is StakingBlockUM.Staked && + stakingBlocksState !is StakingBlockUM.TemporaryUnavailable + ) { + return this + } + val yieldBalance = cryptoCurrencyStatus.value.yieldBalance as? YieldBalance.Data val stakingCryptoAmount = yieldBalance?.getTotalWithRewardsStakingBalance() val stakingFiatAmount = stakingCryptoAmount?.let { cryptoCurrencyStatus.value.fiatRate?.multiply(it) } From d444dfe28bc75a52b43ff78e2fe9f7a5bbce4e40 Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 11 Mar 2025 14:32:54 +0000 Subject: [PATCH 8/9] Updated on 2026-08-14 --- gradle/tangem_dependencies.toml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/gradle/tangem_dependencies.toml b/gradle/tangem_dependencies.toml index 9096044540..76cabfe4a7 100644 --- a/gradle/tangem_dependencies.toml +++ b/gradle/tangem_dependencies.toml @@ -1,7 +1,7 @@ [versions] -tangemBlockchainSdk = "release-app_5.21-976" +tangemBlockchainSdk = "develop-977" #tangemBlockchainSdk = "0.0.1" # Keep it! - used for local builds -tangemCardSdk = "release-app_5.21-435" +tangemCardSdk = "develop-437" #tangemCardSdk = "0.0.1" # Keep it! - used for local builds ^ tangemVico = "2.0.0-alpha.25-tangem-developments8" #tangemVico = "0.0.1" # Keep it! - used for local builds ^ From 77b4c175742a59f7cfca336392b02a4ad75572a7 Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 11 Mar 2025 20:56:47 +0300 Subject: [PATCH 9/9] Updated on 2026-08-14 --- .../tangem/tap/network/auth/DefaultExpressAuthProvider.kt | 8 +++++++- gradle/tangem_dependencies.toml | 2 +- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/com/tangem/tap/network/auth/DefaultExpressAuthProvider.kt b/app/src/main/java/com/tangem/tap/network/auth/DefaultExpressAuthProvider.kt index 452b90127d..5087ef7019 100644 --- a/app/src/main/java/com/tangem/tap/network/auth/DefaultExpressAuthProvider.kt +++ b/app/src/main/java/com/tangem/tap/network/auth/DefaultExpressAuthProvider.kt @@ -1,5 +1,6 @@ package com.tangem.tap.network.auth +import com.tangem.common.CardIdRangeDec import com.tangem.datasource.local.preferences.AppPreferencesStore import com.tangem.datasource.local.preferences.PreferencesKeys import com.tangem.datasource.local.preferences.utils.getSyncOrDefault @@ -48,7 +49,12 @@ internal class DefaultExpressAuthProvider( } private fun isChangeNow(selectedUserWallet: UserWallet): Boolean { - return selectedUserWallet.scanResponse.card.batchId == BATCH_ID_CHANGENOW + val changeNowRange = CardIdRangeDec( + start = "AF99001800554008", + end = "AF99001800559994", + ) + val card = selectedUserWallet.scanResponse.card + return card.batchId == BATCH_ID_CHANGENOW || changeNowRange?.contains(card.cardId) == true } private fun isPartner(selectedUserWallet: UserWallet): Boolean { diff --git a/gradle/tangem_dependencies.toml b/gradle/tangem_dependencies.toml index 9096044540..df3e3f8b44 100644 --- a/gradle/tangem_dependencies.toml +++ b/gradle/tangem_dependencies.toml @@ -1,7 +1,7 @@ [versions] tangemBlockchainSdk = "release-app_5.21-976" #tangemBlockchainSdk = "0.0.1" # Keep it! - used for local builds -tangemCardSdk = "release-app_5.21-435" +tangemCardSdk = "release-app_5.21-438" #tangemCardSdk = "0.0.1" # Keep it! - used for local builds ^ tangemVico = "2.0.0-alpha.25-tangem-developments8" #tangemVico = "0.0.1" # Keep it! - used for local builds ^