diff --git a/common/ui/src/main/java/com/tangem/common/ui/amountScreen/ui/AmountFieldV2.kt b/common/ui/src/main/java/com/tangem/common/ui/amountScreen/ui/AmountFieldV2.kt index dbc955ab1e..9fdb77d943 100644 --- a/common/ui/src/main/java/com/tangem/common/ui/amountScreen/ui/AmountFieldV2.kt +++ b/common/ui/src/main/java/com/tangem/common/ui/amountScreen/ui/AmountFieldV2.kt @@ -62,6 +62,7 @@ fun AmountFieldV2( onValuePastedTriggerDismiss: () -> Unit, onCurrencyChange: (Boolean) -> Unit, modifier: Modifier = Modifier, + reserveSpaceForError: Boolean = true, ) { val decimalFormat = rememberDecimalFormat() @@ -120,12 +121,13 @@ fun AmountFieldV2( AmountSecondary( amountUM = amountUM, onCurrencyChange = onCurrencyChange, + reserveSpaceForError = reserveSpaceForError, ) } } @Composable -private fun AmountSecondary(amountUM: AmountState, onCurrencyChange: (Boolean) -> Unit) { +private fun AmountSecondary(amountUM: AmountState, onCurrencyChange: (Boolean) -> Unit, reserveSpaceForError: Boolean) { Box( modifier = Modifier .fillMaxWidth() @@ -144,29 +146,33 @@ private fun AmountSecondary(amountUM: AmountState, onCurrencyChange: (Boolean) - AmountFieldCurrencyInfo( amountUM = amountUM, onCurrencyChange = onCurrencyChange, + modifier = if (reserveSpaceForError) Modifier.padding(bottom = 16.dp) else Modifier, ) AmountFieldError( isError = amountUM.amountTextField.isError, isWarning = amountUM.amountTextField.isWarning, error = amountUM.amountTextField.error, + reserveSpaceForError = reserveSpaceForError, modifier = Modifier - .align(BottomCenter) - .padding(top = 24.dp), + .align(BottomCenter), ) } } } @Composable -private fun BoxScope.AmountFieldCurrencyInfo(amountUM: AmountState.Data, onCurrencyChange: (Boolean) -> Unit) { +private fun BoxScope.AmountFieldCurrencyInfo( + amountUM: AmountState.Data, + onCurrencyChange: (Boolean) -> Unit, + modifier: Modifier = Modifier, +) { val isFiatAvailable = amountUM.amountTextField.fiatAmount.value != null Row( verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(4.dp), - modifier = Modifier + modifier = modifier .align(TopCenter) - .padding(bottom = 16.dp) .clickable( interactionSource = remember { MutableInteractionSource() }, indication = null, @@ -255,13 +261,14 @@ private fun AmountFieldError( isError: Boolean, isWarning: Boolean, error: TextReference, + reserveSpaceForError: Boolean, modifier: Modifier = Modifier, ) { AnimatedVisibility( visible = isError || isWarning, enter = fadeIn(), exit = fadeOut(), - modifier = modifier, + modifier = if (reserveSpaceForError) modifier.padding(top = 24.dp) else modifier, label = "Error field appearance animation", ) { val errorText = remember(this, error) { error } @@ -271,7 +278,13 @@ private fun AmountFieldError( style = TangemTheme.typography.caption2, color = color, textAlign = TextAlign.Center, - modifier = Modifier.testTag(SendScreenTestTags.AMOUNT_ERROR_TEXT), + modifier = if (reserveSpaceForError) { + Modifier.testTag(SendScreenTestTags.AMOUNT_ERROR_TEXT) + } else { + Modifier + .padding(top = 24.dp) + .testTag(SendScreenTestTags.AMOUNT_ERROR_TEXT) + }, ) } } @@ -324,6 +337,7 @@ private fun AmountFieldV2_Preview(@PreviewParameter(AmountFieldV2PreviewProvider onValueChange = {}, onValuePastedTriggerDismiss = { }, onCurrencyChange = {}, + reserveSpaceForError = false, modifier = Modifier.background(TangemTheme.colors.background.action), ) } diff --git a/common/ui/src/main/java/com/tangem/common/ui/amountScreen/utils/AmountUtils.kt b/common/ui/src/main/java/com/tangem/common/ui/amountScreen/utils/AmountUtils.kt index fe59dbf317..990e4bc9c1 100644 --- a/common/ui/src/main/java/com/tangem/common/ui/amountScreen/utils/AmountUtils.kt +++ b/common/ui/src/main/java/com/tangem/common/ui/amountScreen/utils/AmountUtils.kt @@ -40,13 +40,13 @@ internal fun String.checkExceedBalance( maxEnterAmount: EnterAmountBoundary, amountTextField: AmountFieldModel, ): Boolean { - val currencyCryptoAmount = maxEnterAmount.amount ?: BigDecimal.ZERO - val currencyFiatAmount = maxEnterAmount.fiatAmount ?: BigDecimal.ZERO val fiatDecimal = parseToBigDecimal(amountTextField.fiatAmount.decimals) val cryptoDecimal = parseToBigDecimal(amountTextField.cryptoAmount.decimals) return if (amountTextField.isFiatValue) { + val currencyFiatAmount = maxEnterAmount.fiatAmount ?: return false fiatDecimal > currencyFiatAmount } else { + val currencyCryptoAmount = maxEnterAmount.amount ?: return false cryptoDecimal > currencyCryptoAmount } } diff --git a/core/ui/src/main/res/drawable/ic_fixed.xml b/core/ui/src/main/res/drawable/ic_fixed.xml new file mode 100644 index 0000000000..585d3632c3 --- /dev/null +++ b/core/ui/src/main/res/drawable/ic_fixed.xml @@ -0,0 +1,9 @@ + + + diff --git a/core/ui/src/main/res/drawable/ic_floating.xml b/core/ui/src/main/res/drawable/ic_floating.xml new file mode 100644 index 0000000000..0b94fd88fa --- /dev/null +++ b/core/ui/src/main/res/drawable/ic_floating.xml @@ -0,0 +1,13 @@ + + + + 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 0f4ef33398..f5d7042b43 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 @@ -15,6 +15,8 @@ import com.tangem.datasource.api.express.TangemExpressApi import com.tangem.datasource.api.express.models.request.ExchangeSentRequestBody import com.tangem.datasource.api.express.models.request.PairsRequestBody import com.tangem.datasource.api.express.models.response.ExchangeDataResponseWithTxDetails +import com.tangem.datasource.api.express.models.response.RateType +import com.tangem.datasource.api.express.models.response.SwapPairProvider import com.tangem.datasource.api.express.models.response.TxDetails import com.tangem.datasource.crypto.DataSignatureVerifier import com.tangem.datasource.di.NetworkMoshi @@ -98,8 +100,7 @@ internal class DefaultSwapRepositoryV2 @Inject constructor( } val mappedProviders = pair.providers - .filterNot { it.hasOnlyFixedRateType() } - .mapNotNull { expressProviders[it.providerId] } + .mapNotNull { it.withExpressProvider(expressProviders) } .filterYieldSupplyProvider(statusFrom) if (statusFrom != null && statusTo != null && mappedProviders.isNotEmpty()) { @@ -157,8 +158,7 @@ internal class DefaultSwapRepositoryV2 @Inject constructor( val currencyStatusTo = createSendWithSwapCryptoCurrencyStatus(statusToDeferred.await()) val mappedProvider = pair.providers - .filterNot { it.hasOnlyFixedRateType() } - .mapNotNull { mappedProviders[it.providerId] } + .mapNotNull { it.withExpressProvider(mappedProviders) } .filterYieldSupplyProvider(currencyStatusFrom) if (currencyStatusFrom != null && currencyStatusTo != null && mappedProvider.isNotEmpty()) { @@ -443,6 +443,18 @@ internal class DefaultSwapRepositoryV2 @Inject constructor( } } + private fun SwapPairProvider.withExpressProvider( + expressProviders: Map, + ): ExpressProvider? { + val provider = expressProviders[providerId] ?: return null + return provider.copy(rateTypes = rateTypes.map { it.toExpressRateType() }) + } + + private fun RateType.toExpressRateType(): ExpressRateType = when (this) { + RateType.FLOAT -> ExpressRateType.Float + RateType.FIXED -> ExpressRateType.Fixed + } + private fun CryptoCurrency.getContractAddress(): String { return when (this) { is CryptoCurrency.Token -> this.contractAddress 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 3a65c92b9c..45df45fc76 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 @@ -8,7 +8,7 @@ import java.math.BigDecimal * * @property provider swap provider * @property toTokenAmount amount of token you want to receive - * @property fromTokenAmount amount of from-token required (for fixed rate quotes) + * @property fromTokenAmount amount of from-token required (only set for fixed rate quotes) * @property allowanceContract whether swap occurs via third token */ data class SwapQuoteModel( diff --git a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/entity/SwapAmountUM.kt b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/entity/SwapAmountUM.kt index c749636dc6..b5cdb3898e 100644 --- a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/entity/SwapAmountUM.kt +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/entity/SwapAmountUM.kt @@ -10,6 +10,7 @@ import com.tangem.domain.models.currency.CryptoCurrencyStatus import com.tangem.domain.swap.models.SwapAmountType import com.tangem.domain.swap.models.SwapCurrencies import com.tangem.domain.swap.models.SwapDirection +import com.tangem.domain.swap.models.SwapRateMode import com.tangem.features.swap.v2.impl.common.entity.SwapQuoteUM import kotlinx.collections.immutable.ImmutableList @@ -44,6 +45,7 @@ internal sealed class SwapAmountUM { // selected swap route val swapRateType: ExpressRateType, + val swapRateMode: SwapRateMode, // swap models val swapCurrencies: SwapCurrencies, diff --git a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/SwapAmountClickIntents.kt b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/SwapAmountClickIntents.kt index 0e9ba829e8..38716f07d8 100644 --- a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/SwapAmountClickIntents.kt +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/SwapAmountClickIntents.kt @@ -8,6 +8,7 @@ internal interface SwapAmountClickIntents : AmountScreenClickIntents { fun onExpandEditField(selectedAmountType: SwapAmountType) fun onInfoClick() fun onSelectTokenClick() + fun onRateClick() fun onSeparatorClick() fun onProviderClick() } \ No newline at end of file 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 45499a10b7..ac2fe177b4 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 @@ -16,17 +16,18 @@ import com.tangem.core.decompose.di.ModelScoped import com.tangem.core.decompose.model.Model import com.tangem.core.decompose.model.ParamsContainer import com.tangem.core.ui.extensions.resourceReference +import com.tangem.core.ui.utils.parseBigDecimal import com.tangem.datasource.local.swap.SwapBestRateAnimationStore import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.express.models.ExpressError -import com.tangem.domain.express.models.ExpressRateType import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.models.currency.CryptoCurrencyStatus import com.tangem.domain.notifications.ShouldShowNotificationUseCase import com.tangem.domain.settings.usercountry.GetUserCountryUseCase import com.tangem.domain.settings.usercountry.models.UserCountry import com.tangem.domain.settings.usercountry.models.needApplyFCARestrictions +import com.tangem.domain.express.models.ExpressRateType import com.tangem.domain.swap.models.* import com.tangem.domain.swap.models.SwapDirection.Companion.withSwapDirection import com.tangem.domain.swap.usecase.GetSwapQuoteUseCase @@ -172,17 +173,43 @@ internal class SwapAmountModel @Inject constructor( secondaryMaximumAmountBoundary = secondaryMaximumAmountBoundary, secondaryMinimumAmountBoundary = secondaryMinimumAmountBoundary, isNeedApplyFCARestrictions = userCountry.needApplyFCARestrictions(), + isBalanceHidden = params.isBalanceHidingFlow.value, + primaryMaximumAmountBoundary = primaryMaximumAmountBoundary, + primaryMinimumAmountBoundary = primaryMinimumAmountBoundary, ), ) } override fun onExpandEditField(selectedAmountType: SwapAmountType) { + val content = uiState.value as? SwapAmountUM.Content ?: return + + val newSwapRateType = when (selectedAmountType) { + SwapAmountType.To -> { + if (content.swapRateMode != SwapRateMode.FLOAT_ONLY) { + ExpressRateType.Fixed + } else { + return + } + } + SwapAmountType.From -> { + if (content.swapRateMode == SwapRateMode.FIXED_ONLY) { + ExpressRateType.Fixed + } else if (content.swapRateMode == SwapRateMode.FLOAT_AND_FIXED) { + ExpressRateType.Float + } else { + return + } + } + } + uiState.update { amountUM -> - if (amountUM !is SwapAmountUM.Content) return + if (amountUM !is SwapAmountUM.Content) return@update amountUM amountUM.copy( selectedAmountType = selectedAmountType, + swapRateType = newSwapRateType, ) } + startLoadingQuotesTask(isSilentReload = false) } override fun onInfoClick() { @@ -290,6 +317,10 @@ internal class SwapAmountModel @Inject constructor( } } + override fun onRateClick() { + // TODO [REDACTED_TASK_KEY] + } + override fun onSeparatorClick() { val amountParams = params as? SwapAmountComponentParams.AmountParams ?: return @@ -335,17 +366,9 @@ internal class SwapAmountModel @Inject constructor( private fun subscribeOnBalanceHiddenUpdates() { params.isBalanceHidingFlow.onEach { isHidden -> - val isOnlyOneWallet = getWalletsUseCase.invokeSync().size == 1 uiState.transformerUpdate( SwapAmountBalanceHiddenTransformer( isBalanceHidden = isHidden, - isSingleWallet = isOnlyOneWallet, - userWallet = userWallet, - appCurrency = appCurrency, - swapDirection = swapDirection, - clickIntents = this, - isAccountsMode = params.isAccountModeFlow.value, - account = params.accountFlow.value, ), ) }.launchIn(modelScope) @@ -525,16 +548,14 @@ internal class SwapAmountModel @Inject constructor( private fun initPairs(swapCurrencies: SwapCurrencies, secondaryCryptoCurrency: CryptoCurrency?) { modelScope.launch { - val swapCryptoCurrency = selectInitialPairUseCase( + val secondaryCurrency = selectInitialPairUseCase( primaryCryptoCurrency = primaryCryptoCurrency, secondaryCryptoCurrency = secondaryCryptoCurrency, userWallet = userWallet, swapCurrencies = swapCurrencies, swapDirection = params.swapDirection, ) - - val secondaryStatus = swapCryptoCurrency?.currencyStatus - + val secondaryStatus = secondaryCurrency?.currencyStatus val primaryStatus = (uiState.value as? SwapAmountUM.Content)?.primaryCryptoCurrencyStatus if (secondaryStatus != null && primaryStatus != null) { initCurrencies(primaryStatus, secondaryStatus) @@ -553,8 +574,14 @@ internal class SwapAmountModel @Inject constructor( isSingleWallet = isOnlyOneWallet, isAccountsMode = params.isAccountModeFlow.value, account = params.accountFlow.value, + providers = secondaryCurrency.providers, ), ) + + val currentState = uiState.value as? SwapAmountUM.Content + if (currentState != null && currentState.swapRateMode != SwapRateMode.FLOAT_ONLY) { + computeAndSetSecondaryAmount(currentState) + } startLoadingQuotesTask(isSilentReload = false) } else { @Suppress("NullableToStringCall") @@ -592,7 +619,11 @@ internal class SwapAmountModel @Inject constructor( fiatRate = secondaryStatus.value.fiatRate, fiatAmount = secondaryStatus.value.fiatAmount, ) - secondaryMaximumAmountBoundary = MaxEnterAmountConverter().convert(secondaryStatus) + secondaryMaximumAmountBoundary = EnterAmountBoundary( + amount = null, + fiatRate = secondaryStatus.value.fiatRate, + fiatAmount = null, + ) } } @@ -606,18 +637,39 @@ internal class SwapAmountModel @Inject constructor( onReverse = { state.secondaryCryptoCurrencyStatus.currency to state.primaryCryptoCurrencyStatus.currency }, ) - val fromAmount = when (state.swapDirection) { - SwapDirection.Direct -> state.primaryAmount.amountField - SwapDirection.Reverse -> state.secondaryAmount.amountField - } as? AmountState.Data + val amountField = state.swapDirection.withSwapDirection( + onDirect = { + if (state.selectedAmountType == SwapAmountType.From) { + state.primaryAmount.amountField + } else { + state.secondaryAmount.amountField + } + }, + onReverse = { + if (state.selectedAmountType == SwapAmountType.From) { + state.secondaryAmount.amountField + } else { + state.primaryAmount.amountField + } + }, + ) as? AmountState.Data - val fromAmountValue = fromAmount?.amountTextField?.cryptoAmount?.value.orZero() + val amountValue = amountField?.amountTextField?.cryptoAmount?.value.orZero() val isAmountScreen = params is SwapAmountComponentParams.AmountParams - val isAmountError = fromAmount?.amountTextField?.isError == true || fromAmountValue.isNullOrZero() + val isAmountError = amountField?.amountTextField?.isError == true || amountValue.isNullOrZero() if (isAmountScreen && isAmountError) { uiState.transformerUpdate(SwapQuoteEmptyStateTransformer); return } + val rateType = when (state.selectedAmountType) { + SwapAmountType.To -> ExpressRateType.Fixed + SwapAmountType.From -> if (state.swapRateMode == SwapRateMode.FIXED_ONLY) { + ExpressRateType.Fixed + } else { + ExpressRateType.Float + } + } + if (!isSilentReload) uiState.transformerUpdate(SwapQuoteLoadingStateTransformer) modelScope.launch { @@ -625,6 +677,7 @@ internal class SwapAmountModel @Inject constructor( .asSequence() .filter { swapCurrencyStatus -> swapCurrencyStatus.currencyStatus.currency.id == toCryptoCurrency.id } .flatMap(SwapCryptoCurrency::providers) + .filter { provider -> provider.rateTypes.contains(rateType) } .toList() .map { provider -> async { @@ -632,9 +685,9 @@ internal class SwapAmountModel @Inject constructor( userWallet = userWallet, fromCryptoCurrency = fromCryptoCurrency, toCryptoCurrency = toCryptoCurrency, - amount = fromAmountValue, - amountType = SwapAmountType.From, - rateType = ExpressRateType.Float, + amount = amountValue, + amountType = state.selectedAmountType, + rateType = rateType, provider = provider, ).fold( ifLeft = { error -> @@ -650,7 +703,7 @@ internal class SwapAmountModel @Inject constructor( swapDirection = swapDirection, allowanceContract = quote.allowanceContract, isApprovalNeeded = checkAllowance(state, quote), - fromAmount = fromAmountValue, + fromAmount = amountValue, ).convert( SwapQuoteUMConverter.Data( quote = quote, @@ -669,12 +722,48 @@ internal class SwapAmountModel @Inject constructor( secondaryMinimumAmountBoundary = secondaryMinimumAmountBoundary, isSilentReload = isSilentReload, isNeedApplyFcaRestrictions = userCountry.needApplyFCARestrictions(), + isBalanceHidden = params.isBalanceHidingFlow.value, + primaryMaximumAmountBoundary = primaryMaximumAmountBoundary, + primaryMinimumAmountBoundary = primaryMinimumAmountBoundary, ), ) feeSelectorReloadTrigger.triggerUpdate() } } + /** + * Compute secondary amount from primary amount using fiat rate conversion + * secondaryAmount = primaryAmount × primaryFiatRate / secondaryFiatRate + * + */ + private fun computeAndSetSecondaryAmount(state: SwapAmountUM.Content) { + val secondaryStatus = state.secondaryCryptoCurrencyStatus ?: return + val primaryFiatRate = state.primaryCryptoCurrencyStatus.value.fiatRate ?: return + val secondaryFiatRate = secondaryStatus.value.fiatRate ?: return + if (secondaryFiatRate <= BigDecimal.ZERO) return + + val primaryAmountField = (state.primaryAmount as? SwapAmountFieldUM.Content) + ?.amountField as? AmountState.Data ?: return + val primaryAmount = primaryAmountField.amountTextField.cryptoAmount.value ?: return + if (primaryAmount <= BigDecimal.ZERO) return + + val secondaryAmount = primaryAmount + .multiply(primaryFiatRate) + .divide(secondaryFiatRate, secondaryStatus.currency.decimals, java.math.RoundingMode.HALF_UP) + + val secondaryValue = secondaryAmount.parseBigDecimal(secondaryStatus.currency.decimals) + + uiState.transformerUpdate( + SwapAmountValueChangeTransformer( + primaryMaximumAmountBoundary = primaryMaximumAmountBoundary, + secondaryMaximumAmountBoundary = secondaryMaximumAmountBoundary, + primaryMinimumAmountBoundary = primaryMinimumAmountBoundary, + secondaryMinimumAmountBoundary = secondaryMinimumAmountBoundary, + value = secondaryValue, + ), + ) + } + private fun startLoadingQuotesTask(isSilentReload: Boolean) { quoteTaskScheduler.cancelTask() loadQuotes(isSilentReload = isSilentReload) diff --git a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/SwapAmountQuoteUtils.kt b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/SwapAmountQuoteUtils.kt index f3b1614ce2..f0fee9face 100644 --- a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/SwapAmountQuoteUtils.kt +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/SwapAmountQuoteUtils.kt @@ -58,9 +58,9 @@ internal object SwapAmountQuoteUtils { ): SwapAmountUM { if (this !is SwapAmountUM.Content) return this - val updatedAmountField = if ( - selectedAmountType == SwapAmountType.From && swapDirection == SwapDirection.Direct - ) { + val isPrimaryFieldEdited = selectedAmountType == SwapAmountType.From && swapDirection == SwapDirection.Direct + + val updatedAmountField = if (isPrimaryFieldEdited) { val amountFieldUM = primaryAmount as? SwapAmountFieldUM.Content ?: return this amountFieldUM.onPrimaryAmount(primaryCryptoCurrencyStatus) } else { @@ -68,9 +68,9 @@ internal object SwapAmountQuoteUtils { val amountFieldUM = secondaryAmount as? SwapAmountFieldUM.Content ?: return this amountFieldUM.onSecondaryAmount(secondaryCryptoCurrencyStatus) } - return copy( - primaryAmount = updatedAmountField, + primaryAmount = if (isPrimaryFieldEdited) updatedAmountField else primaryAmount, + secondaryAmount = if (isPrimaryFieldEdited) secondaryAmount else updatedAmountField, ) } } \ No newline at end of file diff --git a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/converter/SwapAmountFieldConverter.kt b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/converter/SwapAmountFieldConverter.kt index 9adad0ddeb..6bfb4de5bd 100644 --- a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/converter/SwapAmountFieldConverter.kt +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/converter/SwapAmountFieldConverter.kt @@ -5,12 +5,8 @@ import com.tangem.common.ui.amountScreen.converters.AmountAccountConverter import com.tangem.common.ui.amountScreen.converters.AmountStateConverter import com.tangem.common.ui.amountScreen.converters.MaxEnterAmountConverter import com.tangem.common.ui.amountScreen.models.AmountParameters -import com.tangem.core.ui.components.atoms.text.TextEllipsis import com.tangem.core.ui.components.currency.icon.converter.CryptoCurrencyToIconStateConverter import com.tangem.core.ui.extensions.* -import com.tangem.core.ui.format.bigdecimal.crypto -import com.tangem.core.ui.format.bigdecimal.fiat -import com.tangem.core.ui.format.bigdecimal.format import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.models.account.Account import com.tangem.domain.models.currency.CryptoCurrencyStatus @@ -19,7 +15,7 @@ import com.tangem.domain.swap.models.SwapAmountType import com.tangem.domain.swap.models.SwapDirection import com.tangem.features.swap.v2.impl.R import com.tangem.features.swap.v2.impl.amount.entity.SwapAmountFieldUM -import com.tangem.utils.StringsSigns.DOT +import java.math.BigDecimal @Suppress("LongParameterList") internal class SwapAmountFieldConverter( @@ -36,24 +32,34 @@ internal class SwapAmountFieldConverter( private val iconStateConverter = CryptoCurrencyToIconStateConverter() private val maxEnterAmountConverter = MaxEnterAmountConverter() - fun convert(selectedType: SwapAmountType, cryptoCurrencyStatus: CryptoCurrencyStatus): SwapAmountFieldUM { + fun convert( + swapAmountType: SwapAmountType, + cryptoCurrencyStatus: CryptoCurrencyStatus, + isSelected: Boolean, + isAmountEmpty: Boolean = true, + displayAmount: BigDecimal? = null, + ): SwapAmountFieldUM { val walletTitle = if (isSingleWallet) { resourceReference(R.string.send_from_title) } else { resourceReference(R.string.send_from_wallet_name, wrappedList(userWallet.name)) } + val subtitles = computeSubtitles( + swapAmountType = swapAmountType, + cryptoCurrencyStatus = cryptoCurrencyStatus, + isEntering = isSelected, + isAmountEmpty = isAmountEmpty, + displayAmount = displayAmount, + ) return SwapAmountFieldUM.Content( - amountType = selectedType, + amountType = swapAmountType, title = stringReference(cryptoCurrencyStatus.currency.name), - subtitleLeft = getSubtitleLeft(selectedType = selectedType, cryptoCurrencyStatus = cryptoCurrencyStatus), - subtitleEllipsisLeft = getSubtitleEllipsisLeft( - selectedType = selectedType, - cryptoCurrencyStatus = cryptoCurrencyStatus, - ), - subtitleRight = getSubtitleRight(selectedType = selectedType, cryptoCurrencyStatus = cryptoCurrencyStatus), - subtitleEllipsisRight = TextEllipsis.OffsetEnd(appCurrency.symbol.length), + subtitleLeft = subtitles.subtitleLeft, + subtitleEllipsisLeft = subtitles.subtitleEllipsisLeft, + subtitleRight = subtitles.subtitleRight, + subtitleEllipsisRight = subtitles.subtitleEllipsisRight, priceImpact = null, - isClickEnabled = selectedType.isViewingField(), + isClickEnabled = true, amountField = AmountStateConverter( clickIntents = clickIntents, appCurrency = appCurrency, @@ -65,8 +71,8 @@ internal class SwapAmountFieldConverter( isAccountsMode = isAccountsMode, walletTitle = walletTitle, prefixText = when { - selectedType.isEnteringField() -> resourceReference(R.string.common_from) - selectedType.isViewingField() -> resourceReference(R.string.common_to) + swapAmountType.isEnteringField() -> resourceReference(R.string.common_from) + swapAmountType.isViewingField() -> resourceReference(R.string.common_to) else -> TextReference.Companion.EMPTY }, ).convert(account), @@ -79,44 +85,29 @@ internal class SwapAmountFieldConverter( ) } - private fun getSubtitleLeft(selectedType: SwapAmountType, cryptoCurrencyStatus: CryptoCurrencyStatus) = when { - selectedType.isEnteringField() -> combinedReference( - stringReference( - cryptoCurrencyStatus.value.amount.format { - crypto(cryptoCurrency = cryptoCurrencyStatus.currency) - }, - ), - ).orMaskWithStars(isBalanceHidden) - selectedType.isViewingField() -> resourceReference(R.string.send_with_swap_recipient_get_amount) - else -> TextReference.Companion.EMPTY + private fun computeSubtitles( + swapAmountType: SwapAmountType, + cryptoCurrencyStatus: CryptoCurrencyStatus, + isEntering: Boolean, + isAmountEmpty: Boolean, + displayAmount: BigDecimal?, + ): SwapSubtitleResult = when (swapAmountType) { + SwapAmountType.From -> SwapFromSubtitleConverter.convert( + cryptoCurrencyStatus = cryptoCurrencyStatus, + isBalanceHidden = isBalanceHidden, + isEntering = isEntering, + isAmountEmpty = isAmountEmpty, + displayAmount = displayAmount, + ) + SwapAmountType.To -> SwapToSubtitleConverter.convert( + cryptoCurrencyStatus = cryptoCurrencyStatus, + isBalanceHidden = isBalanceHidden, + isEntering = isEntering, + isAmountEmpty = isAmountEmpty, + displayAmount = displayAmount, + ) } - private fun getSubtitleRight(selectedType: SwapAmountType, cryptoCurrencyStatus: CryptoCurrencyStatus) = when { - selectedType.isEnteringField() -> if (isBalanceHidden) { - TextReference.EMPTY - } else { - combinedReference( - stringReference(value = " $DOT "), - stringReference( - cryptoCurrencyStatus.value.fiatAmount.format { - fiat( - fiatCurrencyCode = appCurrency.code, - fiatCurrencySymbol = appCurrency.symbol, - ) - }, - ), - ) - } - else -> TextReference.EMPTY - } - - private fun getSubtitleEllipsisLeft(selectedType: SwapAmountType, cryptoCurrencyStatus: CryptoCurrencyStatus) = - when { - selectedType.isEnteringField() -> TextEllipsis.OffsetEnd(cryptoCurrencyStatus.currency.symbol.length) - selectedType.isViewingField() -> TextEllipsis.End - else -> TextEllipsis.End - } - private fun SwapAmountType.isEnteringField(): Boolean { return this == SwapAmountType.From && swapDirection == SwapDirection.Direct || this == SwapAmountType.To && swapDirection == SwapDirection.Reverse diff --git a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/converter/SwapAmountUpdateSubtitleConverter.kt b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/converter/SwapAmountUpdateSubtitleConverter.kt new file mode 100644 index 0000000000..1ed8a5c867 --- /dev/null +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/converter/SwapAmountUpdateSubtitleConverter.kt @@ -0,0 +1,63 @@ +package com.tangem.features.swap.v2.impl.amount.model.converter + +import com.tangem.domain.models.currency.CryptoCurrencyStatus +import com.tangem.domain.swap.models.SwapAmountType +import com.tangem.features.swap.v2.impl.amount.entity.SwapAmountFieldUM +import java.math.BigDecimal + +/** + * Updates all subtitle fields on an existing [SwapAmountFieldUM.Content] field. + * + * Delegates to [SwapFromSubtitleConverter] / [SwapToSubtitleConverter] based on the field's + * [SwapAmountFieldUM.Content.amountType]. All other field properties are preserved. + * + * `isEntering` is derived internally: the field is "entering" when its amountType matches + * [selectedAmountType], so callers cannot accidentally pass a wrong value. + * + * @param selectedAmountType the currently selected amount type from [SwapAmountUM.Content] + * @param isBalanceHidden whether balance values should be masked + */ +internal class SwapAmountUpdateSubtitleConverter( + private val selectedAmountType: SwapAmountType, + private val isBalanceHidden: Boolean, +) { + + /** + * Updates all subtitle fields after a quote arrives. + * + * @param field the current field content to update + * @param cryptoCurrencyStatus status used for balance formatting + * @param isAmountEmpty whether the amount in this field is empty/null + * @param displayAmount optional override for the displayed amount (e.g. quote toAmount/fromAmount) + */ + fun updateSubtitles( + field: SwapAmountFieldUM.Content, + cryptoCurrencyStatus: CryptoCurrencyStatus, + isAmountEmpty: Boolean, + displayAmount: BigDecimal? = null, + ): SwapAmountFieldUM.Content { + val isEntering = field.amountType == selectedAmountType + val subtitles = when (field.amountType) { + SwapAmountType.From -> SwapFromSubtitleConverter.convert( + cryptoCurrencyStatus = cryptoCurrencyStatus, + isBalanceHidden = isBalanceHidden, + isEntering = isEntering, + isAmountEmpty = isAmountEmpty, + displayAmount = displayAmount, + ) + SwapAmountType.To -> SwapToSubtitleConverter.convert( + cryptoCurrencyStatus = cryptoCurrencyStatus, + isBalanceHidden = isBalanceHidden, + isEntering = isEntering, + isAmountEmpty = isAmountEmpty, + displayAmount = displayAmount, + ) + } + return field.copy( + subtitleLeft = subtitles.subtitleLeft, + subtitleEllipsisLeft = subtitles.subtitleEllipsisLeft, + subtitleRight = subtitles.subtitleRight, + subtitleEllipsisRight = subtitles.subtitleEllipsisRight, + ) + } +} \ No newline at end of file diff --git a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/converter/SwapFromSubtitleConverter.kt b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/converter/SwapFromSubtitleConverter.kt new file mode 100644 index 0000000000..ed5c112fd0 --- /dev/null +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/converter/SwapFromSubtitleConverter.kt @@ -0,0 +1,69 @@ +package com.tangem.features.swap.v2.impl.amount.model.converter + +import com.tangem.core.ui.components.atoms.text.TextEllipsis +import com.tangem.core.ui.extensions.* +import com.tangem.core.ui.format.bigdecimal.crypto +import com.tangem.core.ui.format.bigdecimal.format +import com.tangem.domain.models.currency.CryptoCurrencyStatus +import com.tangem.features.swap.v2.impl.R +import java.math.BigDecimal + +/** + * Computes all subtitle fields for the **From** (primary) field. + * + * | State | subtitleLeft | subtitleRight | ellipsisLeft | + * |--------------------------------|-----------------------------------|-----------------------------------|------------------------| + * | Entering (float), any | "Balance: " (empty param) | "{balance}" masked | OffsetEnd(symbol) | + * | Viewing (fixed), empty | "Balance: " (empty param) | "{balance}" masked (crypto only) | End | + * | Viewing (fixed), not empty | send_from_title | "{displayStr}" masked | OffsetEnd(symbol) | + */ +internal object SwapFromSubtitleConverter { + + fun convert( + cryptoCurrencyStatus: CryptoCurrencyStatus, + isBalanceHidden: Boolean, + isEntering: Boolean, + isAmountEmpty: Boolean, + displayAmount: BigDecimal?, + ): SwapSubtitleResult { + val symbol = cryptoCurrencyStatus.currency.symbol + val balance = cryptoCurrencyStatus.value.amount.format { + crypto(cryptoCurrency = cryptoCurrencyStatus.currency) + } + val displayStr = displayAmount?.format { + crypto(cryptoCurrency = cryptoCurrencyStatus.currency) + } ?: balance + + val subtitleLeft: TextReference + val subtitleRight: TextReference + val ellipsisLeft: TextEllipsis + + when { + isEntering -> { + subtitleLeft = resourceReference(R.string.common_balance, wrappedList("")) + subtitleRight = combinedReference(stringReference(balance)) + .orMaskWithStars(isBalanceHidden) + ellipsisLeft = TextEllipsis.OffsetEnd(symbol.length) + } + !isEntering && isAmountEmpty -> { + subtitleLeft = resourceReference(R.string.common_balance, wrappedList("")) + subtitleRight = combinedReference(stringReference(balance)) + .orMaskWithStars(isBalanceHidden) + ellipsisLeft = TextEllipsis.End + } + else -> { + subtitleLeft = resourceReference(R.string.send_from_title) + subtitleRight = combinedReference(stringReference(displayStr)) + .orMaskWithStars(isBalanceHidden) + ellipsisLeft = TextEllipsis.OffsetEnd(symbol.length) + } + } + + return SwapSubtitleResult( + subtitleLeft = subtitleLeft, + subtitleRight = subtitleRight, + subtitleEllipsisLeft = ellipsisLeft, + subtitleEllipsisRight = TextEllipsis.OffsetEnd(symbol.length), + ) + } +} \ No newline at end of file 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 c014e539e7..3f5255f097 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 @@ -42,6 +42,10 @@ internal class SwapQuoteUMConverter( append(rate.format { crypto(secondaryCurrency) }) } + val fromAmountValue = stringReference( + quote.fromTokenAmount?.format { crypto(primaryCurrency) }.orEmpty(), + ) + return if (allowanceContract != null) { if (isApprovalNeeded) { SwapQuoteUM.Allowance( @@ -51,11 +55,13 @@ internal class SwapQuoteUMConverter( } else { SwapQuoteUM.Content( provider = provider, - quoteAmount = quote.toTokenAmount, + toAmount = quote.toTokenAmount, + fromAmount = quote.fromTokenAmount, diffPercent = DifferencePercent.Empty, - quoteAmountValue = stringReference( + toAmountValue = stringReference( quote.toTokenAmount.toQuoteValue(), ), + fromAmountValue = fromAmountValue, rate = annotatedReference(rateString), isSingleProvider = false, ) @@ -63,11 +69,13 @@ internal class SwapQuoteUMConverter( } else { SwapQuoteUM.Content( provider = provider, - quoteAmount = quote.toTokenAmount, + toAmount = quote.toTokenAmount, + fromAmount = quote.fromTokenAmount, diffPercent = DifferencePercent.Empty, - quoteAmountValue = stringReference( + toAmountValue = stringReference( quote.toTokenAmount.toQuoteValue(), ), + fromAmountValue = fromAmountValue, rate = annotatedReference(rateString), isSingleProvider = false, ) diff --git a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/converter/SwapSubtitleResult.kt b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/converter/SwapSubtitleResult.kt new file mode 100644 index 0000000000..3253f56f44 --- /dev/null +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/converter/SwapSubtitleResult.kt @@ -0,0 +1,11 @@ +package com.tangem.features.swap.v2.impl.amount.model.converter + +import com.tangem.core.ui.components.atoms.text.TextEllipsis +import com.tangem.core.ui.extensions.TextReference + +internal data class SwapSubtitleResult( + val subtitleLeft: TextReference, + val subtitleRight: TextReference, + val subtitleEllipsisLeft: TextEllipsis, + val subtitleEllipsisRight: TextEllipsis, +) \ No newline at end of file diff --git a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/converter/SwapToSubtitleConverter.kt b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/converter/SwapToSubtitleConverter.kt new file mode 100644 index 0000000000..cba9789884 --- /dev/null +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/converter/SwapToSubtitleConverter.kt @@ -0,0 +1,73 @@ +package com.tangem.features.swap.v2.impl.amount.model.converter + +import com.tangem.core.ui.components.atoms.text.TextEllipsis +import com.tangem.core.ui.extensions.* +import com.tangem.core.ui.format.bigdecimal.crypto +import com.tangem.core.ui.format.bigdecimal.format +import com.tangem.domain.models.currency.CryptoCurrencyStatus +import com.tangem.features.swap.v2.impl.R +import com.tangem.utils.StringsSigns.TILDE_SIGN +import java.math.BigDecimal + +/** + * Computes all subtitle fields for the **To** (secondary) field. + * + * | State | subtitleLeft | subtitleRight | ellipsisLeft | + * |--------------------------------|------------------------------------|----------------------------|------------------------| + * | Viewing (float), empty | "Will be sent to recipient" | EMPTY | End | + * | Viewing (float), not empty | "Recipient gets" (empty param) | "~{displayStr}" masked | End | + * | Entering (fixed), any | "Will be sent to recipient" | EMPTY | End | + */ +internal object SwapToSubtitleConverter { + + fun convert( + cryptoCurrencyStatus: CryptoCurrencyStatus, + isBalanceHidden: Boolean, + isEntering: Boolean, + isAmountEmpty: Boolean, + displayAmount: BigDecimal?, + ): SwapSubtitleResult { + val symbol = cryptoCurrencyStatus.currency.symbol + val balance = cryptoCurrencyStatus.value.amount.format { + crypto(cryptoCurrency = cryptoCurrencyStatus.currency) + } + val displayStr = displayAmount?.format { + crypto(cryptoCurrency = cryptoCurrencyStatus.currency) + } ?: balance + + val subtitleLeft: TextReference + val subtitleRight: TextReference + val ellipsisLeft: TextEllipsis + + when { + isEntering -> { + subtitleLeft = resourceReference(R.string.send_amount_receive_token_subtitle) + subtitleRight = TextReference.EMPTY + ellipsisLeft = TextEllipsis.End + } + !isEntering && isAmountEmpty -> { + subtitleLeft = resourceReference(R.string.send_amount_receive_token_subtitle) + subtitleRight = TextReference.EMPTY + ellipsisLeft = TextEllipsis.End + } + else -> { + subtitleLeft = resourceReference( + R.string.send_with_swap_recipient_get_amount, + wrappedList(""), + ) + subtitleRight = combinedReference( + stringReference(TILDE_SIGN), + stringReference(displayStr), + ).orMaskWithStars(isBalanceHidden) + ellipsisLeft = TextEllipsis.End + } + } + + return SwapSubtitleResult( + subtitleLeft = subtitleLeft, + subtitleRight = subtitleRight, + subtitleEllipsisLeft = ellipsisLeft, + subtitleEllipsisRight = TextEllipsis.OffsetEnd(symbol.length), + ) + } +} \ No newline at end of file diff --git a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/transformers/SwapAmountBalanceHiddenTransformer.kt b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/transformers/SwapAmountBalanceHiddenTransformer.kt index ede7a349fb..1fe1635001 100644 --- a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/transformers/SwapAmountBalanceHiddenTransformer.kt +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/transformers/SwapAmountBalanceHiddenTransformer.kt @@ -1,70 +1,55 @@ package com.tangem.features.swap.v2.impl.amount.model.transformers -import com.tangem.common.ui.amountScreen.AmountScreenClickIntents import com.tangem.common.ui.amountScreen.models.AmountState -import com.tangem.domain.appcurrency.model.AppCurrency -import com.tangem.domain.models.account.Account -import com.tangem.domain.models.wallet.UserWallet -import com.tangem.domain.swap.models.SwapAmountType -import com.tangem.domain.swap.models.SwapDirection import com.tangem.features.swap.v2.impl.amount.entity.SwapAmountFieldUM import com.tangem.features.swap.v2.impl.amount.entity.SwapAmountUM -import com.tangem.features.swap.v2.impl.amount.model.converter.SwapAmountFieldConverter +import com.tangem.features.swap.v2.impl.amount.model.converter.SwapAmountUpdateSubtitleConverter +import com.tangem.features.swap.v2.impl.common.entity.SwapQuoteUM import com.tangem.utils.transformer.Transformer -@Suppress("LongParameterList") internal class SwapAmountBalanceHiddenTransformer( private val isBalanceHidden: Boolean, - private val isSingleWallet: Boolean, - private val userWallet: UserWallet, - private val appCurrency: AppCurrency, - private val swapDirection: SwapDirection, - private val clickIntents: AmountScreenClickIntents, - private val isAccountsMode: Boolean, - private val account: Account?, ) : Transformer { override fun transform(prevState: SwapAmountUM): SwapAmountUM { val content = prevState as? SwapAmountUM.Content ?: return prevState - val amountFieldConverter = SwapAmountFieldConverter( - swapDirection = swapDirection, + val quoteContent = content.selectedQuote as? SwapQuoteUM.Content + + val subtitleConverter = SwapAmountUpdateSubtitleConverter( + selectedAmountType = content.selectedAmountType, isBalanceHidden = isBalanceHidden, - userWallet = userWallet, - appCurrency = appCurrency, - clickIntents = clickIntents, - isSingleWallet = isSingleWallet, - isAccountsMode = isAccountsMode, - account = account, ) - val recalculatedPrimary = amountFieldConverter.convert( - selectedType = SwapAmountType.From, - cryptoCurrencyStatus = content.primaryCryptoCurrencyStatus, - ) as SwapAmountFieldUM.Content - - val oldPrimary = content.primaryAmount as? SwapAmountFieldUM.Content - - val mergedAmountField = if ( - oldPrimary?.amountField is AmountState.Data && recalculatedPrimary.amountField is AmountState.Data - ) { - val oldData = oldPrimary.amountField - val newData = recalculatedPrimary.amountField - newData.copy( - amountTextField = oldData.amountTextField, - isPrimaryButtonEnabled = oldData.isPrimaryButtonEnabled, - isEditingDisabled = oldData.isEditingDisabled, - reduceAmountBy = oldData.reduceAmountBy, - isIgnoreReduce = oldData.isIgnoreReduce, + val updatedPrimary = (content.primaryAmount as? SwapAmountFieldUM.Content)?.let { primary -> + val isAmountEmpty = (primary.amountField as? AmountState.Data) + ?.amountTextField?.cryptoAmount?.value == null + subtitleConverter.updateSubtitles( + field = primary, + cryptoCurrencyStatus = content.primaryCryptoCurrencyStatus, + isAmountEmpty = isAmountEmpty, + displayAmount = quoteContent?.fromAmount, ) + } ?: content.primaryAmount + + val updatedSecondary = if (content.secondaryCryptoCurrencyStatus != null) { + (content.secondaryAmount as? SwapAmountFieldUM.Content)?.let { secondary -> + val isAmountEmpty = (secondary.amountField as? AmountState.Data) + ?.amountTextField?.cryptoAmount?.value == null + subtitleConverter.updateSubtitles( + field = secondary, + cryptoCurrencyStatus = content.secondaryCryptoCurrencyStatus, + isAmountEmpty = isAmountEmpty, + displayAmount = quoteContent?.toAmount, + ) + } ?: content.secondaryAmount } else { - recalculatedPrimary.amountField + content.secondaryAmount } - val updatedPrimaryAmount = recalculatedPrimary.copy( - amountField = mergedAmountField, + return content.copy( + primaryAmount = updatedPrimary, + secondaryAmount = updatedSecondary, ) - - return content.copy(primaryAmount = updatedPrimaryAmount) } } \ No newline at end of file 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 new file mode 100644 index 0000000000..edb2d9e257 --- /dev/null +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/transformers/SwapAmountErrorQuoteTransformer.kt @@ -0,0 +1,58 @@ +package com.tangem.features.swap.v2.impl.amount.model.transformers + +import com.tangem.common.ui.amountScreen.models.AmountState +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.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. + */ +internal object SwapAmountErrorQuoteTransformer : Transformer { + + override fun transform(prevState: SwapAmountUM): SwapAmountUM { + if (prevState !is SwapAmountUM.Content) return prevState + + val error = resourceReference(R.string.send_with_swap_something_went_wrong) + + val isPrimarySelected = prevState.selectedAmountType == SwapAmountType.From + + val newPrimaryAmount = if (isPrimarySelected) { + applyError(prevState.primaryAmount, error) + } else { + prevState.primaryAmount + } + + val newSecondaryAmount = if (!isPrimarySelected) { + applyError(prevState.secondaryAmount, error) + } else { + prevState.secondaryAmount + } + + return prevState.copy( + isPrimaryButtonEnabled = false, + primaryAmount = newPrimaryAmount, + secondaryAmount = newSecondaryAmount, + ) + } + + private fun applyError( + field: SwapAmountFieldUM, + error: com.tangem.core.ui.extensions.TextReference, + ): SwapAmountFieldUM { + val content = field as? SwapAmountFieldUM.Content ?: return field + val amountData = content.amountField as? AmountState.Data ?: return field + return content.copy( + amountField = amountData.copy( + amountTextField = amountData.amountTextField.copy( + error = error, + isError = true, + ), + ), + ) + } +} \ No newline at end of file diff --git a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/transformers/SwapAmountPrimaryReadyStateTransformer.kt b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/transformers/SwapAmountPrimaryReadyStateTransformer.kt index bb5046a48d..e2d648a6c9 100644 --- a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/transformers/SwapAmountPrimaryReadyStateTransformer.kt +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/transformers/SwapAmountPrimaryReadyStateTransformer.kt @@ -9,6 +9,7 @@ import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.swap.models.SwapAmountType import com.tangem.domain.swap.models.SwapCurrencies import com.tangem.domain.swap.models.SwapDirection +import com.tangem.domain.swap.models.SwapRateMode 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.SwapAmountFieldConverter @@ -46,8 +47,9 @@ internal class SwapAmountPrimaryReadyStateTransformer( isPrimaryButtonEnabled = false, primaryCryptoCurrencyStatus = primaryCryptoCurrencyStatus, primaryAmount = amountFieldConverter.convert( - selectedType = SwapAmountType.From, + swapAmountType = SwapAmountType.From, cryptoCurrencyStatus = primaryCryptoCurrencyStatus, + isSelected = prevState.selectedAmountType == SwapAmountType.From, ), secondaryCryptoCurrencyStatus = null, secondaryAmount = SwapAmountFieldUM.Empty( @@ -62,6 +64,7 @@ internal class SwapAmountPrimaryReadyStateTransformer( appCurrency = appCurrency, isShowBestRateAnimation = isShowBestRateAnimation, isShowFCAWarning = false, + swapRateMode = (prevState as? SwapAmountUM.Content)?.swapRateMode ?: SwapRateMode.FLOAT_ONLY, ) } } \ No newline at end of file diff --git a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/transformers/SwapAmountSecondaryReadyStateTransformer.kt b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/transformers/SwapAmountSecondaryReadyStateTransformer.kt index 545c13db6e..cc49a10c46 100644 --- a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/transformers/SwapAmountSecondaryReadyStateTransformer.kt +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/transformers/SwapAmountSecondaryReadyStateTransformer.kt @@ -2,13 +2,15 @@ package com.tangem.features.swap.v2.impl.amount.model.transformers import com.tangem.common.ui.amountScreen.AmountScreenClickIntents import com.tangem.domain.appcurrency.model.AppCurrency +import com.tangem.domain.express.models.ExpressProvider import com.tangem.domain.express.models.ExpressRateType import com.tangem.domain.models.account.Account import com.tangem.domain.models.currency.CryptoCurrencyStatus import com.tangem.domain.models.wallet.UserWallet -import com.tangem.domain.swap.models.SwapAmountType import com.tangem.domain.swap.models.SwapCurrencies import com.tangem.domain.swap.models.SwapDirection +import com.tangem.domain.swap.models.SwapAmountType +import com.tangem.domain.swap.models.SwapRateMode import com.tangem.features.swap.v2.impl.amount.entity.SwapAmountUM import com.tangem.features.swap.v2.impl.amount.model.converter.SwapAmountFieldConverter import com.tangem.features.swap.v2.impl.common.entity.SwapQuoteUM @@ -20,6 +22,7 @@ internal class SwapAmountSecondaryReadyStateTransformer( private val userWallet: UserWallet, private val primaryCryptoCurrencyStatus: CryptoCurrencyStatus, private val secondaryCryptoCurrencyStatus: CryptoCurrencyStatus, + private val providers: List, private val appCurrency: AppCurrency, private val swapCurrencies: SwapCurrencies, private val clickIntents: AmountScreenClickIntents, @@ -43,24 +46,41 @@ internal class SwapAmountSecondaryReadyStateTransformer( ) override fun transform(prevState: SwapAmountUM): SwapAmountUM { + val rateTypes = providers.flatMapTo(mutableSetOf()) { it.rateTypes } + val swapRateMode = when { + rateTypes.containsAll(listOf(ExpressRateType.Float, ExpressRateType.Fixed)) -> SwapRateMode.FLOAT_AND_FIXED + rateTypes.contains(ExpressRateType.Fixed) -> SwapRateMode.FIXED_ONLY + else -> SwapRateMode.FLOAT_ONLY + } + val selectedAmountType = if (swapRateMode != SwapRateMode.FLOAT_ONLY) { + ExpressRateType.Fixed + } else { + ExpressRateType.Float + } return SwapAmountUM.Content( isPrimaryButtonEnabled = false, primaryAmount = prevState.primaryAmount, primaryCryptoCurrencyStatus = primaryCryptoCurrencyStatus, secondaryAmount = amountFieldConverter.convert( - selectedType = SwapAmountType.To, + swapAmountType = SwapAmountType.To, cryptoCurrencyStatus = secondaryCryptoCurrencyStatus, + isSelected = prevState.selectedAmountType == SwapAmountType.To, ), secondaryCryptoCurrencyStatus = secondaryCryptoCurrencyStatus, swapCurrencies = swapCurrencies, - selectedAmountType = prevState.selectedAmountType, + selectedAmountType = if (selectedAmountType == ExpressRateType.Fixed) { + SwapAmountType.To + } else { + SwapAmountType.From + }, swapDirection = swapDirection, - swapRateType = ExpressRateType.Float, + swapRateType = selectedAmountType, swapQuotes = persistentListOf(), selectedQuote = SwapQuoteUM.Empty, appCurrency = appCurrency, isShowBestRateAnimation = isShowBestRateAnimation, isShowFCAWarning = false, + swapRateMode = swapRateMode, ) } } \ No newline at end of file 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 3f3f9ac158..8e1878c21c 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 @@ -4,12 +4,15 @@ import com.tangem.common.ui.amountScreen.converters.field.AmountFieldChangeTrans import com.tangem.common.ui.amountScreen.models.AmountState import com.tangem.common.ui.amountScreen.models.EnterAmountBoundary import com.tangem.core.ui.extensions.TextReference +import com.tangem.core.ui.extensions.resourceReference import com.tangem.core.ui.utils.parseBigDecimal import com.tangem.domain.swap.models.SwapAmountType 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.SwapAmountQuoteUtils.calculatePriceImpact import com.tangem.features.swap.v2.impl.amount.model.converter.SwapAmountErrorConverter +import com.tangem.features.swap.v2.impl.amount.model.converter.SwapAmountUpdateSubtitleConverter +import com.tangem.features.swap.v2.impl.R import com.tangem.features.swap.v2.impl.common.entity.SwapQuoteUM import com.tangem.features.swap.v2.impl.common.isRestrictedByFCA import com.tangem.utils.extensions.orZero @@ -20,63 +23,162 @@ internal class SwapAmountSelectQuoteTransformer( private val secondaryMaximumAmountBoundary: EnterAmountBoundary?, private val secondaryMinimumAmountBoundary: EnterAmountBoundary?, private val isNeedApplyFCARestrictions: Boolean, + private val isBalanceHidden: Boolean, + private val primaryMaximumAmountBoundary: EnterAmountBoundary? = null, + private val primaryMinimumAmountBoundary: EnterAmountBoundary? = null, ) : Transformer { + + @Suppress("CyclomaticComplexMethod", "LongMethod") override fun transform(prevState: SwapAmountUM): SwapAmountUM { if (prevState !is SwapAmountUM.Content) return prevState - val providerErrorConverter = SwapAmountErrorConverter( + val isPrimarySelected = prevState.selectedAmountType == SwapAmountType.From + val isSecondarySelected = prevState.selectedAmountType == SwapAmountType.To + + val primaryProviderErrorConverter = SwapAmountErrorConverter( cryptoCurrency = prevState.primaryCryptoCurrencyStatus.currency, ) + val secondaryProviderErrorConverter = prevState.secondaryCryptoCurrencyStatus?.let { + SwapAmountErrorConverter(cryptoCurrency = it.currency) + } - return prevState.copy( - isPrimaryButtonEnabled = quoteUM is SwapQuoteUM.Content, - selectedQuote = quoteUM, - isShowFCAWarning = isNeedApplyFCARestrictions && quoteUM.provider?.isRestrictedByFCA() == true, - primaryAmount = if (prevState.selectedAmountType == SwapAmountType.From) { - val swapAmountField = prevState.primaryAmount as? SwapAmountFieldUM.Content - val amountField = swapAmountField?.amountField as? AmountState.Data + val quoteContent = quoteUM as? SwapQuoteUM.Content + val fromAmount = quoteContent?.fromAmount + val toAmount = quoteContent?.toAmount - val amountError = (quoteUM as? SwapQuoteUM.Error)?.expressError?.let(providerErrorConverter::convert) + val primarySwapAmountField = prevState.primaryAmount as? SwapAmountFieldUM.Content + val secondarySwapAmountField = prevState.secondaryAmount as? SwapAmountFieldUM.Content - swapAmountField?.copy( + val subtitleConverter = SwapAmountUpdateSubtitleConverter( + selectedAmountType = prevState.selectedAmountType, + isBalanceHidden = isBalanceHidden, + ) + + val newPrimaryAmount = when { + fromAmount != null && primaryMaximumAmountBoundary != null -> { + primarySwapAmountField?.let { fromField -> + subtitleConverter.updateSubtitles( + field = fromField, + cryptoCurrencyStatus = prevState.primaryCryptoCurrencyStatus, + isAmountEmpty = false, + displayAmount = fromAmount, + ).copy( + amountField = AmountFieldChangeTransformer( + cryptoCurrencyStatus = prevState.primaryCryptoCurrencyStatus, + maxEnterAmount = primaryMaximumAmountBoundary, + minimumTransactionAmount = primaryMinimumAmountBoundary, + value = fromAmount.parseBigDecimal( + prevState.primaryCryptoCurrencyStatus.currency.decimals, + ), + ).transform(fromField.amountField), + ) + } ?: prevState.primaryAmount + } + isPrimarySelected -> { + val amountField = primarySwapAmountField?.amountField as? AmountState.Data + val amountError = (quoteUM as? SwapQuoteUM.Error)?.expressError + ?.let(primaryProviderErrorConverter::convert) + primarySwapAmountField?.copy( amountField = amountField?.copy( amountTextField = amountField.amountTextField.copy( error = amountError ?: TextReference.EMPTY, isError = amountError != null, ), - ) ?: swapAmountField.amountField, + ) ?: primarySwapAmountField.amountField, ) ?: prevState.primaryAmount - } else { - prevState.primaryAmount - }, - secondaryAmount = if (prevState.selectedAmountType == SwapAmountType.From && - prevState.secondaryCryptoCurrencyStatus != null && secondaryMaximumAmountBoundary != null - ) { - val secondaryAmountField = prevState.secondaryAmount as? SwapAmountFieldUM.Content - val fromAmount = (prevState.primaryAmount.amountField as? AmountState.Data) - ?.amountTextField?.cryptoAmount?.value.orZero() - val toAmount = (quoteUM as? SwapQuoteUM.Content)?.quoteAmount - val priceImpact = calculatePriceImpact( - swapDirection = prevState.swapDirection, - fromTokenAmount = fromAmount, - toTokenAmount = toAmount.orZero(), - primaryCryptoCurrencyStatus = prevState.primaryCryptoCurrencyStatus, - secondaryCryptoCurrencyStatus = prevState.secondaryCryptoCurrencyStatus, - ) - - secondaryAmountField?.copy( - priceImpact = priceImpact, + } + !isPrimarySelected && primarySwapAmountField != null && primaryMaximumAmountBoundary != null -> { + subtitleConverter.updateSubtitles( + field = primarySwapAmountField, + cryptoCurrencyStatus = prevState.primaryCryptoCurrencyStatus, + isAmountEmpty = true, + ).copy( amountField = AmountFieldChangeTransformer( - cryptoCurrencyStatus = prevState.secondaryCryptoCurrencyStatus, - maxEnterAmount = secondaryMaximumAmountBoundary, - minimumTransactionAmount = secondaryMinimumAmountBoundary, - value = toAmount?.parseBigDecimal(prevState.secondaryCryptoCurrencyStatus.currency.decimals) - .orEmpty(), - ).transform(secondaryAmountField.amountField), - ) ?: prevState.secondaryAmount + cryptoCurrencyStatus = prevState.primaryCryptoCurrencyStatus, + maxEnterAmount = primaryMaximumAmountBoundary, + minimumTransactionAmount = primaryMinimumAmountBoundary, + value = "", + ).transform(primarySwapAmountField.amountField), + ) + } + else -> prevState.primaryAmount + } + + val newSecondaryAmount = if ( + prevState.secondaryCryptoCurrencyStatus != null && + secondaryMaximumAmountBoundary != null && + secondarySwapAmountField != null + ) { + val fromAmountForPriceImpact = fromAmount + ?: (prevState.primaryAmount.amountField as? AmountState.Data) + ?.amountTextField?.cryptoAmount?.value.orZero() + val priceImpact = calculatePriceImpact( + swapDirection = prevState.swapDirection, + fromTokenAmount = fromAmountForPriceImpact, + toTokenAmount = toAmount.orZero(), + primaryCryptoCurrencyStatus = prevState.primaryCryptoCurrencyStatus, + secondaryCryptoCurrencyStatus = prevState.secondaryCryptoCurrencyStatus, + ) + val isAmountEmpty = toAmount == null + val secondaryAmountError = if (isSecondarySelected) { + (quoteUM as? SwapQuoteUM.Error)?.expressError + ?.let { secondaryProviderErrorConverter?.convert(it) } } else { - prevState.secondaryAmount - }, + null + } + val transformedSecondaryAmountField = if (isSecondarySelected && toAmount == null) { + secondarySwapAmountField.amountField + } else { + AmountFieldChangeTransformer( + cryptoCurrencyStatus = prevState.secondaryCryptoCurrencyStatus, + maxEnterAmount = secondaryMaximumAmountBoundary, + minimumTransactionAmount = secondaryMinimumAmountBoundary, + value = toAmount?.parseBigDecimal(prevState.secondaryCryptoCurrencyStatus.currency.decimals) + .orEmpty(), + ).transform(secondarySwapAmountField.amountField) + } + val insufficientFundsError = if (isSecondarySelected && fromAmount != null) { + val primaryBalance = prevState.primaryCryptoCurrencyStatus.value.amount + if (primaryBalance != null && fromAmount > primaryBalance) { + resourceReference(R.string.swapping_insufficient_funds) + } else { + null + } + } else { + null + } + val effectiveSecondaryError = secondaryAmountError ?: insufficientFundsError + val secondaryAmountFieldWithError = if (isSecondarySelected) { + (transformedSecondaryAmountField as? AmountState.Data)?.let { data -> + data.copy( + amountTextField = data.amountTextField.copy( + error = effectiveSecondaryError ?: TextReference.EMPTY, + isError = effectiveSecondaryError != null, + ), + ) + } ?: transformedSecondaryAmountField + } else { + transformedSecondaryAmountField + } + subtitleConverter.updateSubtitles( + field = secondarySwapAmountField, + cryptoCurrencyStatus = prevState.secondaryCryptoCurrencyStatus, + isAmountEmpty = isAmountEmpty, + displayAmount = toAmount, + ).copy( + priceImpact = priceImpact, + amountField = secondaryAmountFieldWithError, + ) + } else { + prevState.secondaryAmount + } + + return prevState.copy( + isPrimaryButtonEnabled = quoteUM is SwapQuoteUM.Content, + selectedQuote = quoteUM, + isShowFCAWarning = isNeedApplyFCARestrictions && quoteUM.provider?.isRestrictedByFCA() == true, + primaryAmount = newPrimaryAmount, + secondaryAmount = newSecondaryAmount, ) } } \ No newline at end of file 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 cd19623928..9349099eb2 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 @@ -5,6 +5,7 @@ import com.tangem.core.ui.extensions.stringReference import com.tangem.core.ui.format.bigdecimal.format import com.tangem.core.ui.format.bigdecimal.percent import com.tangem.domain.express.models.ExpressError +import com.tangem.domain.swap.models.SwapAmountType import com.tangem.features.swap.v2.impl.amount.entity.SwapAmountUM import com.tangem.features.swap.v2.impl.common.entity.SwapQuoteUM import com.tangem.features.swap.v2.impl.common.entity.SwapQuoteUM.Content.DifferencePercent @@ -23,18 +24,29 @@ internal class SwapAmountSetQuotesTransformer( private val secondaryMinimumAmountBoundary: EnterAmountBoundary?, private val isSilentReload: Boolean, private val isNeedApplyFcaRestrictions: Boolean, + private val isBalanceHidden: Boolean, + 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 sortedQuotes = quotes.sortedWith(SwapQuotesComparator) - val bestQuote = findBestQuote(quotes) ?: SwapQuoteUM.Empty - val quotesWithDiff = getQuotesWithDiff(sortedQuotes, bestQuote, isSingleProvider) + 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 @@ -51,14 +63,30 @@ internal class SwapAmountSetQuotesTransformer( secondaryMinimumAmountBoundary = secondaryMinimumAmountBoundary, isNeedApplyFCARestrictions = isNeedApplyFcaRestrictions && selectedQuote.provider?.isRestrictedByFCA() == true, + isBalanceHidden = isBalanceHidden, + primaryMaximumAmountBoundary = primaryMaximumAmountBoundary, + primaryMinimumAmountBoundary = primaryMinimumAmountBoundary, ) val updatedState = selectQuoteTransformer.transform(prevState = prevState) if (updatedState !is SwapAmountUM.Content) return prevState - return updatedState.copy( - isPrimaryButtonEnabled = updatedState.isPrimaryButtonEnabled && quotesWithDiff.isNotEmpty(), - swapQuotes = getQuotesWithDiff(sortedQuotes, bestQuote, isSingleProvider), + val areAllQuotesErrors = quotes.all { it is SwapQuoteUM.Error } + val stateWithError = if (areAllQuotesErrors) { + SwapAmountErrorQuoteTransformer.transform(updatedState) + } else { + 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, + ), ) } @@ -66,54 +94,75 @@ internal class SwapAmountSetQuotesTransformer( sortedQuotes: List, bestQuote: SwapQuoteUM, isSingleProvider: Boolean, + selectedAmountType: SwapAmountType, ): ImmutableList { - return sortedQuotes.sortedWith(SwapQuotesComparator) - .map { quote -> - if (quote is SwapQuoteUM.Content && bestQuote is SwapQuoteUM.Content) { - if (quote.provider.providerId == bestQuote.provider.providerId) { - quote.copy( - diffPercent = DifferencePercent.Best, - isSingleProvider = isSingleProvider, - ) - } else { - // current / selected - 1 - val percent = quote.quoteAmount / bestQuote.quoteAmount - BigDecimal.ONE - quote.copy( - diffPercent = DifferencePercent.Diff( - isPositive = percent.isPositive(), - percent = stringReference( - if (percent.isPositive()) { - "${StringsSigns.PLUS}${percent.format { percent() }}" - } else { - "${StringsSigns.DASH_SIGN}${percent.format { percent() }}" - }, - ), - ), - ) - } + return sortedQuotes.map { quote -> + if (quote is SwapQuoteUM.Content && bestQuote is SwapQuoteUM.Content) { + if (quote.provider.providerId == bestQuote.provider.providerId) { + quote.copy( + diffPercent = DifferencePercent.Best, + isSingleProvider = isSingleProvider, + ) } else { - quote + val percent = if (selectedAmountType == SwapAmountType.To) { + // Fixed mode: best has lowest fromTokenAmount; compare as (best/current - 1) + val bestFrom = bestQuote.fromAmount + val quoteFrom = quote.fromAmount + if (bestFrom != null && quoteFrom != null && quoteFrom > BigDecimal.ZERO) { + bestFrom / quoteFrom - BigDecimal.ONE + } else { + BigDecimal.ZERO + } + } else { + // Float mode: best has highest toAmount; compare as (current/best - 1) + quote.toAmount / bestQuote.toAmount - BigDecimal.ONE + } + quote.copy( + diffPercent = DifferencePercent.Diff( + isPositive = percent.isPositive(), + percent = stringReference( + if (percent.isPositive()) { + "${StringsSigns.PLUS}${percent.format { percent() }}" + } else { + "${StringsSigns.DASH_SIGN}${percent.format { percent() }}" + }, + ), + ), + ) } - }.toPersistentList() + } else { + quote + } + }.toPersistentList() } - private fun findBestQuote(quotes: List): SwapQuoteUM? { - return quotes - .sortedWith(SwapQuotesComparator) - .firstOrNull() + private fun findBestQuote(quotes: List, comparator: Comparator): SwapQuoteUM? { + return quotes.sortedWith(comparator).firstOrNull() } - private object SwapQuotesComparator : Comparator { + private class SwapQuotesComparator(private val selectedAmountType: SwapAmountType) : Comparator { override fun compare(p0: SwapQuoteUM?, p1: SwapQuoteUM?): Int { return when { p0 is SwapQuoteUM.Content && p1 !is SwapQuoteUM.Content -> -1 p0 !is SwapQuoteUM.Content && p1 is SwapQuoteUM.Content -> 1 p0 is SwapQuoteUM.Error && p1 is SwapQuoteUM.Error -> compareErrorQuote(p0 = p0, p1 = p1) - p0 is SwapQuoteUM.Content && p1 is SwapQuoteUM.Content -> p1.quoteAmount.compareTo(p0.quoteAmount) + p0 is SwapQuoteUM.Content && p1 is SwapQuoteUM.Content -> compareContent(p0, p1) else -> 0 } } + private fun compareContent(p0: SwapQuoteUM.Content, p1: SwapQuoteUM.Content): Int { + return if (selectedAmountType == SwapAmountType.To) { + // Fixed mode: lower fromTokenAmount = better (ascending) + val f0 = p0.fromAmount ?: BigDecimal.ZERO + val f1 = p1.fromAmount ?: BigDecimal.ZERO + f0.compareTo(f1) + } else { + // Float mode: higher toAmount = better (descending) + p1.toAmount.compareTo(p0.toAmount) + } + } + private fun compareErrorQuote(p0: SwapQuoteUM.Error, p1: SwapQuoteUM.Error): Int = when { p0.expressError is ExpressError.AmountError && p1.expressError !is ExpressError.AmountError -> -1 p0.expressError !is ExpressError.AmountError && p1.expressError is ExpressError.AmountError -> 1 diff --git a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/ui/SwapAmountBlockContent.kt b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/ui/SwapAmountBlockContent.kt index b4df59fefc..d75eb733a2 100644 --- a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/ui/SwapAmountBlockContent.kt +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/ui/SwapAmountBlockContent.kt @@ -37,9 +37,9 @@ import com.tangem.core.ui.extensions.resourceReference import com.tangem.core.ui.extensions.stringResourceSafe import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreview -import com.tangem.domain.swap.models.SwapAmountType import com.tangem.features.swap.v2.impl.R import com.tangem.features.swap.v2.impl.amount.entity.SwapAmountFieldUM +import com.tangem.domain.swap.models.SwapAmountType import com.tangem.features.swap.v2.impl.amount.entity.SwapAmountUM import com.tangem.features.swap.v2.impl.amount.ui.preview.SwapAmountContentPreview import com.tangem.features.swap.v2.impl.chooseprovider.ui.SwapChooseProviderContent diff --git a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/ui/SwapAmountContent.kt b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/ui/SwapAmountContent.kt index b314e610b9..69d9cb7e19 100644 --- a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/ui/SwapAmountContent.kt +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/ui/SwapAmountContent.kt @@ -4,6 +4,7 @@ import android.content.res.Configuration import androidx.compose.animation.AnimatedContent import androidx.compose.animation.AnimatedVisibility import androidx.compose.foundation.background +import androidx.compose.foundation.border import androidx.compose.foundation.clickable import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.layout.* @@ -39,9 +40,9 @@ import com.tangem.core.ui.extensions.stringResourceSafe import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreview import com.tangem.domain.express.models.ExpressRateType -import com.tangem.domain.swap.models.SwapAmountType import com.tangem.features.swap.v2.impl.R import com.tangem.features.swap.v2.impl.amount.entity.SwapAmountFieldUM +import com.tangem.domain.swap.models.SwapAmountType import com.tangem.features.swap.v2.impl.amount.entity.SwapAmountUM import com.tangem.features.swap.v2.impl.amount.model.SwapAmountClickIntents import com.tangem.features.swap.v2.impl.amount.ui.preview.SwapAmountClickIntentsStub @@ -156,10 +157,12 @@ private fun SwapAmountBlock( Box { SwapAmountEditBlock( amountFieldUM = amountFieldUM, + isFixedRate = isFixedRate, modifier = Modifier, onValueChange = clickIntents::onAmountValueChange, onValuePastedTriggerDismiss = clickIntents::onAmountPasteTriggerDismiss, onCurrencyChange = clickIntents::onCurrencyChangeClick, + onRateClick = clickIntents::onRateClick, ) HorizontalDivider( modifier = Modifier @@ -182,12 +185,15 @@ private fun SwapAmountBlock( } } +@Suppress("LongParameterList") @Composable private fun SwapAmountEditBlock( amountFieldUM: SwapAmountFieldUM, + isFixedRate: Boolean, onValueChange: (String) -> Unit, onValuePastedTriggerDismiss: () -> Unit, onCurrencyChange: (Boolean) -> Unit, + onRateClick: () -> Unit, modifier: Modifier = Modifier, ) { Column( @@ -209,8 +215,44 @@ private fun SwapAmountEditBlock( onValueChange = onValueChange, onValuePastedTriggerDismiss = onValuePastedTriggerDismiss, onCurrencyChange = onCurrencyChange, + reserveSpaceForError = false, modifier = Modifier, ) + SwapRateBadge( + isFixedRate = isFixedRate, + onClick = onRateClick, + ) + } +} + +@Composable +private fun SwapRateBadge(isFixedRate: Boolean, onClick: () -> Unit, modifier: Modifier = Modifier) { + val iconRes = if (isFixedRate) R.drawable.ic_fixed else R.drawable.ic_floating + val textRes = if (isFixedRate) R.string.send_rate_is_fixed else R.string.send_rate_floating_info_title + Row( + modifier = modifier + .clip(RoundedCornerShape(6.dp)) + .clickable(onClick = onClick) + .border( + width = 1.dp, + color = TangemTheme.colors.stroke.primary, + shape = RoundedCornerShape(6.dp), + ) + .padding(vertical = 2.dp, horizontal = 4.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(4.dp), + ) { + Icon( + painter = rememberVectorPainter(ImageVector.vectorResource(id = iconRes)), + contentDescription = null, + tint = TangemTheme.colors.icon.informative, + modifier = Modifier.padding(start = TangemTheme.dimens.spacing2), + ) + Text( + text = stringResourceSafe(textRes), + style = TangemTheme.typography.caption2, + color = TangemTheme.colors.text.tertiary, + ) } } @@ -227,6 +269,7 @@ private fun SwapAmountInfo( modifier: Modifier = Modifier, ) { val tokenIconState = (amountFieldUM.amountField as? AmountState.Data)?.tokenIconState ?: CurrencyIconState.Loading + val isEnabled = (amountFieldUM as? SwapAmountFieldUM.Content)?.isClickEnabled == true Row( verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(12.dp), @@ -234,11 +277,11 @@ private fun SwapAmountInfo( .clickable( interactionSource = remember { MutableInteractionSource() }, indication = ripple(), - enabled = (amountFieldUM as? SwapAmountFieldUM.Content)?.isClickEnabled == true, + enabled = isEnabled, onClick = { - if (isFixedRate) { + if (!isSelectedAmountType) { onExpandEditField() - } else { + } else if (amountFieldUM.amountType == SwapAmountType.To) { onSelectTokenClick() } }, @@ -256,15 +299,14 @@ private fun SwapAmountInfo( SwapAmountInfoMain( amountFieldUM = amountFieldUM, selectedQuote = selectedQuote, - isSelectedAmountType = isSelectedAmountType, modifier = Modifier.weight(1f), ) AnimatedContent( targetState = isSelectedAmountType, ) { isSelected -> - if (isSelected) { + if (isSelected && amountFieldUM.amountType == SwapAmountType.From) { AmountMaxButton(onMaxAmountClick) - } else { + } else if (isSelected && amountFieldUM.amountType == SwapAmountType.To) { SwapAmountInfoQuote( quoteUM = selectedQuote, isFixedRate = isFixedRate, @@ -279,7 +321,6 @@ private fun SwapAmountInfo( private fun SwapAmountInfoMain( amountFieldUM: SwapAmountFieldUM, selectedQuote: SwapQuoteUM?, - isSelectedAmountType: Boolean, modifier: Modifier = Modifier, ) { Column( @@ -300,48 +341,52 @@ private fun SwapAmountInfoMain( ) } } - AnimatedContent(isSelectedAmountType) { isSelected -> - if (isSelected && amountFieldUM is SwapAmountFieldUM.Content) { - SpacerH2() - Row { - EllipsisText( - text = amountFieldUM.subtitleLeft.resolveReference(), - style = TangemTheme.typography.caption2, - color = TangemTheme.colors.text.tertiary, - ellipsis = amountFieldUM.subtitleEllipsisLeft, - modifier = Modifier.weight(1f, fill = false), - ) - EllipsisText( - text = amountFieldUM.subtitleRight.resolveReference(), - style = TangemTheme.typography.caption2, - color = TangemTheme.colors.text.tertiary, - ellipsis = amountFieldUM.subtitleEllipsisRight, - ) - } - } else { - AnimatedContent(selectedQuote) { quote -> - when (quote) { - is SwapQuoteUM.Content -> { - SpacerH2() + + AnimatedContent(selectedQuote) { quote -> + when (quote) { + is SwapQuoteUM.Content -> { + SpacerH2() + if (amountFieldUM is SwapAmountFieldUM.Content) { + SpacerH2() + Row( + horizontalArrangement = Arrangement.spacedBy(4.dp), + ) { EllipsisText( - text = stringResourceSafe( - R.string.send_with_swap_recipient_get_amount, - quote.quoteAmountValue.resolveReference(), - ), + text = amountFieldUM.subtitleLeft.resolveReference(), style = TangemTheme.typography.caption2, color = TangemTheme.colors.text.tertiary, + ellipsis = amountFieldUM.subtitleEllipsisLeft, + modifier = Modifier.weight(1f, fill = false), ) - } - SwapQuoteUM.Loading -> { - SpacerH2() - TextShimmer( + EllipsisText( + text = amountFieldUM.subtitleRight.resolveReference(), style = TangemTheme.typography.caption2, - modifier = Modifier.width(72.dp), + color = TangemTheme.colors.text.tertiary, + ellipsis = amountFieldUM.subtitleEllipsisRight, ) } - else -> Unit + } else { + SpacerH2() + TextShimmer( + style = TangemTheme.typography.caption2, + modifier = Modifier.width(72.dp), + ) } } + SwapQuoteUM.Loading -> { + SpacerH2() + TextShimmer( + style = TangemTheme.typography.caption2, + modifier = Modifier.width(72.dp), + ) + } + else -> { + SpacerH2() + Text( + text = "", + style = TangemTheme.typography.caption2, + ) + } } } } diff --git a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/ui/preview/SwapAmountClickIntentsStub.kt b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/ui/preview/SwapAmountClickIntentsStub.kt index 4a7380480b..351e841fba 100644 --- a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/ui/preview/SwapAmountClickIntentsStub.kt +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/ui/preview/SwapAmountClickIntentsStub.kt @@ -10,6 +10,8 @@ internal object SwapAmountClickIntentsStub : SwapAmountClickIntents { override fun onSelectTokenClick() {} + override fun onRateClick() {} + override fun onSeparatorClick() {} override fun onProviderClick() {} diff --git a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/ui/preview/SwapAmountContentPreview.kt b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/ui/preview/SwapAmountContentPreview.kt index b4ebcff7cb..6f354c847a 100644 --- a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/ui/preview/SwapAmountContentPreview.kt +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/ui/preview/SwapAmountContentPreview.kt @@ -12,10 +12,11 @@ import com.tangem.domain.express.models.ExpressRateType import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.models.currency.CryptoCurrencyStatus import com.tangem.domain.models.network.Network -import com.tangem.domain.swap.models.SwapAmountType import com.tangem.domain.swap.models.SwapCurrencies import com.tangem.domain.swap.models.SwapDirection +import com.tangem.domain.swap.models.SwapRateMode import com.tangem.features.swap.v2.impl.amount.entity.SwapAmountFieldUM +import com.tangem.domain.swap.models.SwapAmountType import com.tangem.features.swap.v2.impl.amount.entity.SwapAmountUM import com.tangem.features.swap.v2.impl.common.entity.SwapQuoteUM import com.tangem.utils.StringsSigns @@ -66,8 +67,10 @@ internal data object SwapAmountContentPreview { private val quote = SwapQuoteUM.Content( provider = provider, - quoteAmount = "123".toBigDecimal(), - quoteAmountValue = stringReference("123"), + toAmount = "123".toBigDecimal(), + fromAmount = null, + toAmountValue = stringReference("123"), + fromAmountValue = TextReference.EMPTY, rate = stringReference("1 USD ≈ 123.123 POL"), diffPercent = SwapQuoteUM.Content.DifferencePercent.Best, isSingleProvider = false, @@ -92,6 +95,7 @@ internal data object SwapAmountContentPreview { appCurrency = AppCurrency.Default, isShowBestRateAnimation = false, isShowFCAWarning = false, + swapRateMode = SwapRateMode.FLOAT_ONLY, ) val defaultState = SwapAmountUM.Content( @@ -127,10 +131,11 @@ internal data object SwapAmountContentPreview { selectedQuote = quote, primaryCryptoCurrencyStatus = cryptoCurrencyStatus, secondaryCryptoCurrencyStatus = cryptoCurrencyStatus, - swapRateType = ExpressRateType.Float, + swapRateType = ExpressRateType.Fixed, isPrimaryButtonEnabled = true, isShowBestRateAnimation = false, isShowFCAWarning = true, + swapRateMode = SwapRateMode.FLOAT_AND_FIXED, ) val defaultStateAccount: SwapAmountUM.Content diff --git a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/chooseprovider/model/converter/SwapProviderListItemConverter.kt b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/chooseprovider/model/converter/SwapProviderListItemConverter.kt index 56c7a0f93a..7383f568e4 100644 --- a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/chooseprovider/model/converter/SwapProviderListItemConverter.kt +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/chooseprovider/model/converter/SwapProviderListItemConverter.kt @@ -41,7 +41,7 @@ internal class SwapProviderListItemConverter( title = stringReference(provider.name), subtitle = stringReference(provider.type.typeName), infoText = when (value) { - is SwapQuoteUM.Content -> value.quoteAmountValue + is SwapQuoteUM.Content -> value.toAmountValue else -> TextReference.EMPTY }, iconUrl = provider.imageLarge, diff --git a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/chooseprovider/model/converter/SwapProviderStateConverter.kt b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/chooseprovider/model/converter/SwapProviderStateConverter.kt index 54f3030f4f..a591206770 100644 --- a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/chooseprovider/model/converter/SwapProviderStateConverter.kt +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/chooseprovider/model/converter/SwapProviderStateConverter.kt @@ -50,7 +50,7 @@ internal class SwapProviderStateConverter( name = provider.name, iconUrl = provider.imageLarge, type = provider.type.typeName, - subtitle = quoteAmountValue, + subtitle = toAmountValue, additionalBadge = additionalBadge, diffPercent = diffPercent, isSelected = provider == selectedProvider, diff --git a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/chooseprovider/ui/preview/SwapChooseProviderContentPreview.kt b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/chooseprovider/ui/preview/SwapChooseProviderContentPreview.kt index 238cf49cb3..4765401f15 100644 --- a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/chooseprovider/ui/preview/SwapChooseProviderContentPreview.kt +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/chooseprovider/ui/preview/SwapChooseProviderContentPreview.kt @@ -35,8 +35,10 @@ internal object SwapChooseProviderContentPreview { private val quote1 = SwapQuoteUM.Content( provider = provider1, - quoteAmount = "123".toBigDecimal(), - quoteAmountValue = stringReference("123"), + toAmount = "123".toBigDecimal(), + fromAmount = null, + toAmountValue = stringReference("123"), + fromAmountValue = stringReference(""), rate = stringReference("1 USD ≈ 123.123 POL"), diffPercent = SwapQuoteUM.Content.DifferencePercent.Best, isSingleProvider = false, @@ -44,8 +46,10 @@ internal object SwapChooseProviderContentPreview { private val quote2 = SwapQuoteUM.Content( provider = provider2, - quoteAmount = "13.12".toBigDecimal(), - quoteAmountValue = stringReference("13.12"), + toAmount = "13.12".toBigDecimal(), + fromAmount = null, + toAmountValue = stringReference("13.12"), + fromAmountValue = stringReference(""), rate = stringReference("1 USD ≈ 12.123 POL"), diffPercent = SwapQuoteUM.Content.DifferencePercent.Empty, isSingleProvider = false, diff --git a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/choosetoken/fromSupported/ui/SwapChooseTokenNetworkContent.kt b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/choosetoken/fromSupported/ui/SwapChooseTokenNetworkContent.kt index b4466b11a5..9a6c3072c5 100644 --- a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/choosetoken/fromSupported/ui/SwapChooseTokenNetworkContent.kt +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/choosetoken/fromSupported/ui/SwapChooseTokenNetworkContent.kt @@ -6,11 +6,9 @@ import androidx.compose.animation.fadeIn import androidx.compose.animation.fadeOut import androidx.compose.animation.togetherWith import androidx.compose.foundation.Image -import androidx.compose.foundation.border import androidx.compose.foundation.clickable import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.layout.* -import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.Text import androidx.compose.material3.ripple @@ -25,7 +23,6 @@ import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.tooling.preview.PreviewParameterProvider import androidx.compose.ui.unit.dp import androidx.compose.ui.util.fastForEachIndexed -import com.tangem.core.ui.components.SpacerWMax import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig import com.tangem.core.ui.components.bottomsheets.message.* import com.tangem.core.ui.components.bottomsheets.modal.TangemModalBottomSheet @@ -141,21 +138,6 @@ private fun SwapChooseTokenNetworkContentList(swapNetworks: ImmutableList Unit): TextReference { +private fun SendWithSwapFooter( + confirmUM: ConfirmUM, + stackState: ChildStack, + primaryButton: NavigationButton, + onLinkClick: (String) -> Unit, +) { + Column { + AnimatedVisibility( + visible = stackState.active.configuration == SendWithSwapRoute.Confirm, + enter = slideInVertically(initialOffsetY = { it / 2 }) + fadeIn(), + exit = slideOutVertically(targetOffsetY = { it / 2 }) + fadeOut(), + ) { + val confirmContentUM = confirmUM as? ConfirmUM.Content + val sendFooter = confirmContentUM?.sendingFooter ?: TextReference.EMPTY + val legalFooter = getAnnotatedStringForLegals( + tosUM = confirmContentUM?.tosUM, + sendFooter = sendFooter, + onClick = onLinkClick, + ) + val footerText = remember(sendFooter, legalFooter) { + if (sendFooter != TextReference.EMPTY || legalFooter != TextReference.EMPTY) { + combinedReference(sendFooter, legalFooter) + } else { + TextReference.EMPTY + } + } + SendingText(footerText = footerText) + } + NavigationPrimaryButton( + primaryButton = primaryButton, + modifier = Modifier.padding( + start = 16.dp, + end = 16.dp, + bottom = 16.dp, + ), + ) + } +} + +@Composable +private fun getAnnotatedStringForLegals( + tosUM: ConfirmUM.Content.TosUM?, + sendFooter: TextReference, + onClick: (String) -> Unit, +): TextReference { if (tosUM == null) return TextReference.EMPTY val tos = tosUM.tosLink val policy = tosUM.policyLink @@ -118,7 +144,9 @@ private fun getAnnotatedStringForLegals(tosUM: ConfirmUM.Content.TosUM?, onClick val policyIndex = fullString.indexOf(policyTitle) annotatedReference { - append(StringsSigns.POINT_SIGN) + if (!sendFooter.resolveReference().endsWith(StringsSigns.POINT_SIGN)) { + append(StringsSigns.POINT_SIGN) + } appendSpace() append(fullString.substring(0, tosIndex)) withLink(