Updated on 2026-08-14
This commit is contained in:
parent
d376a7118d
commit
c7e4ff0598
27 changed files with 245 additions and 57 deletions
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
|
|
|
|||
|
|
@ -25,4 +25,7 @@ data class ExchangeQuoteResponse(
|
|||
@Json(name = "minAmount")
|
||||
val minAmount: BigDecimal,
|
||||
|
||||
@Json(name = "quoteId")
|
||||
val quoteId: String? = null,
|
||||
|
||||
)
|
||||
9
core/ui/src/main/res/drawable/ic_fixed_32.xml
Normal file
9
core/ui/src/main/res/drawable/ic_fixed_32.xml
Normal 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>
|
||||
9
core/ui/src/main/res/drawable/ic_floating_32.xml
Normal file
9
core/ui/src/main/res/drawable/ic_floating_32.xml
Normal 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>
|
||||
|
|
@ -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)) {
|
||||
|
|
|
|||
|
|
@ -16,4 +16,5 @@ data class SwapQuoteModel(
|
|||
val toTokenAmount: BigDecimal,
|
||||
val fromTokenAmount: BigDecimal?,
|
||||
val allowanceContract: String?,
|
||||
val quoteId: String? = null,
|
||||
)
|
||||
|
|
@ -91,6 +91,7 @@ interface SwapRepositoryV2 {
|
|||
expressProvider: ExpressProvider,
|
||||
rateType: ExpressRateType,
|
||||
expressOperationType: ExpressOperationType,
|
||||
quoteId: String?,
|
||||
): SwapDataModel
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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() {
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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?,
|
||||
|
|
|
|||
|
|
@ -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(),
|
||||
),
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
),
|
||||
),
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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() }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue