Updated on 2026-08-14

This commit is contained in:
Tangem 2025-01-16 19:31:35 +03:00
parent 9cfd045c7b
commit 02d7e5df0b
15 changed files with 80 additions and 173 deletions

View file

@ -13,26 +13,6 @@ class CardExchangeRules(
val cardProvider: () -> CardDTO?,
) : ExchangeRules {
override fun isBuyAllowed(): Boolean {
val card = cardProvider() ?: return false
return when {
card.isDemoCard() -> true
card.isStart2Coin -> false
else -> true
}
}
override fun isSellAllowed(): Boolean {
val card = cardProvider() ?: return false
return when {
card.isDemoCard() -> false
card.isStart2Coin -> false
else -> true
}
}
override fun availableForBuy(scanResponse: ScanResponse, currency: Currency): Boolean {
val card = scanResponse.card
@ -46,10 +26,6 @@ class CardExchangeRules(
override fun availableForSell(currency: Currency): Boolean {
val card = cardProvider() ?: return false
return when {
card.isDemoCard() -> false
card.isStart2Coin -> false
else -> true
}
return !card.isStart2Coin
}
}

View file

@ -52,9 +52,6 @@ class CurrencyExchangeManager(
_initializationStatus.value = lceContent()
}
override fun isBuyAllowed(): Boolean = primaryRules.isBuyAllowed() && buyService.isBuyAllowed()
override fun isSellAllowed(): Boolean = primaryRules.isSellAllowed() && sellService.isSellAllowed()
override fun availableForBuy(scanResponse: ScanResponse, currency: Currency): Boolean {
return primaryRules.availableForBuy(scanResponse, currency) &&
buyService.availableForBuy(scanResponse, currency)

View file

@ -1,5 +1,10 @@
package com.tangem.tap.network.exchangeServices
import arrow.core.Either
import arrow.core.raise.catch
import arrow.core.raise.either
import arrow.core.raise.ensure
import arrow.core.right
import com.tangem.blockchainsdk.utils.ExcludedBlockchains
import com.tangem.datasource.api.express.models.TangemExpressValues.EMPTY_CONTRACT_ADDRESS_VALUE
import com.tangem.datasource.api.express.models.response.Asset
@ -37,16 +42,6 @@ internal class DefaultRampManager(
private val cryptoCurrencyConverter = CryptoCurrencyConverter(excludedBlockchains)
override fun isSellSupportedByService(cryptoCurrency: CryptoCurrency): Boolean {
return runCatching {
exchangeService?.availableForSell(
currency = cryptoCurrencyConverter.convertBack(cryptoCurrency),
)
}
.getOrNull()
?: false
}
override suspend fun availableForBuy(
scanResponse: ScanResponse,
userWalletId: UserWalletId,
@ -66,18 +61,40 @@ internal class DefaultRampManager(
?: false
}
override suspend fun availableForSell(userWalletId: UserWalletId, status: CryptoCurrencyStatus): Boolean {
return runCatching {
val sellSupportedByService = isSellSupportedByService(cryptoCurrency = status.currency)
override suspend fun availableForSell(
userWalletId: UserWalletId,
status: CryptoCurrencyStatus,
): Either<ScenarioUnavailabilityReason, Unit> {
return either {
val sellSupportedByService = catch(
block = {
val serviceCurrency = cryptoCurrencyConverter.convertBack(status.currency)
if (!sellSupportedByService) return false
exchangeService?.availableForSell(currency = serviceCurrency) ?: false
},
catch = { raise(ScenarioUnavailabilityReason.NotSupportedBySellService(status.currency.name)) },
)
ensure(condition = sellSupportedByService) {
ScenarioUnavailabilityReason.NotSupportedBySellService(status.currency.name)
}
val reason = getSendUnavailabilityReason(userWalletId, status)
reason == ScenarioUnavailabilityReason.None
ensure(condition = reason is ScenarioUnavailabilityReason.None) {
when (reason) {
is ScenarioUnavailabilityReason.EmptyBalance -> {
reason.copy(withdrawalScenario = ScenarioUnavailabilityReason.WithdrawalScenario.SELL)
}
is ScenarioUnavailabilityReason.PendingTransaction -> {
reason.copy(withdrawalScenario = ScenarioUnavailabilityReason.WithdrawalScenario.SELL)
}
else -> reason
}
}
Unit.right()
}
.getOrNull()
?: false
}
override suspend fun availableForSwap(userWalletId: UserWalletId, cryptoCurrency: CryptoCurrency): Boolean {

View file

@ -11,8 +11,6 @@ import kotlinx.coroutines.flow.StateFlow
typealias ExchangeServiceInitializationStatus = Lce<Throwable, Any>
interface Exchanger {
fun isBuyAllowed(): Boolean
fun isSellAllowed(): Boolean
fun availableForBuy(scanResponse: ScanResponse, currency: Currency): Boolean
fun availableForSell(currency: Currency): Boolean
}
@ -30,8 +28,6 @@ interface ExchangeService : Exchanger, ExchangeUrlBuilder {
MutableStateFlow(value = lceLoading())
override suspend fun update() {}
override fun isBuyAllowed(): Boolean = false
override fun isSellAllowed(): Boolean = false
override fun availableForBuy(scanResponse: ScanResponse, currency: Currency): Boolean = false
override fun availableForSell(currency: Currency): Boolean = false
override fun getUrl(
@ -54,8 +50,6 @@ interface ExchangeRules : Exchanger {
companion object {
fun dummy(): ExchangeRules = object : ExchangeRules {
override fun isBuyAllowed(): Boolean = false
override fun isSellAllowed(): Boolean = false
override fun availableForBuy(scanResponse: ScanResponse, currency: Currency): Boolean = false
override fun availableForSell(currency: Currency): Boolean = false
}

View file

@ -54,13 +54,7 @@ internal class MercuryoService(private val environment: MercuryoEnvironment) : E
}
}
override fun isBuyAllowed(): Boolean = true
override fun isSellAllowed(): Boolean = false
override fun availableForBuy(scanResponse: ScanResponse, currency: Currency): Boolean {
if (!isBuyAllowed()) return false
val mercuryoNetwork = currency.blockchain.mercuryoNetwork
val contractAddress = (currency as? Currency.Token)?.token?.contractAddress ?: ""
val availableCurrency = availableMercuryoCurrencies.firstOrNull {

View file

@ -102,12 +102,6 @@ class MoonPayService(
}
}
override fun isBuyAllowed(): Boolean = false
override fun isSellAllowed(): Boolean {
return status?.responseUserStatus?.isSellAllowed ?: false
}
override fun availableForBuy(scanResponse: ScanResponse, currency: Currency): Boolean = false
override fun availableForSell(currency: Currency): Boolean {
@ -187,6 +181,10 @@ class MoonPayService(
return Base64.encodeToString(sha256encoded, Base64.NO_WRAP)
}
private fun isSellAllowed(): Boolean {
return status?.responseUserStatus?.isSellAllowed ?: false
}
companion object {
const val URL_SELL = "sell.moonpay.com"
}

View file

@ -1,9 +1,11 @@
package com.tangem.domain.exchange
import arrow.core.Either
import com.tangem.domain.core.lce.Lce
import com.tangem.domain.models.scan.ScanResponse
import com.tangem.domain.tokens.model.CryptoCurrency
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
import com.tangem.domain.tokens.model.ScenarioUnavailabilityReason
import com.tangem.domain.wallets.models.UserWalletId
import kotlinx.coroutines.flow.Flow
@ -12,9 +14,6 @@ import kotlinx.coroutines.flow.Flow
*/
interface RampStateManager {
/** Check if sell service is supported for given [CryptoCurrency] */
fun isSellSupportedByService(cryptoCurrency: CryptoCurrency): Boolean
suspend fun availableForBuy(
scanResponse: ScanResponse,
userWalletId: UserWalletId,
@ -24,10 +23,13 @@ interface RampStateManager {
/**
* Check if [CryptoCurrency] is available for sell
*
* @param userWalletId id of multi-currency wallet
* @param status crypto currency status
* @param userWalletId id of multi-currency wallet
* @param status crypto currency status
*/
suspend fun availableForSell(userWalletId: UserWalletId, status: CryptoCurrencyStatus): Boolean
suspend fun availableForSell(
userWalletId: UserWalletId,
status: CryptoCurrencyStatus,
): Either<ScenarioUnavailabilityReason, Unit>
suspend fun availableForSwap(userWalletId: UserWalletId, cryptoCurrency: CryptoCurrency): Boolean

View file

@ -192,44 +192,18 @@ class GetCryptoCurrencyActionsUseCase(
)
}
// sell
val sellSupportedByService = rampManager.isSellSupportedByService(cryptoCurrency)
val sendAvailable = sendUnavailabilityReason is ScenarioUnavailabilityReason.None
when {
sellSupportedByService && sendAvailable -> {
// region sell
rampManager.availableForSell(
userWalletId = userWallet.walletId,
status = cryptoCurrencyStatus,
)
.onRight {
activeList.add(TokenActionsState.ActionState.Sell(ScenarioUnavailabilityReason.None))
}
sellSupportedByService && !sendAvailable -> {
(sendUnavailabilityReason as? ScenarioUnavailabilityReason.EmptyBalance)?.let {
disabledList.add(
TokenActionsState.ActionState.Sell(
unavailabilityReason = it.copy(
withdrawalScenario = ScenarioUnavailabilityReason.WithdrawalScenario.SELL,
),
),
)
}
(sendUnavailabilityReason as? ScenarioUnavailabilityReason.PendingTransaction)?.let {
disabledList.add(
TokenActionsState.ActionState.Sell(
unavailabilityReason = it.copy(
withdrawalScenario = ScenarioUnavailabilityReason.WithdrawalScenario.SELL,
),
),
)
}
.onLeft { reason ->
disabledList.add(TokenActionsState.ActionState.Sell(reason))
}
else -> {
disabledList.add(
TokenActionsState.ActionState.Sell(
unavailabilityReason = ScenarioUnavailabilityReason.NotSupportedBySellService(
cryptoCurrency.name,
),
),
)
}
}
// endregion
// hide
activeList.add(TokenActionsState.ActionState.HideToken(ScenarioUnavailabilityReason.None))

View file

@ -216,7 +216,10 @@ internal class OnrampTokenListModel @Inject constructor(
)
}
OnrampOperation.SELL -> {
rampStateManager.availableForSell(userWalletId = params.userWalletId, status = status)
rampStateManager.availableForSell(
userWalletId = params.userWalletId,
status = status,
).isRight()
}
OnrampOperation.SWAP -> {
val isAvailable = rampStateManager.availableForSwap(

View file

@ -7,7 +7,6 @@ import com.tangem.core.ui.components.marketprice.PriceChangeState
import com.tangem.core.ui.components.marketprice.PriceChangeType
import com.tangem.core.ui.components.transactions.state.TransactionState
import com.tangem.core.ui.components.transactions.state.TxHistoryState
import com.tangem.core.ui.event.consumedEvent
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.extensions.stringReference
@ -90,7 +89,7 @@ internal object TokenDetailsPreviewData {
),
)
val iconState = IconState.TokenIcon(
private val iconState = IconState.TokenIcon(
url = "https://s3.eu-central-1.amazonaws.com/tangem.api/coins/large/stellar.png",
fallbackTint = Color.Cyan,
fallbackBackground = Color.Blue,
@ -314,7 +313,6 @@ internal object TokenDetailsPreviewData {
bottomSheetConfig = null,
isBalanceHidden = false,
isMarketPriceAvailable = false,
event = consumedEvent(),
)
val tokenDetailsState_2 = TokenDetailsState(
@ -345,7 +343,6 @@ internal object TokenDetailsPreviewData {
bottomSheetConfig = null,
isBalanceHidden = false,
isMarketPriceAvailable = true,
event = consumedEvent(),
)
val tokenDetailsState_3 = tokenDetailsState_2.copy(

View file

@ -1,15 +1,13 @@
package com.tangem.feature.tokendetails.presentation.tokendetails.state
import com.tangem.common.ui.expressStatus.state.ExpressTransactionStateUM
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig
import com.tangem.core.ui.components.marketprice.MarketPriceBlockState
import com.tangem.core.ui.components.transactions.state.TransactionState
import com.tangem.core.ui.components.transactions.state.TxHistoryState
import com.tangem.core.ui.event.StateEvent
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.pullToRefresh.PullToRefreshConfig
import com.tangem.feature.tokendetails.presentation.tokendetails.state.components.TokenDetailsDialogConfig
import com.tangem.feature.tokendetails.presentation.tokendetails.state.components.TokenDetailsNotification
import com.tangem.common.ui.expressStatus.state.ExpressTransactionStateUM
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.PersistentList
@ -29,5 +27,4 @@ internal data class TokenDetailsState(
val bottomSheetConfig: TangemBottomSheetConfig?,
val isBalanceHidden: Boolean,
val isMarketPriceAvailable: Boolean,
val event: StateEvent<TextReference>,
)

View file

@ -3,7 +3,6 @@ package com.tangem.feature.tokendetails.presentation.tokendetails.state.factory
import arrow.core.getOrElse
import com.tangem.core.ui.components.marketprice.MarketPriceBlockState
import com.tangem.core.ui.components.transactions.state.TxHistoryState
import com.tangem.core.ui.event.consumedEvent
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.networkIconResId
import com.tangem.core.ui.extensions.resourceReference
@ -78,7 +77,6 @@ internal class TokenDetailsSkeletonStateConverter(
bottomSheetConfig = null,
isBalanceHidden = true,
isMarketPriceAvailable = value.id.rawCurrencyId != null,
event = consumedEvent(),
)
}

View file

@ -9,8 +9,6 @@ import com.tangem.core.ui.components.bottomsheets.tokenreceive.TokenReceiveBotto
import com.tangem.core.ui.components.bottomsheets.tokenreceive.mapToAddressModels
import com.tangem.core.ui.components.transactions.state.TransactionState
import com.tangem.core.ui.components.transactions.state.TxHistoryState
import com.tangem.core.ui.event.consumedEvent
import com.tangem.core.ui.event.triggeredEvent
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.res.TangemTheme
@ -321,22 +319,6 @@ internal class TokenDetailsStateFactory(
)
}
fun getStateAndTriggerEvent(
state: TokenDetailsState,
errorMessage: TextReference,
setUiState: (TokenDetailsState) -> Unit,
): TokenDetailsState {
return state.copy(
event = triggeredEvent(
data = errorMessage,
onConsume = {
val currentState = currentStateProvider()
setUiState(currentState.copy(event = consumedEvent()))
},
),
)
}
fun getStateWithUpdatedMenu(
cardTypesResolver: CardTypesResolver,
hasDerivations: Boolean,

View file

@ -11,12 +11,9 @@ import androidx.compose.material.pullrefresh.pullRefresh
import androidx.compose.material.pullrefresh.rememberPullRefreshState
import androidx.compose.material3.Scaffold
import androidx.compose.material3.ScaffoldDefaults
import androidx.compose.material3.SnackbarHostState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.tooling.preview.PreviewParameter
@ -34,10 +31,6 @@ import com.tangem.core.ui.components.notifications.Notification
import com.tangem.core.ui.components.notifications.OkxPromoNotification
import com.tangem.core.ui.components.transactions.state.TxHistoryState
import com.tangem.core.ui.components.transactions.txHistoryItems
import com.tangem.core.ui.event.EventEffect
import com.tangem.core.ui.event.StateEvent
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.resolveReference
import com.tangem.core.ui.pullToRefresh.PullToRefreshConfig
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreview
@ -61,7 +54,6 @@ internal fun TokenDetailsScreen(state: TokenDetailsState, tokenMarketBlockCompon
BackHandler(onBack = state.topAppBarConfig.onBackClick)
val bottomBarHeight = with(LocalDensity.current) { WindowInsets.systemBars.getBottom(this).toDp() }
val snackbarHostState = remember { SnackbarHostState() }
Scaffold(
topBar = { TokenDetailsTopAppBar(config = state.topAppBarConfig) },
contentWindowInsets = ScaffoldDefaults.contentWindowInsets.exclude(WindowInsets.navigationBars),
@ -200,23 +192,9 @@ internal fun TokenDetailsScreen(state: TokenDetailsState, tokenMarketBlockCompon
}
}
}
TokenDetailsEventEffect(snackbarHostState = snackbarHostState, event = state.event)
}
}
@Composable
internal fun TokenDetailsEventEffect(snackbarHostState: SnackbarHostState, event: StateEvent<TextReference>) {
val resources = LocalContext.current.resources
EventEffect(
event = event,
onTrigger = { value ->
snackbarHostState.showSnackbar(message = value.resolveReference(resources))
},
)
}
// region Preview
@Preview(showBackground = true, widthDp = 360)
@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES)

View file

@ -8,8 +8,12 @@ import com.tangem.blockchain.common.address.AddressType
import com.tangem.common.routing.AppRoute
import com.tangem.common.routing.AppRouter
import com.tangem.common.routing.bundle.unbundle
import com.tangem.common.ui.expressStatus.ExpressStatusBottomSheetConfig
import com.tangem.common.ui.expressStatus.state.ExpressTransactionStateUM
import com.tangem.core.analytics.api.AnalyticsEventHandler
import com.tangem.core.analytics.models.AnalyticsParam
import com.tangem.core.decompose.di.GlobalUiMessageSender
import com.tangem.core.decompose.ui.UiMessageSender
import com.tangem.core.deeplink.DeepLinksRegistry
import com.tangem.core.deeplink.global.BuyCurrencyDeepLink
import com.tangem.core.navigation.share.ShareManager
@ -23,6 +27,7 @@ import com.tangem.core.ui.extensions.stringReference
import com.tangem.core.ui.extensions.wrappedList
import com.tangem.core.ui.haptic.TangemHapticEffect
import com.tangem.core.ui.haptic.VibratorHapticManager
import com.tangem.core.ui.message.SnackbarMessage
import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase
import com.tangem.domain.appcurrency.model.AppCurrency
import com.tangem.domain.balancehiding.GetBalanceHidingSettingsUseCase
@ -31,8 +36,8 @@ import com.tangem.domain.card.NetworkHasDerivationUseCase
import com.tangem.domain.common.util.cardTypesResolver
import com.tangem.domain.demo.IsDemoCardUseCase
import com.tangem.domain.onramp.model.OnrampSource
import com.tangem.domain.redux.ReduxStateHolder
import com.tangem.domain.promo.ShouldShowSwapPromoTokenUseCase
import com.tangem.domain.redux.ReduxStateHolder
import com.tangem.domain.staking.GetStakingAvailabilityUseCase
import com.tangem.domain.staking.GetStakingEntryInfoUseCase
import com.tangem.domain.staking.GetStakingIntegrationIdUseCase
@ -67,10 +72,8 @@ import com.tangem.feature.tokendetails.presentation.tokendetails.analytics.Token
import com.tangem.feature.tokendetails.presentation.tokendetails.analytics.TokenDetailsNotificationsAnalyticsSender
import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenBalanceSegmentedButtonConfig
import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenDetailsState
import com.tangem.common.ui.expressStatus.state.ExpressTransactionStateUM
import com.tangem.feature.tokendetails.presentation.tokendetails.state.factory.TokenDetailsStateFactory
import com.tangem.feature.tokendetails.presentation.tokendetails.state.factory.express.ExpressStatusFactory
import com.tangem.common.ui.expressStatus.ExpressStatusBottomSheetConfig
import com.tangem.features.onramp.OnrampFeatureToggles
import com.tangem.features.tokendetails.impl.R
import com.tangem.utils.Provider
@ -118,6 +121,7 @@ internal class TokenDetailsViewModel @Inject constructor(
private val getCryptoCurrencySyncUseCase: GetCryptoCurrencyStatusSyncUseCase,
private val onrampFeatureToggles: OnrampFeatureToggles,
private val shareManager: ShareManager,
@GlobalUiMessageSender private val uiMessageSender: UiMessageSender,
expressStatusFactory: ExpressStatusFactory.Factory,
getUserWalletUseCase: GetUserWalletUseCase,
getStakingIntegrationIdUseCase: GetStakingIntegrationIdUseCase,
@ -597,10 +601,9 @@ internal class TokenDetailsViewModel @Inject constructor(
if (extendedKey.isNotBlank()) {
vibratorHapticManager.performOneTime(TangemHapticEffect.OneTime.Click)
clipboardManager.setText(text = extendedKey, isSensitive = true)
internalUiState.value = stateFactory.getStateAndTriggerEvent(
state = internalUiState.value,
errorMessage = resourceReference(R.string.wallet_notification_address_copied),
setUiState = { internalUiState.value = it },
uiMessageSender.send(
message = SnackbarMessage(message = resourceReference(R.string.wallet_notification_address_copied)),
)
}
}
@ -698,17 +701,14 @@ internal class TokenDetailsViewModel @Inject constructor(
}
private fun showErrorIfDemoModeOrElse(action: () -> Unit) {
viewModelScope.launch(dispatchers.main) {
if (isDemoCardUseCase(cardId = userWallet.cardId)) {
internalUiState.value = stateFactory.getStateWithClosedBottomSheet()
internalUiState.value = stateFactory.getStateAndTriggerEvent(
state = internalUiState.value,
errorMessage = resourceReference(id = R.string.alert_demo_feature_disabled),
setUiState = { internalUiState.value = it },
)
} else {
action()
}
if (isDemoCardUseCase(cardId = userWallet.cardId)) {
internalUiState.value = stateFactory.getStateWithClosedBottomSheet()
uiMessageSender.send(
message = SnackbarMessage(message = resourceReference(R.string.alert_demo_feature_disabled)),
)
} else {
action()
}
}