Updated on 2026-08-14

This commit is contained in:
Tangem 2026-03-24 16:36:05 +07:00
parent d376a7118d
commit c7e4ff0598
27 changed files with 245 additions and 57 deletions

View file

@ -73,7 +73,7 @@ class AmountFieldChangeTransformer(
fiatValue = fiatValue,
isError = isCheckFailed,
error = when {
isExceedBalance -> resourceReference(R.string.send_validation_amount_exceeds_balance)
isExceedBalance -> resourceReference(R.string.common_insufficient_balance)
isLessThanMinimumIfProvided -> {
val minimumAmount = minimumTransactionAmount.amount.format {
crypto(cryptoCurrencyStatus.currency)

View file

@ -70,6 +70,7 @@ interface TangemExpressApi {
@Query("refundExtraId") refundExtraId: String?, // for cex only
@Query("partnerOperationType") partnerOperationType: String?, // swap/ swap-and-send
@Query("toExtraId") toExtraId: String?, // swap-and-send memo
@Query("quoteId") quoteId: String?, // swap-and-send memo
): ApiResponse<ExchangeDataResponse>
@GET("exchange-status")

View file

@ -25,4 +25,7 @@ data class ExchangeQuoteResponse(
@Json(name = "minAmount")
val minAmount: BigDecimal,
@Json(name = "quoteId")
val quoteId: String? = null,
)

View file

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="32dp"
android:height="32dp"
android:viewportWidth="32"
android:viewportHeight="32">
<path
android:pathData="M9.348,29.329H22.648C23.551,29.329 24.222,29.09 24.66,28.612C25.107,28.143 25.33,27.406 25.33,26.402V15.869C25.33,14.874 25.107,14.143 24.66,13.674C24.222,13.196 23.551,12.956 22.648,12.956H9.628C8.734,12.956 8.016,13.196 7.476,13.674C6.936,14.152 6.666,14.884 6.666,15.869V26.402C6.666,27.406 6.89,28.143 7.337,28.612C7.784,29.09 8.454,29.329 9.348,29.329ZM8.887,13.918H11.137V9.053C11.137,7.848 11.36,6.839 11.807,6.025C12.254,5.203 12.845,4.586 13.581,4.174C14.326,3.753 15.132,3.543 15.998,3.543C16.874,3.543 17.679,3.753 18.415,4.174C19.151,4.586 19.742,5.203 20.189,6.025C20.636,6.839 20.86,7.848 20.86,9.053V13.918H23.109V9.355C23.109,8.006 22.913,6.829 22.522,5.825C22.131,4.82 21.6,3.988 20.93,3.328C20.259,2.658 19.495,2.161 18.638,1.835C17.791,1.501 16.911,1.333 15.998,1.333C15.085,1.333 14.205,1.501 13.358,1.835C12.51,2.161 11.751,2.658 11.081,3.328C10.41,3.988 9.874,4.82 9.474,5.825C9.083,6.829 8.887,8.006 8.887,9.355V13.918Z"
android:fillColor="#0099FF"/>
</vector>

View file

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="32dp"
android:height="32dp"
android:viewportWidth="32"
android:viewportHeight="32">
<path
android:pathData="M16,2C23.732,2 30,8.268 30,16C30,23.732 23.732,30 16,30C8.268,30 2,23.732 2,16C2,8.268 8.268,2 16,2ZM17.042,15.184C15.549,13.691 13.972,13.035 12.332,13.303C10.812,13.551 9.551,14.556 8.533,15.573C8.015,16.092 8.015,16.933 8.533,17.452C9.052,17.97 9.894,17.97 10.412,17.452C11.344,16.521 12.103,16.033 12.761,15.926C13.3,15.838 14.064,15.963 15.163,17.063C16.656,18.555 18.232,19.212 19.871,18.944C21.391,18.696 22.652,17.691 23.67,16.674C24.188,16.155 24.188,15.314 23.67,14.795C23.151,14.276 22.31,14.276 21.791,14.795C20.86,15.726 20.1,16.213 19.442,16.32C18.904,16.408 18.14,16.282 17.042,15.184Z"
android:fillColor="#0099FF"/>
</vector>

View file

@ -221,6 +221,7 @@ internal class DefaultSwapRepositoryV2 @Inject constructor(
toTokenAmount = toTokenAmount,
fromTokenAmount = fromTokenAmount,
allowanceContract = response.allowanceContract,
quoteId = response.quoteId,
)
}
@ -235,6 +236,7 @@ internal class DefaultSwapRepositoryV2 @Inject constructor(
expressProvider: ExpressProvider,
rateType: ExpressRateType,
expressOperationType: ExpressOperationType,
quoteId: String?,
): SwapDataModel = withContext(coroutineDispatcher.io) {
val requestId = UUID.randomUUID().toString()
val (fromCurrency, fromStatus) = fromCryptoCurrencyStatus
@ -273,6 +275,7 @@ internal class DefaultSwapRepositoryV2 @Inject constructor(
appPreferencesStore = appPreferencesStore,
),
toExtraId = toExtraId?.ifEmpty { null },
quoteId = quoteId,
).getOrThrow()
if (dataSignatureVerifier.verifySignature(response.signature, response.txDetailsJson)) {

View file

@ -16,4 +16,5 @@ data class SwapQuoteModel(
val toTokenAmount: BigDecimal,
val fromTokenAmount: BigDecimal?,
val allowanceContract: String?,
val quoteId: String? = null,
)

View file

@ -91,6 +91,7 @@ interface SwapRepositoryV2 {
expressProvider: ExpressProvider,
rateType: ExpressRateType,
expressOperationType: ExpressOperationType,
quoteId: String?,
): SwapDataModel
/**

View file

@ -30,6 +30,7 @@ class GetSwapDataUseCase(
expressProvider: ExpressProvider,
rateType: ExpressRateType,
expressOperationType: ExpressOperationType,
quoteId: String? = null,
): Either<ExpressError, SwapDataModel> = Either.catch {
swapRepositoryV2.getSwapData(
userWallet = userWallet,
@ -42,6 +43,7 @@ class GetSwapDataUseCase(
expressProvider = expressProvider,
rateType = rateType,
expressOperationType = expressOperationType,
quoteId = quoteId,
)
}.mapLeft { throwable ->
swapErrorResolver.resolve(throwable)

View file

@ -5,15 +5,23 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.arkivanov.decompose.ComponentContext
import com.arkivanov.decompose.extensions.compose.subscribeAsState
import com.arkivanov.decompose.router.slot.childSlot
import com.arkivanov.decompose.router.slot.dismiss
import com.arkivanov.essenty.lifecycle.subscribe
import com.tangem.common.ui.navigationButtons.NavigationModelCallback
import com.tangem.core.decompose.context.AppComponentContext
import com.tangem.core.decompose.context.childByContext
import com.tangem.core.decompose.model.getOrCreateModel
import com.tangem.core.ui.decompose.ComposableBottomSheetComponent
import com.tangem.core.ui.decompose.ComposableContentComponent
import com.tangem.core.ui.res.TangemTheme
import com.tangem.domain.express.models.ExpressRateType
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.sendviaswap.rateinfo.SwapRateInfoComponent
import dagger.assisted.Assisted
import dagger.assisted.AssistedInject
@ -24,6 +32,14 @@ internal class SwapAmountComponent @AssistedInject constructor(
private val model: SwapAmountModel = getOrCreateModel(params = params)
private val rateInfoSlot = childSlot(
source = model.rateInfoNavigation,
key = "rateInfoSlot",
serializer = null,
handleBackButton = true,
childFactory = ::rateInfoChild,
)
init {
lifecycle.subscribe(
onStart = model::onStart,
@ -36,12 +52,26 @@ internal class SwapAmountComponent @AssistedInject constructor(
@Composable
override fun Content(modifier: Modifier) {
val amountUM by model.uiState.collectAsStateWithLifecycle()
val rateInfo by rateInfoSlot.subscribeAsState()
SwapAmountContent(
amountUM = amountUM,
modifier = Modifier.background(TangemTheme.colors.background.tertiary),
clickIntents = model,
)
rateInfo.child?.instance?.BottomSheet()
}
private fun rateInfoChild(
config: ExpressRateType,
componentContext: ComponentContext,
): ComposableBottomSheetComponent {
return SwapRateInfoComponent(
appComponentContext = childByContext(componentContext),
expressRateType = config,
onDismiss = { model.rateInfoNavigation.dismiss() },
)
}
interface ModelCallback : NavigationModelCallback {

View file

@ -109,6 +109,7 @@ internal class SwapAmountModel @Inject constructor(
private var userCountry: UserCountry = UserCountry.Other(Locale.getDefault().country)
val bottomSheetNavigation: SlotNavigation<SwapChooseProviderConfig> = SlotNavigation()
val rateInfoNavigation: SlotNavigation<ExpressRateType> = SlotNavigation()
private var isShowBestRateAnimation: Boolean = false
@ -318,7 +319,8 @@ internal class SwapAmountModel @Inject constructor(
}
override fun onRateClick() {
// TODO [REDACTED_TASK_KEY]
val content = uiState.value as? SwapAmountUM.Content ?: return
rateInfoNavigation.activate(content.swapRateType)
}
override fun onSeparatorClick() {

View file

@ -64,6 +64,7 @@ internal class SwapQuoteUMConverter(
fromAmountValue = fromAmountValue,
rate = annotatedReference(rateString),
isSingleProvider = false,
quoteId = quote.quoteId,
)
}
} else {
@ -78,6 +79,7 @@ internal class SwapQuoteUMConverter(
fromAmountValue = fromAmountValue,
rate = annotatedReference(rateString),
isSingleProvider = false,
quoteId = quote.quoteId,
)
}
}

View file

@ -5,17 +5,23 @@ import com.tangem.core.ui.extensions.resourceReference
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.SwapAmountUM
import com.tangem.features.swap.v2.impl.amount.model.converter.SwapAmountErrorConverter
import com.tangem.features.swap.v2.impl.common.entity.SwapQuoteUM
import com.tangem.domain.swap.models.SwapAmountType
import com.tangem.utils.transformer.Transformer
/**
* Sets a generic "Something went wrong" error on the selected amount field
* when all quotes returned errors.
* when all quotes returned errors that cannot be converted to a specific amount error.
* Returns [prevState] unchanged when the condition is not met.
*/
internal object SwapAmountErrorQuoteTransformer : Transformer<SwapAmountUM> {
internal class SwapAmountErrorQuoteTransformer(
private val quotes: List<SwapQuoteUM>,
) : Transformer<SwapAmountUM> {
override fun transform(prevState: SwapAmountUM): SwapAmountUM {
if (prevState !is SwapAmountUM.Content) return prevState
if (!areAllQuotesUnrecoverableErrors(prevState)) return prevState
val error = resourceReference(R.string.send_with_swap_something_went_wrong)
@ -40,6 +46,18 @@ internal object SwapAmountErrorQuoteTransformer : Transformer<SwapAmountUM> {
)
}
private fun areAllQuotesUnrecoverableErrors(state: SwapAmountUM.Content): Boolean {
if (!quotes.all { it is SwapQuoteUM.Error }) return false
val secondaryProviderErrorConverter = state.secondaryCryptoCurrencyStatus?.let {
SwapAmountErrorConverter(cryptoCurrency = it.currency)
}
return quotes.all { quote ->
(quote as? SwapQuoteUM.Error)?.expressError
?.let { secondaryProviderErrorConverter?.convert(it) } == null
}
}
private fun applyError(
field: SwapAmountFieldUM,
error: com.tangem.core.ui.extensions.TextReference,

View file

@ -140,7 +140,7 @@ internal class SwapAmountSelectQuoteTransformer(
val insufficientFundsError = if (isSecondarySelected && fromAmount != null) {
val primaryBalance = prevState.primaryCryptoCurrencyStatus.value.amount
if (primaryBalance != null && fromAmount > primaryBalance) {
resourceReference(R.string.swapping_insufficient_funds)
resourceReference(R.string.common_insufficient_balance)
} else {
null
}

View file

@ -28,36 +28,30 @@ internal class SwapAmountSetQuotesTransformer(
private val primaryMaximumAmountBoundary: EnterAmountBoundary? = null,
private val primaryMinimumAmountBoundary: EnterAmountBoundary? = null,
) : Transformer<SwapAmountUM> {
override fun transform(prevState: SwapAmountUM): SwapAmountUM {
if (prevState !is SwapAmountUM.Content) return prevState
val selectedAmountType = prevState.selectedAmountType
val comparator = SwapQuotesComparator(selectedAmountType)
val isSingleProvider = quotes.filter { swapQuoteUM ->
swapQuoteUM is SwapQuoteUM.Content || swapQuoteUM is SwapQuoteUM.Allowance ||
(swapQuoteUM as? SwapQuoteUM.Error)?.expressError is ExpressError.AmountError
}.isSingleItem()
val isSingleProvider = isSingleProvider(quotes)
val sortedQuotes = quotes.sortedWith(comparator)
val bestQuote = findBestQuote(quotes, comparator) ?: SwapQuoteUM.Empty
val quotesWithDiff = getQuotesWithDiff(
sortedQuotes = sortedQuotes,
bestQuote = bestQuote,
isSingleProvider = isSingleProvider,
selectedAmountType = selectedAmountType,
)
val selectedQuote = if (isSilentReload && prevState.selectedQuote !is SwapQuoteUM.Loading) {
quotesWithDiff.firstOrNull { it.provider?.providerId == prevState.selectedQuote.provider?.providerId }
?: prevState.selectedQuote
} else {
(bestQuote as? SwapQuoteUM.Content)?.copy(
diffPercent = DifferencePercent.Best,
isSingleProvider = isSingleProvider,
) ?: bestQuote
}
val selectedQuote = resolveSelectedQuote(
prevState = prevState,
quotesWithDiff = quotesWithDiff,
bestQuote = bestQuote,
isSingleProvider = isSingleProvider,
)
val selectQuoteTransformer = SwapAmountSelectQuoteTransformer(
val updatedState = SwapAmountSelectQuoteTransformer(
quoteUM = selectedQuote,
secondaryMaximumAmountBoundary = secondaryMaximumAmountBoundary,
secondaryMinimumAmountBoundary = secondaryMinimumAmountBoundary,
@ -66,30 +60,42 @@ internal class SwapAmountSetQuotesTransformer(
isBalanceHidden = isBalanceHidden,
primaryMaximumAmountBoundary = primaryMaximumAmountBoundary,
primaryMinimumAmountBoundary = primaryMinimumAmountBoundary,
)
val updatedState = selectQuoteTransformer.transform(prevState = prevState)
).transform(prevState = prevState)
if (updatedState !is SwapAmountUM.Content) return prevState
val areAllQuotesErrors = quotes.all { it is SwapQuoteUM.Error }
val stateWithError = if (areAllQuotesErrors) {
SwapAmountErrorQuoteTransformer.transform(updatedState)
} else {
updatedState
}
val stateWithError = SwapAmountErrorQuoteTransformer(quotes).transform(updatedState)
if (stateWithError !is SwapAmountUM.Content) return prevState
return stateWithError.copy(
isPrimaryButtonEnabled = stateWithError.isPrimaryButtonEnabled && quotesWithDiff.isNotEmpty(),
swapQuotes = getQuotesWithDiff(
sortedQuotes = sortedQuotes,
bestQuote = bestQuote,
isSingleProvider = isSingleProvider,
selectedAmountType = selectedAmountType,
),
swapQuotes = quotesWithDiff,
)
}
private fun resolveSelectedQuote(
prevState: SwapAmountUM.Content,
quotesWithDiff: ImmutableList<SwapQuoteUM>,
bestQuote: SwapQuoteUM,
isSingleProvider: Boolean,
): SwapQuoteUM {
if (isSilentReload && prevState.selectedQuote !is SwapQuoteUM.Loading) {
return quotesWithDiff
.firstOrNull { it.provider?.providerId == prevState.selectedQuote.provider?.providerId }
?: prevState.selectedQuote
}
return (bestQuote as? SwapQuoteUM.Content)?.copy(
diffPercent = DifferencePercent.Best,
isSingleProvider = isSingleProvider,
) ?: bestQuote
}
private fun isSingleProvider(quotes: List<SwapQuoteUM>): Boolean {
return quotes.filter { swapQuoteUM ->
swapQuoteUM is SwapQuoteUM.Content || swapQuoteUM is SwapQuoteUM.Allowance ||
(swapQuoteUM as? SwapQuoteUM.Error)?.expressError is ExpressError.AmountError
}.isSingleItem()
}
private fun getQuotesWithDiff(
sortedQuotes: List<SwapQuoteUM>,
bestQuote: SwapQuoteUM,

View file

@ -10,7 +10,8 @@ import com.tangem.features.swap.v2.impl.common.entity.SwapQuoteUM
import java.math.BigDecimal
internal data class ConfirmData(
val enteredAmount: BigDecimal?,
val enteredFromAmount: BigDecimal?,
val enteredToAmount: BigDecimal?,
val reduceAmountBy: BigDecimal,
val isIgnoreReduce: Boolean,
val enteredDestination: String?,

View file

@ -79,7 +79,7 @@ internal class SwapAlertFactory @Inject constructor(
derivationPath = cryptoCurrency?.network?.derivationPath?.value.orEmpty(),
destinationAddress = confirmData?.enteredDestination.orEmpty(),
tokenSymbol = confirmData?.toCryptoCurrencyStatus?.currency?.symbol.orEmpty(),
amount = confirmData?.enteredAmount?.toString().orEmpty(),
amount = confirmData?.enteredFromAmount?.toString().orEmpty(),
fee = confirmData?.fee?.amount?.value?.toString()
.orEmpty(),
),

View file

@ -38,6 +38,7 @@ internal sealed class SwapQuoteUM {
val diffPercent: DifferencePercent,
val isSingleProvider: Boolean,
val rate: TextReference,
val quoteId: String? = null,
) : SwapQuoteUM() {
sealed class DifferencePercent {
data object Empty : DifferencePercent()

View file

@ -9,6 +9,7 @@ import com.tangem.domain.express.models.ExpressError
import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.domain.models.currency.CryptoCurrencyStatus
import com.tangem.domain.models.wallet.UserWalletId
import java.math.BigDecimal
import com.tangem.features.swap.v2.impl.notifications.model.SwapNotificationsModel
import com.tangem.features.swap.v2.impl.notifications.ui.swapNotifications
import kotlinx.collections.immutable.ImmutableList
@ -46,6 +47,8 @@ internal class SwapNotificationsComponent(
val memo: String? = null,
val toCryptoCurrencyStatus: CryptoCurrencyStatus? = null,
val userWalletId: UserWalletId? = null,
val enteredFromAmount: BigDecimal? = null,
val fromCryptoCurrencyStatus: CryptoCurrencyStatus? = null,
)
}
}

View file

@ -51,6 +51,11 @@ internal object SwapNotificationUM {
subtitle = resourceReference(R.string.warning_express_providers_fca_warning_description),
iconResId = R.drawable.ic_alert_circle_24,
)
data object InsufficientFunds : Error(
title = resourceReference(R.string.swapping_insufficient_funds),
subtitle = resourceReference(R.string.swapping_insufficient_funds_description),
)
}
sealed class Warning(

View file

@ -65,6 +65,7 @@ internal class SwapNotificationsModel @Inject constructor(
private suspend fun buildNotifications() {
val notifications = buildList {
addInsufficientFundsNotification()
addExpressErrorNotification()
addDestinationTagRequiredNotification()
}
@ -93,6 +94,14 @@ internal class SwapNotificationsModel @Inject constructor(
}
}
private fun MutableList<NotificationUM>.addInsufficientFundsNotification() {
val enteredFromAmount = notificationData.enteredFromAmount ?: return
val balance = notificationData.fromCryptoCurrencyStatus?.value?.amount ?: return
if (enteredFromAmount > balance) {
add(SwapNotificationUM.Error.InsufficientFunds)
}
}
fun MutableList<NotificationUM>.addExpressErrorNotification() {
val expressError = notificationData.expressError ?: return
val fromCryptoCurrency = notificationData.fromCryptoCurrency ?: return

View file

@ -117,7 +117,7 @@ internal class SendWithSwapConfirmComponent @AssistedInject constructor(
is CryptoCurrency.Coin -> "0"
},
memo = null,
amountValue = model.confirmData.enteredAmount.orZero(),
amountValue = model.confirmData.enteredFromAmount.orZero(),
reduceAmountBy = model.confirmData.reduceAmountBy.orZero(),
isIgnoreReduce = model.confirmData.isIgnoreReduce,
fee = model.confirmData.fee,
@ -137,6 +137,8 @@ internal class SendWithSwapConfirmComponent @AssistedInject constructor(
memo = model.confirmData.enteredMemo,
toCryptoCurrencyStatus = model.confirmData.toCryptoCurrencyStatus,
userWalletId = params.userWallet.walletId,
enteredFromAmount = model.confirmData.enteredFromAmount,
fromCryptoCurrencyStatus = model.confirmData.fromCryptoCurrencyStatus,
),
),
)

View file

@ -138,12 +138,18 @@ internal class SendWithSwapConfirmModel @Inject constructor(
onDirect = { amountUM.primaryAmount },
onReverse = { amountUM.secondaryAmount },
)
val amountState = fromAmount?.amountField as? AmountState.Data
val toAmount = amountUM?.swapDirection?.withSwapDirection(
onDirect = { amountUM.secondaryAmount },
onReverse = { amountUM.primaryAmount },
)
val amountStateFrom = fromAmount?.amountField as? AmountState.Data
val amountStateTo = toAmount?.amountField as? AmountState.Data
val isQuoteContent = amountUM?.selectedQuote is SwapQuoteUM.Content
return ConfirmData(
enteredAmount = amountState?.amountTextField?.cryptoAmount?.value,
reduceAmountBy = amountState?.reduceAmountBy.takeIf { isQuoteContent }.orZero(),
isIgnoreReduce = amountState?.isIgnoreReduce == true,
enteredToAmount = amountStateTo?.amountTextField?.cryptoAmount?.value,
enteredFromAmount = amountStateFrom?.amountTextField?.cryptoAmount?.value,
reduceAmountBy = amountStateFrom?.reduceAmountBy.takeIf { isQuoteContent }.orZero(),
isIgnoreReduce = amountStateFrom?.isIgnoreReduce == true,
enteredDestination = destinationUM?.addressTextField?.actualAddress,
enteredMemo = destinationUM?.memoTextField?.value,
fee = feeSelectorUM?.selectedFeeItem?.fee.takeIf { isQuoteContent },
@ -250,7 +256,7 @@ internal class SendWithSwapConfirmModel @Inject constructor(
val defaultError = GetFeeError.UnknownError.left()
val provider = (confirmData.quote as? SwapQuoteUM.Content)?.provider ?: return defaultError
val amountValue = confirmData.enteredAmount ?: return defaultError
val amountValue = confirmData.enteredFromAmount ?: return defaultError
return when (val providerType = provider.type) {
ExpressProviderType.CEX -> {
@ -274,7 +280,7 @@ internal class SendWithSwapConfirmModel @Inject constructor(
suspend fun loadFeeExtended(maybeToken: CryptoCurrencyStatus?): Either<GetFeeError, TransactionFeeExtended> {
val defaultError = GetFeeError.UnknownError.left()
val provider = (confirmData.quote as? SwapQuoteUM.Content)?.provider ?: return defaultError
val amountValue = confirmData.enteredAmount ?: return defaultError
val amountValue = confirmData.enteredFromAmount ?: return defaultError
return when (val providerType = provider.type) {
ExpressProviderType.CEX -> {
@ -420,7 +426,7 @@ internal class SendWithSwapConfirmModel @Inject constructor(
is CryptoCurrency.Coin -> "0"
},
memo = null,
amountValue = confirmData.enteredAmount.orZero(),
amountValue = confirmData.enteredFromAmount.orZero(),
reduceAmountBy = confirmData.reduceAmountBy,
isIgnoreReduce = confirmData.isIgnoreReduce,
fee = confirmData.fee,
@ -436,6 +442,8 @@ internal class SendWithSwapConfirmModel @Inject constructor(
memo = confirmData.enteredMemo,
toCryptoCurrencyStatus = confirmData.toCryptoCurrencyStatus,
userWalletId = params.userWallet.walletId,
enteredFromAmount = confirmData.enteredFromAmount,
fromCryptoCurrencyStatus = confirmData.fromCryptoCurrencyStatus,
),
)
uiState.transformerUpdate(

View file

@ -25,6 +25,7 @@ import com.tangem.domain.transaction.usecase.gasless.CreateAndSendGaslessTransac
import com.tangem.domain.utils.convertToSdkAmount
import com.tangem.features.send.v2.api.subcomponents.feeSelector.utils.FeeCalculationUtils
import com.tangem.features.swap.v2.impl.common.ConfirmData
import com.tangem.features.swap.v2.impl.common.entity.SwapQuoteUM
import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
@ -87,33 +88,43 @@ internal class SwapTransactionSender @AssistedInject constructor(
val fromAccount = confirmData.fromAccount
val provider = confirmData.quote?.provider ?: return
val rateType = confirmData.rateType ?: return
val amountValue = confirmData.enteredAmount ?: return
val feeValue = confirmData.fee?.amount?.value ?: return
val destination = confirmData.enteredDestination ?: return
val fromAmount = FeeCalculationUtils.checkAndCalculateSubtractedAmount(
isAmountSubtractAvailable = isAmountSubtractAvailable,
cryptoCurrencyStatus = fromStatus,
amountValue = amountValue,
feeValue = feeValue,
reduceAmountBy = confirmData.reduceAmountBy,
)
val (amount, currencyStatus) = when (confirmData.amountType) {
SwapAmountType.From -> {
val amountValue = confirmData.enteredFromAmount ?: return
val subtracted = FeeCalculationUtils.checkAndCalculateSubtractedAmount(
isAmountSubtractAvailable = isAmountSubtractAvailable,
cryptoCurrencyStatus = fromStatus,
amountValue = amountValue,
feeValue = feeValue,
reduceAmountBy = confirmData.reduceAmountBy,
)
subtracted to fromStatus
}
SwapAmountType.To -> {
val amountValue = confirmData.enteredToAmount ?: return
amountValue to toStatus
}
}
val swapData = getSwapDataUseCase(
userWallet = userWallet,
fromCryptoCurrencyStatus = fromStatus,
amount = fromAmount.toStringWithRightOffset(fromStatus.currency.decimals),
amountType = SwapAmountType.From,
amount = amount.toStringWithRightOffset(currencyStatus.currency.decimals),
amountType = confirmData.amountType,
toCryptoCurrency = toStatus.currency,
toAddress = destination,
toExtraId = confirmData.enteredMemo,
expressProvider = provider,
rateType = rateType,
expressOperationType = expressOperationType,
quoteId = (confirmData.quote as? SwapQuoteUM.Content)?.quoteId,
).getOrElse { error -> onExpressError(error); return }
createAndSendCexTransaction(
fromAmount = fromAmount,
fromAmount = amount,
fromStatus = fromStatus,
fromAccount = fromAccount,
toStatus = toStatus,

View file

@ -0,0 +1,33 @@
package com.tangem.features.swap.v2.impl.sendviaswap.rateinfo
import com.tangem.core.ui.components.bottomsheets.message.*
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.domain.express.models.ExpressRateType
import com.tangem.features.swap.v2.impl.R
internal object SendWithSwapRateInfoFactory {
fun getRateTypeMessage(expressRateType: ExpressRateType, onDismiss: () -> Unit): MessageBottomSheetUM {
val isFixed = expressRateType == ExpressRateType.Fixed
return messageBottomSheetUM {
onDismiss(onDismiss)
infoBlock {
iconImage(if (isFixed) R.drawable.ic_fixed_32 else R.drawable.ic_floating_32)
title = resourceReference(
if (isFixed) R.string.send_rate_fixed_info_title else R.string.send_rate_floating_info_title,
)
body = resourceReference(
if (isFixed) {
R.string.send_rate_fixed_info_description
} else {
R.string.send_rate_floating_info_description
},
)
}
secondaryButton {
text = resourceReference(R.string.common_got_it)
onClick { closeBs() }
}
}
}
}

View file

@ -0,0 +1,27 @@
package com.tangem.features.swap.v2.impl.sendviaswap.rateinfo
import androidx.compose.runtime.Composable
import com.tangem.core.decompose.context.AppComponentContext
import com.tangem.core.ui.components.bottomsheets.message.MessageBottomSheet
import com.tangem.core.ui.components.bottomsheets.message.MessageBottomSheetUM
import com.tangem.core.ui.decompose.ComposableBottomSheetComponent
import com.tangem.domain.express.models.ExpressRateType
internal class SwapRateInfoComponent(
appComponentContext: AppComponentContext,
expressRateType: ExpressRateType,
private val onDismiss: () -> Unit,
) : AppComponentContext by appComponentContext, ComposableBottomSheetComponent {
private val state: MessageBottomSheetUM = SendWithSwapRateInfoFactory.getRateTypeMessage(
expressRateType = expressRateType,
onDismiss = onDismiss,
)
override fun dismiss() = onDismiss()
@Composable
override fun BottomSheet() {
MessageBottomSheet(state = state, onDismissRequest = ::dismiss)
}
}

View file

@ -344,6 +344,7 @@ internal class DefaultSwapRepository(
appPreferencesStore = appPreferencesStore,
),
toExtraId = toExtraId?.ifEmpty { null },
quoteId = null,
).getOrThrow()
if (dataSignatureVerifier.verifySignature(response.signature, response.txDetailsJson)) {
val txDetails = parseTxDetails(response.txDetailsJson)