Updated on 2026-08-14

This commit is contained in:
Tangem 2025-06-30 16:53:01 +05:00
commit d4b01f6065
27 changed files with 1945 additions and 4 deletions

View file

@ -10,6 +10,7 @@ import com.tangem.feature.usedesk.api.UsedeskComponent
import com.tangem.feature.walletsettings.component.WalletSettingsComponent
import com.tangem.features.details.component.DetailsComponent
import com.tangem.features.disclaimer.api.components.DisclaimerComponent
import com.tangem.features.managetokens.component.ChooseManagedTokensComponent
import com.tangem.features.managetokens.component.ManageTokensComponent
import com.tangem.features.managetokens.component.ManageTokensSource
import com.tangem.features.markets.details.MarketsTokenDetailsComponent
@ -78,6 +79,7 @@ internal class ChildFactory @Inject constructor(
private val nftComponentFactory: NFTComponent.Factory,
private val nftSendComponentFactory: NFTSendComponent.Factory,
private val usedeskComponentFactory: UsedeskComponent.Factory,
private val chooseManagedTokensComponentFactory: ChooseManagedTokensComponent.Factory,
private val testerRouter: TesterRouter,
private val walletConnectFeatureToggles: WalletConnectFeatureToggles,
) {
@ -407,6 +409,17 @@ internal class ChildFactory @Inject constructor(
componentFactory = usedeskComponentFactory,
)
}
is AppRoute.ChooseManagedTokens -> {
createComponentChild(
context = context,
params = ChooseManagedTokensComponent.Params(
userWalletId = route.userWalletId,
initialCurrency = route.initialCurrency,
source = ChooseManagedTokensComponent.Source.valueOf(route.source.name),
),
componentFactory = chooseManagedTokensComponentFactory,
)
}
}
}
}

View file

@ -127,6 +127,16 @@ sealed class AppRoute(val path: String) : Route {
}
}
data class ChooseManagedTokens(
val userWalletId: UserWalletId,
val initialCurrency: CryptoCurrency,
val source: Source,
) : AppRoute(path = "/$source/choose_managed_tokens/$userWalletId/${initialCurrency.id.value}") {
enum class Source {
SendViaSwap,
}
}
@Serializable
data class WalletConnectSessions(val userWalletId: UserWalletId) : AppRoute(path = "/wallet_connect_sessions")

View file

@ -12,7 +12,14 @@ import com.tangem.domain.tokens.model.CryptoCurrencyStatus
data class SwapCurrencies(
val fromGroup: SwapCurrenciesGroup,
val toGroup: SwapCurrenciesGroup,
)
) {
companion object {
val EMPTY = SwapCurrencies(
fromGroup = SwapCurrenciesGroup(emptyList(), emptyList(), false),
toGroup = SwapCurrenciesGroup(emptyList(), emptyList(), false),
)
}
}
/**
* Return swap group depending on [swapDirection]

View file

@ -53,6 +53,7 @@ dependencies {
implementation(projects.domain.manageTokens)
implementation(projects.domain.transaction.models)
implementation(projects.domain.transaction)
implementation(projects.domain.legacy)
/** Compose */
implementation(deps.compose.foundation)

View file

@ -0,0 +1,42 @@
package com.tangem.features.swap.v2.impl.amount
import androidx.compose.foundation.background
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.tangem.core.decompose.context.AppComponentContext
import com.tangem.core.decompose.model.getOrCreateModel
import com.tangem.core.ui.decompose.ComposableContentComponent
import com.tangem.core.ui.res.TangemTheme
import com.tangem.features.swap.v2.impl.amount.entity.SwapAmountUM
import com.tangem.features.swap.v2.impl.amount.model.SwapAmountModel
import com.tangem.features.swap.v2.impl.amount.ui.SwapAmountContent
import com.tangem.features.swap.v2.impl.common.SwapNavigationModelCallback
import dagger.assisted.Assisted
import dagger.assisted.AssistedInject
internal class SwapAmountComponent @AssistedInject constructor(
@Assisted appComponentContext: AppComponentContext,
@Assisted private val params: SwapAmountComponentParams.AmountParams,
) : ComposableContentComponent, AppComponentContext by appComponentContext {
private val model: SwapAmountModel = getOrCreateModel(params = params)
fun updateState(amountUM: SwapAmountUM) = model.updateState(amountUM)
@Composable
override fun Content(modifier: Modifier) {
val amountUM by model.uiState.collectAsStateWithLifecycle()
SwapAmountContent(
amountUM = amountUM,
modifier = Modifier.background(TangemTheme.colors.background.tertiary),
clickIntents = model,
)
}
interface ModelCallback : SwapNavigationModelCallback {
fun onAmountResult(amountUM: SwapAmountUM)
}
}

View file

@ -0,0 +1,46 @@
package com.tangem.features.swap.v2.impl.amount
import com.tangem.domain.appcurrency.model.AppCurrency
import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.domain.swap.models.SwapDirection
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
import com.tangem.domain.wallets.models.UserWallet
import com.tangem.features.swap.v2.impl.amount.entity.SwapAmountUM
import com.tangem.features.swap.v2.impl.swap.SwapRoute
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.StateFlow
internal sealed class SwapAmountComponentParams {
abstract val amountUM: SwapAmountUM
abstract val analyticsCategoryName: String
abstract val userWallet: UserWallet
abstract val appCurrency: AppCurrency
abstract val swapDirection: SwapDirection
abstract val isBalanceHidingFlow: StateFlow<Boolean>
data class AmountParams(
override val amountUM: SwapAmountUM,
override val analyticsCategoryName: String,
override val userWallet: UserWallet,
override val isBalanceHidingFlow: StateFlow<Boolean>,
override val appCurrency: AppCurrency,
override val swapDirection: SwapDirection,
val primaryCryptoCurrency: CryptoCurrency,
val secondaryCryptoCurrency: CryptoCurrency?,
val callback: SwapAmountComponent.ModelCallback,
val currentRoute: Flow<SwapRoute.Amount>,
) : SwapAmountComponentParams()
data class AmountBlockParams(
override val amountUM: SwapAmountUM,
override val analyticsCategoryName: String,
override val userWallet: UserWallet,
override val isBalanceHidingFlow: StateFlow<Boolean>,
override val appCurrency: AppCurrency,
override val swapDirection: SwapDirection,
val primaryCryptoCurrencyStatus: CryptoCurrencyStatus,
val secondaryCryptoCurrencyStatus: CryptoCurrencyStatus?,
val blockClickEnableFlow: StateFlow<Boolean>,
) : SwapAmountComponentParams()
}

View file

@ -0,0 +1,20 @@
package com.tangem.features.swap.v2.impl.amount.di
import com.tangem.core.decompose.di.ModelComponent
import com.tangem.core.decompose.model.Model
import com.tangem.features.swap.v2.impl.amount.model.SwapAmountModel
import dagger.Binds
import dagger.Module
import dagger.hilt.InstallIn
import dagger.multibindings.ClassKey
import dagger.multibindings.IntoMap
@InstallIn(ModelComponent::class)
@Module
internal interface SwapAmountModule {
@Binds
@IntoMap
@ClassKey(SwapAmountModel::class)
fun provideSwapAmountModel(impl: SwapAmountModel): Model
}

View file

@ -0,0 +1,93 @@
package com.tangem.features.swap.v2.impl.amount.entity
import androidx.compose.runtime.Immutable
import com.tangem.common.ui.amountScreen.models.AmountState
import com.tangem.core.ui.components.atoms.text.TextEllipsis
import com.tangem.core.ui.extensions.TextReference
import com.tangem.domain.appcurrency.model.AppCurrency
import com.tangem.domain.express.models.ExpressRateType
import com.tangem.domain.swap.models.SwapCurrencies
import com.tangem.domain.swap.models.SwapDirection
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
import com.tangem.features.swap.v2.impl.common.entity.SwapQuoteUM
import kotlinx.collections.immutable.ImmutableList
@Immutable
internal sealed class SwapAmountUM {
abstract val isPrimaryButtonEnabled: Boolean
abstract val primaryAmount: SwapAmountFieldUM
abstract val secondaryAmount: SwapAmountFieldUM
abstract val selectedAmountType: SwapAmountType
data object Empty : SwapAmountUM() {
override val isPrimaryButtonEnabled = false
override val selectedAmountType = SwapAmountType.From
override val primaryAmount = SwapAmountFieldUM.Empty(SwapAmountType.From)
override val secondaryAmount = SwapAmountFieldUM.Empty(SwapAmountType.To)
}
data class Content(
override val isPrimaryButtonEnabled: Boolean,
// two amount fields
override val primaryAmount: SwapAmountFieldUM,
override val secondaryAmount: SwapAmountFieldUM,
override val selectedAmountType: SwapAmountType,
val primaryCryptoCurrencyStatus: CryptoCurrencyStatus?,
val secondaryCryptoCurrencyStatus: CryptoCurrencyStatus?,
// selected swap route
val swapDirection: SwapDirection,
val swapRateType: ExpressRateType,
// swap models
val swapCurrencies: SwapCurrencies,
val swapQuotes: ImmutableList<SwapQuoteUM>,
val selectedQuote: SwapQuoteUM,
// extra data
val appCurrency: AppCurrency?,
) : SwapAmountUM()
}
@Immutable
sealed class SwapAmountFieldUM {
abstract val amountType: SwapAmountType
abstract val amountField: AmountState
data class Empty(
override val amountType: SwapAmountType,
) : SwapAmountFieldUM() {
override val amountField: AmountState = AmountState.Empty(isPrimaryButtonEnabled = false)
}
data class Loading(
override val amountType: SwapAmountType,
) : SwapAmountFieldUM() {
override val amountField: AmountState = AmountState.Empty(isPrimaryButtonEnabled = false)
}
data class Content(
override val amountType: SwapAmountType,
override val amountField: AmountState,
val priceImpact: TextReference?,
val title: TextReference,
val subtitle: TextReference,
val subtitleEllipsis: TextEllipsis,
val isClickEnabled: Boolean,
) : SwapAmountFieldUM()
}
@Immutable
sealed class PriceImpactUM {
data object Empty : PriceImpactUM()
data class Value(val value: Float) : PriceImpactUM()
}
enum class SwapAmountType {
From,
To,
}

View file

@ -0,0 +1,10 @@
package com.tangem.features.swap.v2.impl.amount.model
import com.tangem.common.ui.amountScreen.AmountScreenClickIntents
import com.tangem.features.swap.v2.impl.amount.entity.SwapAmountType
internal interface SwapAmountClickIntents : AmountScreenClickIntents {
fun onExpandEditField(selectedAmountType: SwapAmountType)
fun onSelectTokenClick()
}

View file

@ -0,0 +1,498 @@
package com.tangem.features.swap.v2.impl.amount.model
import arrow.core.getOrElse
import com.tangem.common.routing.AppRoute
import com.tangem.common.routing.AppRouter
import com.tangem.common.ui.amountScreen.converters.MaxEnterAmountConverter
import com.tangem.common.ui.amountScreen.models.AmountState
import com.tangem.common.ui.amountScreen.models.EnterAmountBoundary
import com.tangem.common.ui.navigationButtons.NavigationButton
import com.tangem.core.decompose.di.ModelScoped
import com.tangem.core.decompose.model.Model
import com.tangem.core.decompose.model.ParamsContainer
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.extensions.stringReference
import com.tangem.core.ui.format.bigdecimal.crypto
import com.tangem.core.ui.format.bigdecimal.format
import com.tangem.domain.express.models.ExpressProvider
import com.tangem.domain.express.models.ExpressRateType
import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.domain.swap.models.SwapCurrencies
import com.tangem.domain.swap.models.SwapDirection
import com.tangem.domain.swap.models.SwapQuoteModel
import com.tangem.domain.swap.usecase.GetSwapPairsUseCase
import com.tangem.domain.swap.usecase.GetSwapQuoteUseCase
import com.tangem.domain.swap.usecase.SelectInitialPairUseCase
import com.tangem.domain.tokens.GetMinimumTransactionAmountSyncUseCase
import com.tangem.domain.tokens.GetMultiCryptoCurrencyStatusUseCase
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
import com.tangem.domain.transaction.usecase.GetAllowanceUseCase
import com.tangem.features.swap.v2.impl.R
import com.tangem.features.swap.v2.impl.amount.SwapAmountComponentParams
import com.tangem.features.swap.v2.impl.amount.entity.SwapAmountFieldUM
import com.tangem.features.swap.v2.impl.amount.entity.SwapAmountType
import com.tangem.features.swap.v2.impl.amount.entity.SwapAmountUM
import com.tangem.features.swap.v2.impl.amount.model.converter.SwapAmountReadyStateConverter
import com.tangem.features.swap.v2.impl.amount.model.transformers.*
import com.tangem.features.swap.v2.impl.chooseprovider.SwapChooseProviderComponent
import com.tangem.features.swap.v2.impl.choosetoken.fromSupported.SwapChooseTokenNetworkListener
import com.tangem.features.swap.v2.impl.common.entity.NavigationUM
import com.tangem.features.swap.v2.impl.common.entity.SwapQuoteUM
import com.tangem.features.swap.v2.impl.common.entity.SwapQuoteUM.Content.DifferencePercent
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import com.tangem.utils.extensions.orZero
import kotlinx.collections.immutable.persistentListOf
import kotlinx.coroutines.async
import kotlinx.coroutines.awaitAll
import kotlinx.coroutines.flow.*
import kotlinx.coroutines.launch
import timber.log.Timber
import javax.inject.Inject
import kotlin.properties.Delegates
import com.tangem.utils.transformer.update as transformerUpdate
@Suppress("LargeClass", "LongParameterList")
@ModelScoped
internal class SwapAmountModel @Inject constructor(
paramsContainer: ParamsContainer,
override val dispatchers: CoroutineDispatcherProvider,
private val getSwapPairsUseCase: GetSwapPairsUseCase,
private val getMultiCryptoCurrencyStatusUseCase: GetMultiCryptoCurrencyStatusUseCase,
private val getMinimumTransactionAmountSyncUseCase: GetMinimumTransactionAmountSyncUseCase,
private val selectInitialPairUseCase: SelectInitialPairUseCase,
private val getSwapQuoteUseCase: GetSwapQuoteUseCase,
private val swapChooseTokenNetworkListener: SwapChooseTokenNetworkListener,
private val getAllowanceUseCase: GetAllowanceUseCase,
private val appRouter: AppRouter,
) : Model(), SwapAmountClickIntents, SwapChooseProviderComponent.ModelCallback {
private val params: SwapAmountComponentParams = paramsContainer.require()
private val swapDirection = params.swapDirection
private val appCurrency = params.appCurrency
private var userWallet = params.userWallet
private var primaryCryptoCurrency: CryptoCurrency by Delegates.notNull()
private var secondaryCryptoCurrency: CryptoCurrency? = null
private var primaryCryptoCurrencyStatus: CryptoCurrencyStatus by Delegates.notNull()
private var secondaryCryptoCurrencyStatus: CryptoCurrencyStatus by Delegates.notNull()
private var primaryMaximumAmountBoundary: EnterAmountBoundary by Delegates.notNull()
private var secondaryMaximumAmountBoundary: EnterAmountBoundary by Delegates.notNull()
private var primaryMinimumAmountBoundary: EnterAmountBoundary by Delegates.notNull()
private var secondaryMinimumAmountBoundary: EnterAmountBoundary by Delegates.notNull()
val uiState: StateFlow<SwapAmountUM>
field = MutableStateFlow(params.amountUM)
init {
when (params) {
is SwapAmountComponentParams.AmountParams -> {
primaryCryptoCurrency = params.primaryCryptoCurrency
secondaryCryptoCurrency = params.secondaryCryptoCurrency
}
is SwapAmountComponentParams.AmountBlockParams -> {
primaryCryptoCurrency = params.primaryCryptoCurrencyStatus.currency
secondaryCryptoCurrency = params.secondaryCryptoCurrencyStatus?.currency
primaryCryptoCurrencyStatus = params.primaryCryptoCurrencyStatus
params.secondaryCryptoCurrencyStatus?.let {
secondaryCryptoCurrencyStatus = it
}
}
}
initialState()
configAmountNavigation()
observeChooseSelectToken()
// todo observe balance hiding flow
}
fun updateState(amountUM: SwapAmountUM) {
uiState.update { amountUM }
}
override fun onProviderResult(quoteUM: SwapQuoteUM) {
uiState.transformerUpdate(
SwapAmountSelectQuoteTransformer(
quoteUM = quoteUM,
primaryCryptoCurrencyStatus = primaryCryptoCurrencyStatus,
secondaryCryptoCurrencyStatus = secondaryCryptoCurrencyStatus,
secondaryMaximumAmountBoundary = secondaryMaximumAmountBoundary,
secondaryMinimumAmountBoundary = secondaryMinimumAmountBoundary,
),
)
}
override fun onExpandEditField(selectedAmountType: SwapAmountType) {
uiState.update { amountUM ->
if (amountUM !is SwapAmountUM.Content) return
amountUM.copy(
selectedAmountType = selectedAmountType,
)
}
}
override fun onAmountValueChange(value: String) {
uiState.transformerUpdate(
SwapAmountValueChangeTransformer(
primaryCryptoCurrencyStatus = primaryCryptoCurrencyStatus,
secondaryCryptoCurrencyStatus = secondaryCryptoCurrencyStatus,
primaryMaximumAmountBoundary = primaryMaximumAmountBoundary,
secondaryMaximumAmountBoundary = secondaryMaximumAmountBoundary,
primaryMinimumAmountBoundary = primaryMinimumAmountBoundary,
secondaryMinimumAmountBoundary = secondaryMinimumAmountBoundary,
value = value,
),
)
loadQuotes()
}
override fun onAmountPasteTriggerDismiss() {
uiState.transformerUpdate(SwapAmountPasteTransformer)
}
override fun onMaxValueClick() {
uiState.transformerUpdate(
SwapAmountValueMaxTransformer(
primaryCryptoCurrencyStatus = primaryCryptoCurrencyStatus,
secondaryCryptoCurrencyStatus = secondaryCryptoCurrencyStatus,
primaryMaximumAmountBoundary = primaryMaximumAmountBoundary,
secondaryMaximumAmountBoundary = secondaryMaximumAmountBoundary,
primaryMinimumAmountBoundary = primaryMinimumAmountBoundary,
secondaryMinimumAmountBoundary = secondaryMinimumAmountBoundary,
),
)
loadQuotes()
}
override fun onCurrencyChangeClick(isFiat: Boolean) {
uiState.transformerUpdate(
SwapAmountChangeCurrencyTransformer(
primaryCryptoCurrencyStatus = primaryCryptoCurrencyStatus,
secondaryCryptoCurrencyStatus = secondaryCryptoCurrencyStatus,
isFiatSelected = isFiat,
),
)
}
override fun onAmountNext() {
saveResult()
}
override fun onSelectTokenClick() {
appRouter.push(
AppRoute.ChooseManagedTokens(
userWalletId = userWallet.walletId,
initialCurrency = primaryCryptoCurrency,
source = AppRoute.ChooseManagedTokens.Source.SendViaSwap,
),
)
}
private fun observeChooseSelectToken() {
swapChooseTokenNetworkListener.swapChooseTokenNetworkResultFlow
.onEach { currency ->
uiState.update { amountUM ->
if (amountUM is SwapAmountUM.Content) {
amountUM.copy(
isPrimaryButtonEnabled = false,
secondaryAmount = SwapAmountFieldUM.Loading(
amountType = SwapAmountType.To,
),
)
} else {
amountUM
}
}
initPairs(primaryCryptoCurrency, currency)
}
.launchIn(modelScope)
}
private fun initialState() {
if (uiState.value is SwapAmountUM.Empty) {
initPairs(
primaryCryptoCurrency = primaryCryptoCurrency,
secondaryCryptoCurrency = secondaryCryptoCurrency,
)
uiState.update {
SwapAmountUM.Content(
isPrimaryButtonEnabled = it.isPrimaryButtonEnabled,
primaryAmount = it.primaryAmount,
secondaryAmount = it.secondaryAmount,
swapDirection = swapDirection,
swapCurrencies = SwapCurrencies.EMPTY,
appCurrency = appCurrency,
swapQuotes = persistentListOf(),
swapRateType = ExpressRateType.Float,
selectedAmountType = SwapAmountType.From,
selectedQuote = SwapQuoteUM.Empty,
primaryCryptoCurrencyStatus = null,
secondaryCryptoCurrencyStatus = null,
)
}
}
}
private fun initPairs(primaryCryptoCurrency: CryptoCurrency, secondaryCryptoCurrency: CryptoCurrency?) {
modelScope.launch {
val cryptoCurrencyStatusList = getMultiCryptoCurrencyStatusUseCase
.invokeMultiWalletSync(userWallet.walletId)
.getOrElse { emptyList() }
val primaryStatus = cryptoCurrencyStatusList.firstOrNull {
it.currency.id == primaryCryptoCurrency.id
}
if (primaryStatus == null) {
Timber.e("Failed to get crypto currency status")
// todo error
return@launch
}
val cryptoCurrencyStatusListExceptPrimary = cryptoCurrencyStatusList.filter {
val statusFilter = it.value is CryptoCurrencyStatus.Loaded || it.value is CryptoCurrencyStatus.NoAccount
val notCustomTokenFilter = !it.currency.isCustom
statusFilter && notCustomTokenFilter
}
getSwapPairsUseCase(
userWallet = userWallet,
initialCurrency = primaryCryptoCurrency,
cryptoCurrencyStatusList = cryptoCurrencyStatusListExceptPrimary,
).fold(
ifRight = { swapCurrencies ->
val secondaryStatus = selectInitialPairUseCase(
primaryCryptoCurrency = primaryCryptoCurrency,
secondaryCryptoCurrency = secondaryCryptoCurrency,
userWallet = userWallet,
swapCurrencies = swapCurrencies,
swapDirection = params.swapDirection,
)
if (secondaryStatus != null) {
initCurrencies(primaryStatus, secondaryStatus)
uiState.update {
SwapAmountReadyStateConverter(
swapCurrencies = swapCurrencies,
userWallet = userWallet,
primaryCryptoCurrencyStatus = primaryCryptoCurrencyStatus,
secondaryCryptoCurrencyStatus = secondaryCryptoCurrencyStatus,
appCurrency = appCurrency,
swapDirection = swapDirection,
clickIntents = this@SwapAmountModel,
isBalanceHidden = params.isBalanceHidingFlow.value,
).convert(Unit)
}
} else {
// todo not available currency to swap
}
},
ifLeft = {
// todo error
},
)
}
}
private suspend fun initCurrencies(primaryStatus: CryptoCurrencyStatus, secondaryStatus: CryptoCurrencyStatus) {
primaryCryptoCurrencyStatus = primaryStatus
secondaryCryptoCurrencyStatus = secondaryStatus
primaryMinimumAmountBoundary = EnterAmountBoundary(
amount = getMinimumTransactionAmountSyncUseCase
.invoke(
userWalletId = userWallet.walletId,
cryptoCurrencyStatus = primaryStatus,
).getOrNull().orZero(),
fiatRate = primaryStatus.value.fiatRate,
fiatAmount = primaryStatus.value.fiatAmount,
)
secondaryMinimumAmountBoundary = EnterAmountBoundary(
amount = getMinimumTransactionAmountSyncUseCase
.invoke(
userWalletId = userWallet.walletId,
cryptoCurrencyStatus = secondaryStatus,
).getOrNull().orZero(),
fiatRate = secondaryStatus.value.fiatRate,
fiatAmount = secondaryStatus.value.fiatAmount,
)
primaryMaximumAmountBoundary = MaxEnterAmountConverter().convert(primaryStatus)
secondaryMaximumAmountBoundary = MaxEnterAmountConverter().convert(secondaryStatus)
}
private fun loadQuotes() {
val state = uiState.value as? SwapAmountUM.Content ?: return
val (fromCryptoCurrency, toCryptoCurrency) = when (state.swapDirection) {
SwapDirection.Direct -> primaryCryptoCurrencyStatus.currency to secondaryCryptoCurrencyStatus.currency
SwapDirection.Reverse -> secondaryCryptoCurrencyStatus.currency to primaryCryptoCurrencyStatus.currency
}
val fromAmount = when (state.swapDirection) {
SwapDirection.Direct -> state.primaryAmount.amountField
SwapDirection.Reverse -> state.secondaryAmount.amountField
} as? AmountState.Data
if (fromAmount?.amountTextField?.isError == true) return
val fromAmountValue = fromAmount?.amountTextField?.cryptoAmount?.value ?: return
val swapGroups = when (state.swapDirection) {
SwapDirection.Direct -> state.swapCurrencies.toGroup.available
SwapDirection.Reverse -> state.swapCurrencies.fromGroup.available
}
uiState.transformerUpdate(SwapQuoteLoadingStateTransformer)
modelScope.launch {
val quotes = swapGroups.firstOrNull {
it.currencyStatus.currency.id == toCryptoCurrency.id
}?.providers?.map { provider ->
async {
getSwapQuoteUseCase(
userWallet = userWallet,
fromCryptoCurrency = fromCryptoCurrency,
toCryptoCurrency = toCryptoCurrency,
fromAmount = fromAmountValue,
provider = provider,
).fold(
ifLeft = { error ->
SwapQuoteUM.Error(
provider = provider,
expressError = error,
)
},
ifRight = { quote: SwapQuoteModel ->
convertToSwapProviderUM(
quote = quote,
provider = provider,
differencePercent = DifferencePercent.Empty,
swapDirection = swapDirection,
)
},
)
}
}?.awaitAll().orEmpty()
uiState.transformerUpdate(
SwapAmountSetQuotesTransformer(
quotes = quotes,
primaryCryptoCurrencyStatus = primaryCryptoCurrencyStatus,
secondaryCryptoCurrencyStatus = secondaryCryptoCurrencyStatus,
secondaryMaximumAmountBoundary = secondaryMaximumAmountBoundary,
secondaryMinimumAmountBoundary = secondaryMinimumAmountBoundary,
),
)
}
}
private suspend fun convertToSwapProviderUM(
quote: SwapQuoteModel,
provider: ExpressProvider,
differencePercent: DifferencePercent,
swapDirection: SwapDirection,
): SwapQuoteUM {
// todo swap allowance
val allowanceContract = quote.allowanceContract
return if (allowanceContract != null) {
val allowance = getAllowanceUseCase(
userWalletId = userWallet.walletId,
cryptoCurrency = primaryCryptoCurrencyStatus.currency,
spenderAddress = allowanceContract,
).getOrNull().orZero()
val isApprovalNeeded = allowance < primaryCryptoCurrencyStatus.value.amount.orZero()
if (isApprovalNeeded) {
SwapQuoteUM.Allowance(
provider = provider,
allowanceContract = allowanceContract,
)
} else {
SwapQuoteUM.Content(
provider = provider,
quoteAmount = quote.toTokenAmount,
diffPercent = differencePercent,
quoteAmountValue = stringReference(
quote.toTokenAmount.format {
crypto(
when (swapDirection) {
SwapDirection.Direct -> secondaryCryptoCurrencyStatus.currency
SwapDirection.Reverse -> primaryCryptoCurrencyStatus.currency
},
)
},
),
)
}
} else {
SwapQuoteUM.Content(
provider = provider,
quoteAmount = quote.toTokenAmount,
diffPercent = differencePercent,
quoteAmountValue = stringReference(
quote.toTokenAmount.format {
crypto(
when (swapDirection) {
SwapDirection.Direct -> secondaryCryptoCurrencyStatus.currency
SwapDirection.Reverse -> primaryCryptoCurrencyStatus.currency
},
)
},
),
)
}
}
private fun saveResult() {
val params = params as? SwapAmountComponentParams.AmountParams ?: return
params.callback.onAmountResult(uiState.value)
}
private fun configAmountNavigation() {
val params = params as? SwapAmountComponentParams.AmountParams ?: return
combine(
flow = uiState,
flow2 = params.currentRoute,
transform = { state, route -> state to route },
).onEach { (state, route) ->
params.callback.onNavigationResult(
NavigationUM.Content(
title = resourceReference(R.string.common_swap),
subtitle = null,
backIconRes = if (route.isEditMode) {
R.drawable.ic_back_24
} else {
R.drawable.ic_close_24
},
backIconClick = {
// if (!route.isEditMode) {
// todo analytics
// analyticsEventHandler.send(
// CommonSendAnalyticEvents.CloseButtonClicked(
// categoryName = params.analyticsCategoryName,
// source = SendScreenSource.Address,
// isFromSummary = false,
// isValid = state.isPrimaryButtonEnabled,
// ),
// )
// }
params.callback.onBackClick()
},
primaryButton = NavigationButton(
textReference = if (route.isEditMode) {
resourceReference(R.string.common_continue)
} else {
resourceReference(R.string.common_next)
},
isEnabled = state.isPrimaryButtonEnabled,
onClick = {
saveResult()
params.callback.onNextClick()
},
),
),
)
}.launchIn(modelScope)
}
}

View file

@ -0,0 +1,60 @@
package com.tangem.features.swap.v2.impl.amount.model
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.stringReference
import com.tangem.core.ui.format.bigdecimal.format
import com.tangem.core.ui.format.bigdecimal.percent
import com.tangem.domain.swap.models.SwapDirection
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
import com.tangem.features.swap.v2.impl.amount.entity.SwapAmountFieldUM
import com.tangem.features.swap.v2.impl.amount.entity.SwapAmountType
import com.tangem.features.swap.v2.impl.amount.entity.SwapAmountUM
import com.tangem.utils.extensions.isZero
import java.math.BigDecimal
import java.math.RoundingMode
internal object SwapAmountQuoteUtils {
fun calculatePriceImpact(
fromTokenAmount: BigDecimal,
toTokenAmount: BigDecimal,
swapDirection: SwapDirection,
primaryCryptoCurrencyStatus: CryptoCurrencyStatus,
secondaryCryptoCurrencyStatus: CryptoCurrencyStatus,
): TextReference? {
val (fromRate, toRate) = if (swapDirection == SwapDirection.Direct) {
primaryCryptoCurrencyStatus.value.fiatRate to secondaryCryptoCurrencyStatus.value.fiatRate
} else {
secondaryCryptoCurrencyStatus.value.fiatRate to primaryCryptoCurrencyStatus.value.fiatRate
}
val fromTokenFiatValue = fromTokenAmount.multiply(fromRate)
val toTokenFiatValue = toTokenAmount.multiply(toRate)
// Check for zero division
if (fromTokenFiatValue.isZero() || toTokenFiatValue.isZero()) return null
val value = BigDecimal.ONE - toTokenFiatValue.divide(fromTokenFiatValue, 2, RoundingMode.HALF_UP)
return stringReference("$(-${value.format { percent(withoutSign = false) }})").takeIf {
value > 0.1.toBigDecimal()
}
}
fun SwapAmountUM.updateAmount(
onPrimaryAmount: SwapAmountFieldUM.Content.() -> SwapAmountFieldUM,
onSecondaryAmount: SwapAmountFieldUM.Content.() -> SwapAmountFieldUM,
): SwapAmountUM {
if (this !is SwapAmountUM.Content) return this
return if (
selectedAmountType == SwapAmountType.From && swapDirection == SwapDirection.Direct
) {
val amountFieldUM = primaryAmount as? SwapAmountFieldUM.Content ?: return this
copy(primaryAmount = amountFieldUM.onPrimaryAmount())
} else {
val amountFieldUM = secondaryAmount as? SwapAmountFieldUM.Content ?: return this
copy(secondaryAmount = amountFieldUM.onSecondaryAmount())
}
}
}

View file

@ -0,0 +1,90 @@
package com.tangem.features.swap.v2.impl.amount.model.converter
import com.tangem.common.ui.amountScreen.AmountScreenClickIntents
import com.tangem.common.ui.amountScreen.converters.AmountStateConverterV2
import com.tangem.common.ui.amountScreen.converters.MaxEnterAmountConverter
import com.tangem.common.ui.amountScreen.models.AmountParameters
import com.tangem.core.ui.components.atoms.text.TextEllipsis
import com.tangem.core.ui.components.currency.icon.converter.CryptoCurrencyToIconStateConverter
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.domain.appcurrency.model.AppCurrency
import com.tangem.domain.swap.models.SwapDirection
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
import com.tangem.domain.wallets.models.UserWallet
import com.tangem.features.swap.v2.impl.R
import com.tangem.features.swap.v2.impl.amount.entity.SwapAmountFieldUM
import com.tangem.features.swap.v2.impl.amount.entity.SwapAmountType
internal class SwapAmountFieldConverter(
private val swapDirection: SwapDirection,
private val isBalanceHidden: Boolean,
private val userWallet: UserWallet,
private val appCurrency: AppCurrency,
private val clickIntents: AmountScreenClickIntents,
) {
private val iconStateConverter = CryptoCurrencyToIconStateConverter()
private val maxEnterAmountConverter = MaxEnterAmountConverter()
fun convert(selectedType: SwapAmountType, cryptoCurrencyStatus: CryptoCurrencyStatus): SwapAmountFieldUM {
return SwapAmountFieldUM.Content(
amountType = selectedType,
title = stringReference(cryptoCurrencyStatus.currency.name),
subtitle = getSubtitle(selectedType = selectedType, cryptoCurrencyStatus = cryptoCurrencyStatus),
subtitleEllipsis = getSubtitleEllipsis(
selectedType = selectedType,
cryptoCurrencyStatus = cryptoCurrencyStatus,
),
priceImpact = null,
isClickEnabled = selectedType.isViewingField(),
amountField = AmountStateConverterV2(
clickIntents = clickIntents,
appCurrency = appCurrency,
cryptoCurrencyStatus = cryptoCurrencyStatus,
maxEnterAmount = maxEnterAmountConverter.convert(cryptoCurrencyStatus),
iconStateConverter = iconStateConverter,
isRedesignEnabled = true,
).convert(
AmountParameters(
title = combinedReference(
resourceReference(R.string.send_from_wallet_android),
stringReference(" "),
stringReference(userWallet.name),
),
value = "",
),
),
)
}
private fun getSubtitle(selectedType: SwapAmountType, cryptoCurrencyStatus: CryptoCurrencyStatus) = when {
selectedType.isEnteringField() -> resourceReference(
R.string.common_balance,
wrappedList(
cryptoCurrencyStatus.value.amount.format {
crypto(cryptoCurrency = cryptoCurrencyStatus.currency)
},
),
).orMaskWithStars(isBalanceHidden)
selectedType.isViewingField() -> resourceReference(R.string.send_with_swap_recipient_amount_text)
else -> TextReference.Companion.EMPTY
}
private fun getSubtitleEllipsis(selectedType: SwapAmountType, cryptoCurrencyStatus: CryptoCurrencyStatus) = when {
selectedType.isEnteringField() -> TextEllipsis.OffsetEnd(cryptoCurrencyStatus.currency.symbol.length)
selectedType.isViewingField() -> TextEllipsis.End
else -> TextEllipsis.End
}
private fun SwapAmountType.isEnteringField(): Boolean {
return this == SwapAmountType.From && swapDirection == SwapDirection.Direct ||
this == SwapAmountType.To && swapDirection == SwapDirection.Reverse
}
private fun SwapAmountType.isViewingField(): Boolean {
return this == SwapAmountType.From && swapDirection == SwapDirection.Reverse ||
this == SwapAmountType.To && swapDirection == SwapDirection.Direct
}
}

View file

@ -0,0 +1,58 @@
package com.tangem.features.swap.v2.impl.amount.model.converter
import com.tangem.common.ui.amountScreen.AmountScreenClickIntents
import com.tangem.domain.appcurrency.model.AppCurrency
import com.tangem.domain.express.models.ExpressRateType
import com.tangem.domain.swap.models.SwapCurrencies
import com.tangem.domain.swap.models.SwapDirection
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
import com.tangem.domain.wallets.models.UserWallet
import com.tangem.features.swap.v2.impl.amount.entity.SwapAmountType
import com.tangem.features.swap.v2.impl.amount.entity.SwapAmountUM
import com.tangem.features.swap.v2.impl.common.entity.SwapQuoteUM
import com.tangem.utils.converter.Converter
import kotlinx.collections.immutable.persistentListOf
@Suppress("LongParameterList")
internal class SwapAmountReadyStateConverter(
private val userWallet: UserWallet,
private val swapCurrencies: SwapCurrencies,
private val primaryCryptoCurrencyStatus: CryptoCurrencyStatus,
private val secondaryCryptoCurrencyStatus: CryptoCurrencyStatus,
private val appCurrency: AppCurrency,
private val clickIntents: AmountScreenClickIntents,
private val swapDirection: SwapDirection,
private val isBalanceHidden: Boolean,
) : Converter<Unit, SwapAmountUM> {
private val amountFieldConverter = SwapAmountFieldConverter(
swapDirection = swapDirection,
isBalanceHidden = isBalanceHidden,
userWallet = userWallet,
appCurrency = appCurrency,
clickIntents = clickIntents,
)
override fun convert(value: Unit): SwapAmountUM {
return SwapAmountUM.Content(
primaryCryptoCurrencyStatus = primaryCryptoCurrencyStatus,
secondaryCryptoCurrencyStatus = secondaryCryptoCurrencyStatus,
primaryAmount = amountFieldConverter.convert(
selectedType = SwapAmountType.From,
cryptoCurrencyStatus = primaryCryptoCurrencyStatus,
),
secondaryAmount = amountFieldConverter.convert(
selectedType = SwapAmountType.To,
cryptoCurrencyStatus = secondaryCryptoCurrencyStatus,
),
swapCurrencies = swapCurrencies,
swapDirection = swapDirection,
swapQuotes = persistentListOf(),
selectedQuote = SwapQuoteUM.Empty,
selectedAmountType = SwapAmountType.From,
appCurrency = appCurrency,
swapRateType = ExpressRateType.Float,
isPrimaryButtonEnabled = false,
)
}
}

View file

@ -0,0 +1,34 @@
package com.tangem.features.swap.v2.impl.amount.model.transformers
import com.tangem.common.ui.amountScreen.converters.AmountCurrencyTransformer
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
import com.tangem.features.swap.v2.impl.amount.entity.SwapAmountUM
import com.tangem.features.swap.v2.impl.amount.model.SwapAmountQuoteUtils.updateAmount
import com.tangem.utils.transformer.Transformer
internal class SwapAmountChangeCurrencyTransformer(
private val primaryCryptoCurrencyStatus: CryptoCurrencyStatus,
private val secondaryCryptoCurrencyStatus: CryptoCurrencyStatus,
private val isFiatSelected: Boolean,
) : Transformer<SwapAmountUM> {
override fun transform(prevState: SwapAmountUM): SwapAmountUM {
return prevState.updateAmount(
onPrimaryAmount = {
copy(
amountField = AmountCurrencyTransformer(
cryptoCurrencyStatus = primaryCryptoCurrencyStatus,
value = isFiatSelected,
).transform(prevState.primaryAmount.amountField),
)
},
onSecondaryAmount = {
copy(
amountField = AmountCurrencyTransformer(
cryptoCurrencyStatus = secondaryCryptoCurrencyStatus,
value = isFiatSelected,
).transform(prevState.secondaryAmount.amountField),
)
},
)
}
}

View file

@ -0,0 +1,27 @@
package com.tangem.features.swap.v2.impl.amount.model.transformers
import com.tangem.common.ui.amountScreen.converters.AmountPastedTriggerDismissTransformer
import com.tangem.features.swap.v2.impl.amount.entity.SwapAmountUM
import com.tangem.features.swap.v2.impl.amount.model.SwapAmountQuoteUtils.updateAmount
import com.tangem.utils.transformer.Transformer
internal object SwapAmountPasteTransformer : Transformer<SwapAmountUM> {
override fun transform(prevState: SwapAmountUM): SwapAmountUM {
return prevState.updateAmount(
onPrimaryAmount = {
copy(
amountField = AmountPastedTriggerDismissTransformer.transform(
prevState = prevState.primaryAmount.amountField,
),
)
},
onSecondaryAmount = {
copy(
amountField = AmountPastedTriggerDismissTransformer.transform(
prevState = prevState.secondaryAmount.amountField,
),
)
},
)
}
}

View file

@ -0,0 +1,60 @@
package com.tangem.features.swap.v2.impl.amount.model.transformers
import com.tangem.common.ui.amountScreen.converters.field.AmountFieldChangeTransformer
import com.tangem.common.ui.amountScreen.models.AmountState
import com.tangem.common.ui.amountScreen.models.EnterAmountBoundary
import com.tangem.core.ui.utils.parseBigDecimal
import com.tangem.domain.swap.models.SwapDirection
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
import com.tangem.features.swap.v2.impl.amount.entity.SwapAmountFieldUM
import com.tangem.features.swap.v2.impl.amount.entity.SwapAmountType
import com.tangem.features.swap.v2.impl.amount.entity.SwapAmountUM
import com.tangem.features.swap.v2.impl.amount.model.SwapAmountQuoteUtils.calculatePriceImpact
import com.tangem.features.swap.v2.impl.common.entity.SwapQuoteUM
import com.tangem.utils.extensions.orZero
import com.tangem.utils.transformer.Transformer
internal class SwapAmountSelectQuoteTransformer(
private val quoteUM: SwapQuoteUM,
private val primaryCryptoCurrencyStatus: CryptoCurrencyStatus,
private val secondaryCryptoCurrencyStatus: CryptoCurrencyStatus,
private val secondaryMaximumAmountBoundary: EnterAmountBoundary,
private val secondaryMinimumAmountBoundary: EnterAmountBoundary,
) : Transformer<SwapAmountUM> {
override fun transform(prevState: SwapAmountUM): SwapAmountUM {
if (prevState !is SwapAmountUM.Content) return prevState
return prevState.copy(
isPrimaryButtonEnabled = true,
selectedQuote = quoteUM,
secondaryAmount = if (
prevState.selectedAmountType == SwapAmountType.From && prevState.swapDirection == SwapDirection.Direct
) {
val secondaryAmountField = prevState.secondaryAmount as? SwapAmountFieldUM.Content
val fromAmount = (prevState.primaryAmount.amountField as? AmountState.Data)
?.amountTextField?.cryptoAmount?.value.orZero()
val toAmount = (quoteUM as? SwapQuoteUM.Content)?.quoteAmount
val priceImpact = calculatePriceImpact(
swapDirection = prevState.swapDirection,
fromTokenAmount = fromAmount,
toTokenAmount = toAmount.orZero(),
primaryCryptoCurrencyStatus = primaryCryptoCurrencyStatus,
secondaryCryptoCurrencyStatus = secondaryCryptoCurrencyStatus,
)
secondaryAmountField?.copy(
priceImpact = priceImpact,
amountField = AmountFieldChangeTransformer(
cryptoCurrencyStatus = secondaryCryptoCurrencyStatus,
maxEnterAmount = secondaryMaximumAmountBoundary,
minimumTransactionAmount = secondaryMinimumAmountBoundary,
value = toAmount?.parseBigDecimal(secondaryCryptoCurrencyStatus.currency.decimals)
.orEmpty(),
).transform(secondaryAmountField.amountField),
) ?: prevState.secondaryAmount
} else {
prevState.secondaryAmount
},
)
}
}

View file

@ -0,0 +1,172 @@
package com.tangem.features.swap.v2.impl.amount.model.transformers
import com.tangem.common.ui.amountScreen.converters.field.AmountFieldChangeTransformer
import com.tangem.common.ui.amountScreen.models.AmountState
import com.tangem.common.ui.amountScreen.models.EnterAmountBoundary
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.extensions.stringReference
import com.tangem.core.ui.extensions.wrappedList
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.utils.parseBigDecimal
import com.tangem.domain.express.models.ExpressError
import com.tangem.domain.swap.models.SwapDirection
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
import com.tangem.features.swap.v2.impl.R
import com.tangem.features.swap.v2.impl.amount.entity.SwapAmountFieldUM
import com.tangem.features.swap.v2.impl.amount.entity.SwapAmountType
import com.tangem.features.swap.v2.impl.amount.entity.SwapAmountUM
import com.tangem.features.swap.v2.impl.amount.model.SwapAmountQuoteUtils.calculatePriceImpact
import com.tangem.features.swap.v2.impl.common.entity.SwapQuoteUM
import com.tangem.features.swap.v2.impl.common.entity.SwapQuoteUM.Content.DifferencePercent
import com.tangem.utils.StringsSigns
import com.tangem.utils.extensions.isPositive
import com.tangem.utils.extensions.orZero
import com.tangem.utils.transformer.Transformer
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.toPersistentList
import java.math.BigDecimal
internal class SwapAmountSetQuotesTransformer(
private val quotes: List<SwapQuoteUM>,
private val primaryCryptoCurrencyStatus: CryptoCurrencyStatus,
private val secondaryCryptoCurrencyStatus: CryptoCurrencyStatus,
private val secondaryMaximumAmountBoundary: EnterAmountBoundary,
private val secondaryMinimumAmountBoundary: EnterAmountBoundary,
) : Transformer<SwapAmountUM> {
override fun transform(prevState: SwapAmountUM): SwapAmountUM {
if (prevState !is SwapAmountUM.Content) return prevState
val fromAmount = when (prevState.swapDirection) {
SwapDirection.Direct -> prevState.primaryAmount.amountField
SwapDirection.Reverse -> prevState.secondaryAmount.amountField
} as? AmountState.Data
val fromAmountValue = fromAmount?.amountTextField?.cryptoAmount?.value.orZero()
val sortedQuotes = quotes.sortedWith(SwapQuotesComparator)
val bestQuote = findBestQuote(quotes) ?: SwapQuoteUM.Empty
return prevState.copy(
isPrimaryButtonEnabled = bestQuote is SwapQuoteUM.Content,
swapQuotes = getQuotesWithDiff(sortedQuotes, bestQuote),
selectedQuote = bestQuote,
primaryAmount = if (
prevState.selectedAmountType == SwapAmountType.From && prevState.swapDirection == SwapDirection.Direct
) {
val swapAmountField = prevState.primaryAmount as? SwapAmountFieldUM.Content
val amountField = swapAmountField?.amountField as? AmountState.Data
val amountError = (bestQuote as? SwapQuoteUM.Error)?.expressError.getAmountError()
if (amountField?.amountTextField?.isError == true) {
prevState.primaryAmount
} else {
swapAmountField?.copy(
amountField = amountField?.copy(
amountTextField = amountField.amountTextField.copy(
error = amountError ?: TextReference.EMPTY,
isError = amountError != null,
),
) ?: swapAmountField.amountField,
) ?: prevState.primaryAmount
}
} else {
prevState.primaryAmount
},
secondaryAmount = if (
prevState.selectedAmountType == SwapAmountType.From && prevState.swapDirection == SwapDirection.Direct
) {
val amountField = prevState.secondaryAmount as? SwapAmountFieldUM.Content
val toAmount = (bestQuote as? SwapQuoteUM.Content)?.quoteAmount
val priceImpact = calculatePriceImpact(
swapDirection = prevState.swapDirection,
fromTokenAmount = fromAmountValue,
toTokenAmount = toAmount.orZero(),
primaryCryptoCurrencyStatus = primaryCryptoCurrencyStatus,
secondaryCryptoCurrencyStatus = secondaryCryptoCurrencyStatus,
)
amountField?.copy(
priceImpact = priceImpact,
amountField = AmountFieldChangeTransformer(
cryptoCurrencyStatus = secondaryCryptoCurrencyStatus,
maxEnterAmount = secondaryMaximumAmountBoundary,
minimumTransactionAmount = secondaryMinimumAmountBoundary,
value = toAmount?.parseBigDecimal(secondaryCryptoCurrencyStatus.currency.decimals).orEmpty(),
).transform(amountField.amountField),
) ?: prevState.secondaryAmount
} else {
prevState.secondaryAmount
},
)
}
private fun ExpressError?.getAmountError(): TextReference? = when (this) {
is ExpressError.AmountError.TooSmallError -> resourceReference(
R.string.express_provider_min_amount,
wrappedList(amount.format { crypto(cryptoCurrency = primaryCryptoCurrencyStatus.currency) }),
)
is ExpressError.AmountError.TooBigError -> resourceReference(
R.string.express_provider_max_amount,
wrappedList(amount.format { crypto(cryptoCurrency = primaryCryptoCurrencyStatus.currency) }),
)
else -> null
}
private fun getQuotesWithDiff(sortedQuotes: List<SwapQuoteUM>, bestQuote: SwapQuoteUM): ImmutableList<SwapQuoteUM> {
return sortedQuotes.sortedWith(SwapQuotesComparator)
.map { quote ->
if (quote is SwapQuoteUM.Content && bestQuote is SwapQuoteUM.Content) {
if (quote.provider.providerId == bestQuote.provider.providerId) {
quote.copy(diffPercent = DifferencePercent.Best)
} else {
// current / selected - 1
val percent = quote.quoteAmount / bestQuote.quoteAmount - BigDecimal.ONE
quote.copy(
diffPercent = DifferencePercent.Diff(
percent = stringReference(
if (percent.isPositive()) {
"${StringsSigns.PLUS}${percent.format { percent() }}"
} else {
"${StringsSigns.DASH_SIGN}${percent.format { percent() }}"
},
),
),
)
}
} else {
quote
}
}.toPersistentList()
}
private fun findBestQuote(quotes: List<SwapQuoteUM>): SwapQuoteUM? {
return quotes
.sortedWith(SwapQuotesComparator)
.firstOrNull()
}
private object SwapQuotesComparator : Comparator<SwapQuoteUM> {
override fun compare(p0: SwapQuoteUM?, p1: SwapQuoteUM?): Int {
return when {
p0 is SwapQuoteUM.Content && p1 !is SwapQuoteUM.Content -> -1
p0 !is SwapQuoteUM.Content && p1 is SwapQuoteUM.Content -> 1
p0 is SwapQuoteUM.Error && p1 is SwapQuoteUM.Error -> compareErrorQuote(p0 = p0, p1 = p1)
p0 is SwapQuoteUM.Content && p1 is SwapQuoteUM.Content -> p1.quoteAmount.compareTo(p0.quoteAmount)
else -> 0
}
}
private fun compareErrorQuote(p0: SwapQuoteUM.Error, p1: SwapQuoteUM.Error): Int = when {
p0.expressError is ExpressError.AmountError && p1.expressError !is ExpressError.AmountError -> -1
p0.expressError !is ExpressError.AmountError && p1.expressError is ExpressError.AmountError -> 1
p0.expressError is ExpressError.AmountError && p1.expressError is ExpressError.AmountError -> {
p0.expressError.amount.compareTo(p1.expressError.amount)
}
else -> 0
}
}
}

View file

@ -0,0 +1,47 @@
package com.tangem.features.swap.v2.impl.amount.model.transformers
import com.tangem.common.ui.amountScreen.converters.field.AmountFieldChangeTransformer
import com.tangem.common.ui.amountScreen.models.EnterAmountBoundary
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
import com.tangem.features.swap.v2.impl.amount.entity.SwapAmountUM
import com.tangem.features.swap.v2.impl.amount.model.SwapAmountQuoteUtils.updateAmount
import com.tangem.utils.transformer.Transformer
@Suppress("LongParameterList")
internal class SwapAmountValueChangeTransformer(
private val primaryCryptoCurrencyStatus: CryptoCurrencyStatus,
private val secondaryCryptoCurrencyStatus: CryptoCurrencyStatus,
private val primaryMaximumAmountBoundary: EnterAmountBoundary,
private val secondaryMaximumAmountBoundary: EnterAmountBoundary,
private val primaryMinimumAmountBoundary: EnterAmountBoundary,
private val secondaryMinimumAmountBoundary: EnterAmountBoundary,
private val value: String,
) : Transformer<SwapAmountUM> {
override fun transform(prevState: SwapAmountUM): SwapAmountUM {
if (prevState !is SwapAmountUM.Content) return prevState
return prevState
.updateAmount(
onPrimaryAmount = {
copy(
amountField = AmountFieldChangeTransformer(
cryptoCurrencyStatus = primaryCryptoCurrencyStatus,
maxEnterAmount = primaryMaximumAmountBoundary,
minimumTransactionAmount = primaryMinimumAmountBoundary,
value = value,
).transform(prevState.primaryAmount.amountField),
)
},
onSecondaryAmount = {
copy(
amountField = AmountFieldChangeTransformer(
cryptoCurrencyStatus = secondaryCryptoCurrencyStatus,
maxEnterAmount = secondaryMaximumAmountBoundary,
minimumTransactionAmount = secondaryMinimumAmountBoundary,
value = value,
).transform(prevState.secondaryAmount.amountField),
)
},
)
}
}

View file

@ -0,0 +1,46 @@
package com.tangem.features.swap.v2.impl.amount.model.transformers
import com.tangem.common.ui.amountScreen.converters.field.AmountFieldSetMaxAmountTransformer
import com.tangem.common.ui.amountScreen.models.EnterAmountBoundary
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
import com.tangem.features.swap.v2.impl.amount.entity.SwapAmountUM
import com.tangem.features.swap.v2.impl.amount.model.SwapAmountQuoteUtils.updateAmount
import com.tangem.features.swap.v2.impl.common.entity.SwapQuoteUM
import com.tangem.utils.transformer.Transformer
internal class SwapAmountValueMaxTransformer(
private val primaryCryptoCurrencyStatus: CryptoCurrencyStatus,
private val secondaryCryptoCurrencyStatus: CryptoCurrencyStatus,
private val primaryMaximumAmountBoundary: EnterAmountBoundary,
private val secondaryMaximumAmountBoundary: EnterAmountBoundary,
private val primaryMinimumAmountBoundary: EnterAmountBoundary,
private val secondaryMinimumAmountBoundary: EnterAmountBoundary,
) : Transformer<SwapAmountUM> {
override fun transform(prevState: SwapAmountUM): SwapAmountUM {
if (prevState !is SwapAmountUM.Content) return prevState
return prevState
.copy(selectedQuote = SwapQuoteUM.Loading)
.updateAmount(
onPrimaryAmount = {
copy(
amountField = AmountFieldSetMaxAmountTransformer(
cryptoCurrencyStatus = primaryCryptoCurrencyStatus,
maxAmount = primaryMaximumAmountBoundary,
minAmount = primaryMinimumAmountBoundary,
).transform(prevState.primaryAmount.amountField),
)
},
onSecondaryAmount = {
copy(
amountField = AmountFieldSetMaxAmountTransformer(
cryptoCurrencyStatus = secondaryCryptoCurrencyStatus,
maxAmount = secondaryMaximumAmountBoundary,
minAmount = secondaryMinimumAmountBoundary,
).transform(prevState.secondaryAmount.amountField),
)
},
)
}
}

View file

@ -0,0 +1,19 @@
package com.tangem.features.swap.v2.impl.amount.model.transformers
import com.tangem.features.swap.v2.impl.amount.entity.SwapAmountUM
import com.tangem.features.swap.v2.impl.common.entity.SwapQuoteUM
import com.tangem.utils.transformer.Transformer
internal object SwapQuoteLoadingStateTransformer : Transformer<SwapAmountUM> {
override fun transform(prevState: SwapAmountUM): SwapAmountUM {
if (prevState !is SwapAmountUM.Content) return prevState
return prevState.copy(
selectedQuote = if (prevState.selectedQuote is SwapQuoteUM.Empty) {
SwapQuoteUM.Loading
} else {
prevState.selectedQuote
},
)
}
}

View file

@ -0,0 +1,441 @@
package com.tangem.features.swap.v2.impl.amount.ui
import android.content.res.Configuration
import androidx.compose.animation.AnimatedContent
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.*
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.graphics.vector.rememberVectorPainter
import androidx.compose.ui.res.vectorResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
import androidx.compose.ui.unit.dp
import androidx.constraintlayout.compose.ConstraintLayout
import com.tangem.common.ui.amountScreen.models.AmountState
import com.tangem.common.ui.amountScreen.ui.AmountFieldV2
import com.tangem.core.ui.components.RectangleShimmer
import com.tangem.core.ui.components.SecondaryButton
import com.tangem.core.ui.components.SpacerWMax
import com.tangem.core.ui.components.TextShimmer
import com.tangem.core.ui.components.atoms.text.EllipsisText
import com.tangem.core.ui.components.buttons.common.TangemButtonSize
import com.tangem.core.ui.components.currency.icon.CurrencyIcon
import com.tangem.core.ui.components.currency.icon.CurrencyIconState
import com.tangem.core.ui.extensions.conditionalCompose
import com.tangem.core.ui.extensions.resolveReference
import com.tangem.core.ui.extensions.stringResourceSafe
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreview
import com.tangem.domain.express.models.ExpressRateType
import com.tangem.domain.swap.models.SwapDirection
import com.tangem.features.swap.v2.impl.R
import com.tangem.features.swap.v2.impl.amount.entity.SwapAmountFieldUM
import com.tangem.features.swap.v2.impl.amount.entity.SwapAmountType
import com.tangem.features.swap.v2.impl.amount.entity.SwapAmountUM
import com.tangem.features.swap.v2.impl.amount.model.SwapAmountClickIntents
import com.tangem.features.swap.v2.impl.amount.ui.preview.SwapAmountClickIntentsStub
import com.tangem.features.swap.v2.impl.amount.ui.preview.SwapAmountContentPreview
import com.tangem.features.swap.v2.impl.common.entity.SwapQuoteUM
@Composable
internal fun SwapAmountContent(
amountUM: SwapAmountUM,
clickIntents: SwapAmountClickIntents,
modifier: Modifier = Modifier,
) {
ConstraintLayout(
modifier = modifier,
) {
val (amountFromRef, amountToRef, middleButtonRef) = createRefs()
SwapAmountBlock(
amountUM = amountUM,
amountFieldUM = amountUM.primaryAmount,
clickIntents = clickIntents,
modifier = Modifier.constrainAs(amountFromRef) {
top.linkTo(parent.top)
start.linkTo(parent.start)
end.linkTo(parent.end)
},
)
SwapAmountBlock(
amountUM = amountUM,
amountFieldUM = amountUM.secondaryAmount,
clickIntents = clickIntents,
modifier = Modifier.constrainAs(amountToRef) {
top.linkTo(amountFromRef.bottom, 8.dp)
start.linkTo(parent.start)
end.linkTo(parent.end)
},
)
SwapAmountBlockSeparator(
modifier = Modifier
.constrainAs(middleButtonRef) {
top.linkTo(amountFromRef.bottom)
bottom.linkTo(amountToRef.top)
start.linkTo(parent.start)
end.linkTo(parent.end)
},
)
}
}
@Composable
private fun SwapAmountBlockSeparator(modifier: Modifier = Modifier) {
// todo add swap type like [Swap, SendViaSwap, SendIncognito]
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(8.dp),
modifier = modifier
.heightIn(max = 28.dp)
.clip(RoundedCornerShape(32.dp))
.background(TangemTheme.colors.background.secondary)
.padding(
vertical = 6.dp,
horizontal = 12.dp,
),
) {
Text(
text = "Convert",
style = TangemTheme.typography.caption1,
color = TangemTheme.colors.text.tertiary,
)
VerticalDivider(
thickness = 1.dp,
color = TangemTheme.colors.icon.inactive,
)
Icon(
painter = rememberVectorPainter(
ImageVector.vectorResource(R.drawable.ic_close_24),
),
tint = TangemTheme.colors.icon.informative,
contentDescription = null,
modifier = Modifier.size(16.dp),
)
}
}
@Composable
private fun SwapAmountBlock(
amountUM: SwapAmountUM,
amountFieldUM: SwapAmountFieldUM,
clickIntents: SwapAmountClickIntents,
modifier: Modifier = Modifier,
) {
Column(
modifier = modifier
.padding(horizontal = 16.dp)
.clip(RoundedCornerShape(16.dp))
.fillMaxWidth()
.background(TangemTheme.colors.background.action),
) {
AnimatedVisibility(amountUM.selectedAmountType == amountFieldUM.amountType) {
Box {
SwapAmountEditBlock(
amountFieldUM = amountFieldUM,
modifier = Modifier,
onValueChange = clickIntents::onAmountValueChange,
onValuePastedTriggerDismiss = clickIntents::onAmountPasteTriggerDismiss,
onCurrencyChange = clickIntents::onCurrencyChangeClick,
)
HorizontalDivider(
modifier = Modifier
.align(Alignment.BottomCenter)
.padding(horizontal = 16.dp),
thickness = 0.5.dp,
color = TangemTheme.colors.stroke.primary,
)
}
}
SwapAmountInfo(
amountUM = amountUM,
amountFieldUM = amountFieldUM,
onExpandEditField = clickIntents::onExpandEditField,
onSelectTokenClick = clickIntents::onSelectTokenClick,
onMaxAmountClick = clickIntents::onMaxValueClick,
)
}
}
@Composable
private fun SwapAmountEditBlock(
amountFieldUM: SwapAmountFieldUM,
onValueChange: (String) -> Unit,
onValuePastedTriggerDismiss: () -> Unit,
onCurrencyChange: (Boolean) -> Unit,
modifier: Modifier = Modifier,
) {
Column(
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.spacedBy(12.dp),
modifier = modifier
.padding(vertical = 48.dp),
) {
if (amountFieldUM.amountField !is AmountState.Data) {
TextShimmer(
style = TangemTheme.typography.caption2,
modifier = Modifier.width(60.dp),
)
} else {
Text(
text = (amountFieldUM.amountField as AmountState.Data).title.resolveReference(),
style = TangemTheme.typography.caption2,
color = TangemTheme.colors.text.tertiary,
)
}
AmountFieldV2(
amountUM = amountFieldUM.amountField,
onValueChange = onValueChange,
onValuePastedTriggerDismiss = onValuePastedTriggerDismiss,
onCurrencyChange = onCurrencyChange,
modifier = Modifier,
)
}
}
@Composable
private fun SwapAmountInfo(
amountUM: SwapAmountUM,
amountFieldUM: SwapAmountFieldUM,
onExpandEditField: (SwapAmountType) -> Unit,
onMaxAmountClick: () -> Unit,
onSelectTokenClick: () -> Unit,
modifier: Modifier = Modifier,
) {
val tokenIconState = (amountFieldUM.amountField as? AmountState.Data)?.tokenIconState ?: CurrencyIconState.Loading
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(12.dp),
modifier = modifier
.clickable(
interactionSource = remember { MutableInteractionSource() },
indication = ripple(),
enabled = (amountFieldUM as? SwapAmountFieldUM.Content)?.isClickEnabled == true,
onClick = {
if ((amountUM as? SwapAmountUM.Content)?.swapRateType == ExpressRateType.Fixed) {
onExpandEditField(amountFieldUM.amountType)
} else {
onSelectTokenClick()
}
},
),
) {
CurrencyIcon(
state = tokenIconState,
shouldDisplayNetwork = true,
modifier = Modifier.padding(
start = 16.dp,
top = 16.dp,
bottom = 16.dp,
),
)
SwapAmountInfoMain(amountFieldUM = amountFieldUM)
SpacerWMax()
AnimatedContent(
amountUM,
) { wrappedAmountUM ->
if (wrappedAmountUM is SwapAmountUM.Content) {
SwapAmountInfoExtra(
amountUM = wrappedAmountUM,
amountFieldUM = amountFieldUM,
onMaxAmountClick = onMaxAmountClick,
onSelectTokenClick = onSelectTokenClick,
)
} else {
RectangleShimmer()
}
}
}
}
@Composable
private fun SwapAmountInfoMain(amountFieldUM: SwapAmountFieldUM, modifier: Modifier = Modifier) {
AnimatedContent(
targetState = amountFieldUM !is SwapAmountFieldUM.Content,
modifier = modifier,
) { isContent ->
if (isContent) {
Column(
verticalArrangement = Arrangement.spacedBy(2.dp),
) {
TextShimmer(
style = TangemTheme.typography.subtitle2,
modifier = Modifier.width(56.dp),
)
TextShimmer(
style = TangemTheme.typography.caption2,
modifier = Modifier.width(72.dp),
)
}
} else {
val amountFieldUM = amountFieldUM as SwapAmountFieldUM.Content
Column(
verticalArrangement = Arrangement.spacedBy(2.dp),
) {
Text(
text = amountFieldUM.title.resolveReference(),
style = TangemTheme.typography.subtitle2,
color = TangemTheme.colors.text.primary1,
maxLines = 1,
)
EllipsisText(
text = amountFieldUM.subtitle.resolveReference(),
style = TangemTheme.typography.caption2,
color = TangemTheme.colors.text.tertiary,
ellipsis = amountFieldUM.subtitleEllipsis,
)
}
}
}
}
@Composable
private fun SwapAmountInfoExtra(
amountUM: SwapAmountUM.Content,
amountFieldUM: SwapAmountFieldUM,
onMaxAmountClick: () -> Unit,
onSelectTokenClick: () -> Unit,
) {
when (amountFieldUM.amountType) {
SwapAmountType.From -> when (amountUM.swapDirection) {
SwapDirection.Direct -> {
AnimatedVisibility(
visible = amountUM.selectedAmountType == amountFieldUM.amountType,
enter = fadeIn(),
exit = fadeOut(),
) {
AmountMaxButton(onMaxAmountClick)
}
}
SwapDirection.Reverse -> {
SwapAmountInfoQuote(
quoteUM = amountUM.selectedQuote,
swapRateType = amountUM.swapRateType,
onSelectTokenClick =
onSelectTokenClick,
)
}
}
SwapAmountType.To -> when (amountUM.swapDirection) {
SwapDirection.Direct -> {
SwapAmountInfoQuote(
quoteUM = amountUM.selectedQuote,
swapRateType = amountUM.swapRateType,
onSelectTokenClick = onSelectTokenClick,
)
}
SwapDirection.Reverse -> {
AnimatedVisibility(
visible = amountUM.selectedAmountType == amountFieldUM.amountType,
enter = fadeIn(),
exit = fadeOut(),
) {
AmountMaxButton(onMaxAmountClick)
}
}
}
}
}
@Composable
private fun AmountMaxButton(onMaxAmountClick: () -> Unit) {
SecondaryButton(
text = stringResourceSafe(R.string.send_max_amount),
size = TangemButtonSize.Small,
onClick = onMaxAmountClick,
modifier = Modifier.padding(end = 16.dp),
)
}
@Composable
private fun SwapAmountInfoQuote(quoteUM: SwapQuoteUM, swapRateType: ExpressRateType, onSelectTokenClick: () -> Unit) {
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier.conditionalCompose(
condition = swapRateType == ExpressRateType.Fixed,
modifier = {
clickable(
interactionSource = remember { MutableInteractionSource() },
indication = ripple(),
onClick = onSelectTokenClick,
)
},
),
) {
when (quoteUM) {
is SwapQuoteUM.Content -> EllipsisText(
text = quoteUM.quoteAmountValue.resolveReference(),
style = TangemTheme.typography.body2,
color = TangemTheme.colors.text.tertiary,
modifier = Modifier.padding(end = 2.dp),
)
is SwapQuoteUM.Error,
is SwapQuoteUM.Empty,
-> Box(modifier = Modifier.padding(start = 16.dp))
is SwapQuoteUM.Loading -> CircularProgressIndicator(
color = TangemTheme.colors.icon.inactive,
modifier = Modifier
.padding(end = 4.dp)
.size(20.dp),
)
is SwapQuoteUM.Allowance -> Text(
text = "ALLOWANCE NOT IMPLEMENTED",
)
}
Icon(
painter = rememberVectorPainter(
ImageVector.vectorResource(R.drawable.ic_chevron_24),
),
tint = TangemTheme.colors.icon.informative,
contentDescription = null,
modifier = Modifier
.padding(
end = 16.dp,
top = 24.dp,
bottom = 24.dp,
)
.size(24.dp),
)
}
}
// region Preview
@Composable
@Preview(showBackground = true, widthDp = 360)
@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES)
private fun SwapAmountContent_Preview(
@PreviewParameter(SwapAmountContentPreviewProvider::class) params: SwapAmountUM,
) {
TangemThemePreview {
SwapAmountContent(
amountUM = params,
modifier = Modifier,
clickIntents = SwapAmountClickIntentsStub,
)
}
}
private class SwapAmountContentPreviewProvider : PreviewParameterProvider<SwapAmountUM> {
override val values: Sequence<SwapAmountUM>
get() = sequenceOf(
SwapAmountContentPreview.emptyState,
SwapAmountContentPreview.defaultState,
)
}
// endregion

View file

@ -0,0 +1,20 @@
package com.tangem.features.swap.v2.impl.amount.ui.preview
import com.tangem.features.swap.v2.impl.amount.entity.SwapAmountType
import com.tangem.features.swap.v2.impl.amount.model.SwapAmountClickIntents
internal object SwapAmountClickIntentsStub : SwapAmountClickIntents {
override fun onExpandEditField(selectedAmountType: SwapAmountType) {}
override fun onSelectTokenClick() {}
override fun onAmountValueChange(value: String) {}
override fun onAmountPasteTriggerDismiss() {}
override fun onMaxValueClick() {}
override fun onCurrencyChangeClick(isFiat: Boolean) {}
override fun onAmountNext() {}
}

View file

@ -0,0 +1,73 @@
package com.tangem.features.swap.v2.impl.amount.ui.preview
import com.tangem.common.ui.amountScreen.preview.AmountStatePreviewData
import com.tangem.core.ui.components.atoms.text.TextEllipsis
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.stringReference
import com.tangem.domain.appcurrency.model.AppCurrency
import com.tangem.domain.express.models.ExpressRateType
import com.tangem.domain.swap.models.SwapCurrencies
import com.tangem.domain.swap.models.SwapDirection
import com.tangem.features.swap.v2.impl.amount.entity.SwapAmountFieldUM
import com.tangem.features.swap.v2.impl.amount.entity.SwapAmountType
import com.tangem.features.swap.v2.impl.amount.entity.SwapAmountUM
import com.tangem.features.swap.v2.impl.common.entity.SwapQuoteUM
import kotlinx.collections.immutable.persistentListOf
internal data object SwapAmountContentPreview {
val emptyState = SwapAmountUM.Content(
isPrimaryButtonEnabled = false,
primaryAmount = SwapAmountFieldUM.Empty(
amountType = SwapAmountType.From,
),
secondaryAmount = SwapAmountFieldUM.Empty(
amountType = SwapAmountType.To,
),
swapDirection = SwapDirection.Direct,
selectedAmountType = SwapAmountType.From,
swapCurrencies = SwapCurrencies.EMPTY,
swapQuotes = persistentListOf(),
selectedQuote = SwapQuoteUM.Empty,
primaryCryptoCurrencyStatus = null,
secondaryCryptoCurrencyStatus = null,
swapRateType = ExpressRateType.Float,
appCurrency = AppCurrency.Default,
)
val defaultState = SwapAmountUM.Content(
primaryAmount = SwapAmountFieldUM.Content(
amountType = SwapAmountType.From,
amountField = AmountStatePreviewData.amountState.copy(
availableBalance = stringReference("Balance: 100 BTC"),
),
title = stringReference("Tether"),
subtitle = stringReference("Balance: 100 BTC"),
priceImpact = null,
isClickEnabled = false,
subtitleEllipsis = TextEllipsis.OffsetEnd(3),
),
secondaryAmount = SwapAmountFieldUM.Content(
amountType = SwapAmountType.To,
amountField = AmountStatePreviewData.amountState.copy(
title = stringReference("Amount to receive"),
availableBalance = TextReference.EMPTY,
),
title = stringReference("Shiba Inu"),
priceImpact = stringReference("(-10%)"),
subtitle = TextReference.EMPTY,
isClickEnabled = false,
subtitleEllipsis = TextEllipsis.OffsetEnd(3),
),
appCurrency = AppCurrency.Default,
swapDirection = SwapDirection.Direct,
selectedAmountType = SwapAmountType.From,
swapCurrencies = SwapCurrencies.EMPTY,
swapQuotes = persistentListOf(),
selectedQuote = SwapQuoteUM.Empty,
primaryCryptoCurrencyStatus = null,
secondaryCryptoCurrencyStatus = null,
swapRateType = ExpressRateType.Float,
isPrimaryButtonEnabled = true,
)
}

View file

@ -21,9 +21,6 @@ internal class SwapChooseProviderModel @Inject constructor(
private val params: SwapChooseProviderComponent.Params = paramsContainer.require()
val uiState: StateFlow<SwapChooseProviderBottomSheetContent>
field: MutableStateFlow<SwapChooseProviderBottomSheetContent> = MutableStateFlow(getInitialState())
private val swapProviderListItemConverter by lazy(LazyThreadSafetyMode.NONE) {
SwapProviderListItemConverter(
cryptoCurrency = params.cryptoCurrency,
@ -31,6 +28,9 @@ internal class SwapChooseProviderModel @Inject constructor(
)
}
val uiState: StateFlow<SwapChooseProviderBottomSheetContent>
field: MutableStateFlow<SwapChooseProviderBottomSheetContent> = MutableStateFlow(getInitialState())
fun onProviderClick(quoteUM: SwapQuoteUM) {
params.callback.onProviderResult(quoteUM)
}

View file

@ -0,0 +1,9 @@
package com.tangem.features.swap.v2.impl.common
import com.tangem.features.swap.v2.impl.common.entity.NavigationUM
internal interface SwapNavigationModelCallback {
fun onNavigationResult(navigationUM: NavigationUM)
fun onBackClick()
fun onNextClick()
}

View file

@ -0,0 +1,22 @@
package com.tangem.features.swap.v2.impl.common.entity
import androidx.annotation.DrawableRes
import androidx.compose.runtime.Immutable
import com.tangem.common.ui.navigationButtons.NavigationButton
import com.tangem.core.ui.extensions.TextReference
@Immutable
internal sealed class NavigationUM {
data class Content(
val title: TextReference,
val subtitle: TextReference?,
@DrawableRes val backIconRes: Int,
val backIconClick: () -> Unit,
@DrawableRes val additionalIconRes: Int? = null,
val additionalIconClick: (() -> Unit)? = null,
val primaryButton: NavigationButton,
val secondaryPairButtonsUM: Pair<NavigationButton, NavigationButton>? = null,
) : NavigationUM()
data object Empty : NavigationUM()
}

View file

@ -0,0 +1,23 @@
package com.tangem.features.swap.v2.impl.swap
import com.tangem.core.decompose.navigation.Route
import kotlinx.serialization.Serializable
internal sealed class SwapRoute : Route {
abstract val isEditMode: Boolean
data object Empty : SwapRoute() {
override val isEditMode: Boolean = false
}
@Serializable
data object Confirm : SwapRoute() {
override val isEditMode: Boolean = true
}
@Serializable
data class Amount(
override val isEditMode: Boolean,
) : SwapRoute()
}