diff --git a/common/ui/src/main/java/com/tangem/common/ui/amountScreen/converters/field/AmountFieldChangeTransformer.kt b/common/ui/src/main/java/com/tangem/common/ui/amountScreen/converters/field/AmountFieldChangeTransformer.kt index 4187ce21d2..c4a063c28b 100644 --- a/common/ui/src/main/java/com/tangem/common/ui/amountScreen/converters/field/AmountFieldChangeTransformer.kt +++ b/common/ui/src/main/java/com/tangem/common/ui/amountScreen/converters/field/AmountFieldChangeTransformer.kt @@ -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) diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/express/TangemExpressApi.kt b/core/datasource/src/main/java/com/tangem/datasource/api/express/TangemExpressApi.kt index b64ceb1ec2..4d63fd4b3a 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/express/TangemExpressApi.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/express/TangemExpressApi.kt @@ -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 @GET("exchange-status") diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/express/models/response/ExchangeQuoteResponse.kt b/core/datasource/src/main/java/com/tangem/datasource/api/express/models/response/ExchangeQuoteResponse.kt index d08ebf45bc..4326d871bf 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/express/models/response/ExchangeQuoteResponse.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/express/models/response/ExchangeQuoteResponse.kt @@ -25,4 +25,7 @@ data class ExchangeQuoteResponse( @Json(name = "minAmount") val minAmount: BigDecimal, + @Json(name = "quoteId") + val quoteId: String? = null, + ) \ No newline at end of file diff --git a/core/ui/src/main/res/drawable/ic_fixed_32.xml b/core/ui/src/main/res/drawable/ic_fixed_32.xml new file mode 100644 index 0000000000..3197cc1649 --- /dev/null +++ b/core/ui/src/main/res/drawable/ic_fixed_32.xml @@ -0,0 +1,9 @@ + + + diff --git a/core/ui/src/main/res/drawable/ic_floating_32.xml b/core/ui/src/main/res/drawable/ic_floating_32.xml new file mode 100644 index 0000000000..c2ccfb5a40 --- /dev/null +++ b/core/ui/src/main/res/drawable/ic_floating_32.xml @@ -0,0 +1,9 @@ + + + diff --git a/data/swap/src/main/java/com/tangem/data/swap/DefaultSwapRepositoryV2.kt b/data/swap/src/main/java/com/tangem/data/swap/DefaultSwapRepositoryV2.kt index f5d7042b43..d51b43b1e1 100644 --- a/data/swap/src/main/java/com/tangem/data/swap/DefaultSwapRepositoryV2.kt +++ b/data/swap/src/main/java/com/tangem/data/swap/DefaultSwapRepositoryV2.kt @@ -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)) { diff --git a/domain/swap/models/src/main/java/com/tangem/domain/swap/models/SwapQuoteModel.kt b/domain/swap/models/src/main/java/com/tangem/domain/swap/models/SwapQuoteModel.kt index 45df45fc76..bd321cbf88 100644 --- a/domain/swap/models/src/main/java/com/tangem/domain/swap/models/SwapQuoteModel.kt +++ b/domain/swap/models/src/main/java/com/tangem/domain/swap/models/SwapQuoteModel.kt @@ -16,4 +16,5 @@ data class SwapQuoteModel( val toTokenAmount: BigDecimal, val fromTokenAmount: BigDecimal?, val allowanceContract: String?, + val quoteId: String? = null, ) \ No newline at end of file diff --git a/domain/swap/src/main/java/com/tangem/domain/swap/SwapRepositoryV2.kt b/domain/swap/src/main/java/com/tangem/domain/swap/SwapRepositoryV2.kt index b456087ac4..dacec2c058 100644 --- a/domain/swap/src/main/java/com/tangem/domain/swap/SwapRepositoryV2.kt +++ b/domain/swap/src/main/java/com/tangem/domain/swap/SwapRepositoryV2.kt @@ -91,6 +91,7 @@ interface SwapRepositoryV2 { expressProvider: ExpressProvider, rateType: ExpressRateType, expressOperationType: ExpressOperationType, + quoteId: String?, ): SwapDataModel /** diff --git a/domain/swap/src/main/java/com/tangem/domain/swap/usecase/GetSwapDataUseCase.kt b/domain/swap/src/main/java/com/tangem/domain/swap/usecase/GetSwapDataUseCase.kt index 448e5ddcd5..2726e77e94 100644 --- a/domain/swap/src/main/java/com/tangem/domain/swap/usecase/GetSwapDataUseCase.kt +++ b/domain/swap/src/main/java/com/tangem/domain/swap/usecase/GetSwapDataUseCase.kt @@ -30,6 +30,7 @@ class GetSwapDataUseCase( expressProvider: ExpressProvider, rateType: ExpressRateType, expressOperationType: ExpressOperationType, + quoteId: String? = null, ): Either = 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) diff --git a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/SwapAmountComponent.kt b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/SwapAmountComponent.kt index 035c016356..91b726bd17 100644 --- a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/SwapAmountComponent.kt +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/SwapAmountComponent.kt @@ -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 { diff --git a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/SwapAmountModel.kt b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/SwapAmountModel.kt index ac2fe177b4..98c238edb1 100644 --- a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/SwapAmountModel.kt +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/SwapAmountModel.kt @@ -109,6 +109,7 @@ internal class SwapAmountModel @Inject constructor( private var userCountry: UserCountry = UserCountry.Other(Locale.getDefault().country) val bottomSheetNavigation: SlotNavigation = SlotNavigation() + val rateInfoNavigation: SlotNavigation = 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() { diff --git a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/converter/SwapQuoteUMConverter.kt b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/converter/SwapQuoteUMConverter.kt index 3f5255f097..c521bbceb9 100644 --- a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/converter/SwapQuoteUMConverter.kt +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/converter/SwapQuoteUMConverter.kt @@ -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, ) } } diff --git a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/transformers/SwapAmountErrorQuoteTransformer.kt b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/transformers/SwapAmountErrorQuoteTransformer.kt index edb2d9e257..122e946132 100644 --- a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/transformers/SwapAmountErrorQuoteTransformer.kt +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/transformers/SwapAmountErrorQuoteTransformer.kt @@ -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 { +internal class SwapAmountErrorQuoteTransformer( + private val quotes: List, +) : Transformer { 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 { ) } + 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, diff --git a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/transformers/SwapAmountSelectQuoteTransformer.kt b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/transformers/SwapAmountSelectQuoteTransformer.kt index 8e1878c21c..627c8e5881 100644 --- a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/transformers/SwapAmountSelectQuoteTransformer.kt +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/transformers/SwapAmountSelectQuoteTransformer.kt @@ -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 } diff --git a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/transformers/SwapAmountSetQuotesTransformer.kt b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/transformers/SwapAmountSetQuotesTransformer.kt index 9349099eb2..9b84a536bf 100644 --- a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/transformers/SwapAmountSetQuotesTransformer.kt +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/transformers/SwapAmountSetQuotesTransformer.kt @@ -28,36 +28,30 @@ internal class SwapAmountSetQuotesTransformer( private val primaryMaximumAmountBoundary: EnterAmountBoundary? = null, private val primaryMinimumAmountBoundary: EnterAmountBoundary? = null, ) : Transformer { + 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, + 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): 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, bestQuote: SwapQuoteUM, diff --git a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/common/ConfirmData.kt b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/common/ConfirmData.kt index 7da47a57aa..f42349e6c2 100644 --- a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/common/ConfirmData.kt +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/common/ConfirmData.kt @@ -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?, diff --git a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/common/SwapAlertFactory.kt b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/common/SwapAlertFactory.kt index 0fb397effb..22ead70b9b 100644 --- a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/common/SwapAlertFactory.kt +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/common/SwapAlertFactory.kt @@ -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(), ), diff --git a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/common/entity/SwapQuoteUM.kt b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/common/entity/SwapQuoteUM.kt index b07b87a6ac..552956a5b9 100644 --- a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/common/entity/SwapQuoteUM.kt +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/common/entity/SwapQuoteUM.kt @@ -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() diff --git a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/notifications/SwapNotificationsComponent.kt b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/notifications/SwapNotificationsComponent.kt index 3634569cf9..db6c8d8dbe 100644 --- a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/notifications/SwapNotificationsComponent.kt +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/notifications/SwapNotificationsComponent.kt @@ -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, ) } } \ No newline at end of file diff --git a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/notifications/entity/SwapNotificationUM.kt b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/notifications/entity/SwapNotificationUM.kt index eadc69c82c..fc14191b9b 100644 --- a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/notifications/entity/SwapNotificationUM.kt +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/notifications/entity/SwapNotificationUM.kt @@ -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( diff --git a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/notifications/model/SwapNotificationsModel.kt b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/notifications/model/SwapNotificationsModel.kt index 89e743866c..73dc591264 100644 --- a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/notifications/model/SwapNotificationsModel.kt +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/notifications/model/SwapNotificationsModel.kt @@ -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.addInsufficientFundsNotification() { + val enteredFromAmount = notificationData.enteredFromAmount ?: return + val balance = notificationData.fromCryptoCurrencyStatus?.value?.amount ?: return + if (enteredFromAmount > balance) { + add(SwapNotificationUM.Error.InsufficientFunds) + } + } + fun MutableList.addExpressErrorNotification() { val expressError = notificationData.expressError ?: return val fromCryptoCurrency = notificationData.fromCryptoCurrency ?: return diff --git a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/sendviaswap/confirm/SendWithSwapConfirmComponent.kt b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/sendviaswap/confirm/SendWithSwapConfirmComponent.kt index 59d34c0701..98b57000b7 100644 --- a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/sendviaswap/confirm/SendWithSwapConfirmComponent.kt +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/sendviaswap/confirm/SendWithSwapConfirmComponent.kt @@ -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, ), ), ) diff --git a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/sendviaswap/confirm/model/SendWithSwapConfirmModel.kt b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/sendviaswap/confirm/model/SendWithSwapConfirmModel.kt index bea5544651..3c685ed2c6 100644 --- a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/sendviaswap/confirm/model/SendWithSwapConfirmModel.kt +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/sendviaswap/confirm/model/SendWithSwapConfirmModel.kt @@ -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 { 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( diff --git a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/sendviaswap/confirm/model/SwapTransactionSender.kt b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/sendviaswap/confirm/model/SwapTransactionSender.kt index b5c91be315..080ab91574 100644 --- a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/sendviaswap/confirm/model/SwapTransactionSender.kt +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/sendviaswap/confirm/model/SwapTransactionSender.kt @@ -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, diff --git a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/sendviaswap/rateinfo/SendWithSwapRateInfoFactory.kt b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/sendviaswap/rateinfo/SendWithSwapRateInfoFactory.kt new file mode 100644 index 0000000000..29b381307f --- /dev/null +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/sendviaswap/rateinfo/SendWithSwapRateInfoFactory.kt @@ -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() } + } + } + } +} \ No newline at end of file diff --git a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/sendviaswap/rateinfo/SwapRateInfoComponent.kt b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/sendviaswap/rateinfo/SwapRateInfoComponent.kt new file mode 100644 index 0000000000..f7f7cb6c6f --- /dev/null +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/sendviaswap/rateinfo/SwapRateInfoComponent.kt @@ -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) + } +} \ No newline at end of file diff --git a/features/swap/data/src/main/java/com/tangem/feature/swap/DefaultSwapRepository.kt b/features/swap/data/src/main/java/com/tangem/feature/swap/DefaultSwapRepository.kt index ebd13fd35b..968145424e 100644 --- a/features/swap/data/src/main/java/com/tangem/feature/swap/DefaultSwapRepository.kt +++ b/features/swap/data/src/main/java/com/tangem/feature/swap/DefaultSwapRepository.kt @@ -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)