Updated on 2026-08-14
This commit is contained in:
parent
b4ae0c1547
commit
e43f438c1a
32 changed files with 971 additions and 331 deletions
|
|
@ -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),
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
|||
9
core/ui/src/main/res/drawable/ic_fixed.xml
Normal file
9
core/ui/src/main/res/drawable/ic_fixed.xml
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="10dp"
|
||||
android:height="10dp"
|
||||
android:viewportWidth="10"
|
||||
android:viewportHeight="10">
|
||||
<path
|
||||
android:pathData="M7.834,5.222C7.834,4.946 7.61,4.722 7.334,4.722H2.667C2.391,4.722 2.167,4.946 2.167,5.222V7.834C2.167,8.11 2.391,8.334 2.667,8.334H7.334C7.61,8.333 7.834,8.11 7.834,7.834V5.222ZM6.444,3.111C6.444,2.313 5.798,1.667 5,1.667C4.202,1.667 3.556,2.313 3.556,3.111V3.722H6.444V3.111ZM7.444,3.727C8.221,3.784 8.834,4.431 8.834,5.222V7.834C8.834,8.662 8.162,9.333 7.334,9.334H2.667C1.839,9.334 1.167,8.662 1.167,7.834V5.222C1.167,4.431 1.779,3.784 2.556,3.727V3.111C2.556,1.761 3.65,0.667 5,0.667C6.35,0.667 7.444,1.761 7.444,3.111V3.727Z"
|
||||
android:fillColor="#919191"/>
|
||||
</vector>
|
||||
13
core/ui/src/main/res/drawable/ic_floating.xml
Normal file
13
core/ui/src/main/res/drawable/ic_floating.xml
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="10dp"
|
||||
android:height="11dp"
|
||||
android:viewportWidth="10"
|
||||
android:viewportHeight="11">
|
||||
<path
|
||||
android:pathData="M3.639,4.008C4.256,3.907 4.849,4.154 5.411,4.716C5.825,5.129 6.112,5.177 6.314,5.144C6.562,5.103 6.848,4.92 7.198,4.569C7.393,4.374 7.71,4.374 7.905,4.569C8.1,4.765 8.1,5.081 7.905,5.276C7.522,5.659 7.048,6.037 6.476,6.131C5.859,6.231 5.266,5.984 4.704,5.423C4.29,5.009 4.003,4.962 3.8,4.995C3.552,5.036 3.267,5.219 2.916,5.569C2.721,5.764 2.404,5.764 2.209,5.569C2.014,5.374 2.014,5.057 2.209,4.862C2.592,4.479 3.066,4.101 3.639,4.008Z"
|
||||
android:fillColor="#919191"/>
|
||||
<path
|
||||
android:pathData="M5,0C7.761,0 10,2.239 10,5C10,7.761 7.761,10 5,10C2.239,10 0,7.761 0,5C0,2.239 2.239,0 5,0ZM5,1C2.791,1 1,2.791 1,5C1,7.209 2.791,9 5,9C7.209,9 9,7.209 9,5C9,2.791 7.209,1 5,1Z"
|
||||
android:fillColor="#919191"
|
||||
android:fillType="evenOdd"/>
|
||||
</vector>
|
||||
|
|
@ -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<String, ExpressProvider>,
|
||||
): 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
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ internal interface SwapAmountClickIntents : AmountScreenClickIntents {
|
|||
fun onExpandEditField(selectedAmountType: SwapAmountType)
|
||||
fun onInfoClick()
|
||||
fun onSelectTokenClick()
|
||||
fun onRateClick()
|
||||
fun onSeparatorClick()
|
||||
fun onProviderClick()
|
||||
}
|
||||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -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),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -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,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
|
|
@ -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),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -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<SwapAmountUM> {
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
|
@ -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<SwapAmountUM> {
|
||||
|
||||
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,
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -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<ExpressProvider>,
|
||||
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,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -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<SwapAmountUM> {
|
||||
|
||||
@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,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -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<SwapAmountUM> {
|
||||
override fun transform(prevState: SwapAmountUM): SwapAmountUM {
|
||||
if (prevState !is SwapAmountUM.Content) return prevState
|
||||
|
||||
val selectedAmountType = prevState.selectedAmountType
|
||||
val comparator = SwapQuotesComparator(selectedAmountType)
|
||||
|
||||
val isSingleProvider = quotes.filter { swapQuoteUM ->
|
||||
swapQuoteUM is SwapQuoteUM.Content || swapQuoteUM is SwapQuoteUM.Allowance ||
|
||||
(swapQuoteUM as? SwapQuoteUM.Error)?.expressError is ExpressError.AmountError
|
||||
}.isSingleItem()
|
||||
|
||||
val 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<SwapQuoteUM>,
|
||||
bestQuote: SwapQuoteUM,
|
||||
isSingleProvider: Boolean,
|
||||
selectedAmountType: SwapAmountType,
|
||||
): ImmutableList<SwapQuoteUM> {
|
||||
return sortedQuotes.sortedWith(SwapQuotesComparator)
|
||||
.map { quote ->
|
||||
if (quote is SwapQuoteUM.Content && bestQuote is SwapQuoteUM.Content) {
|
||||
if (quote.provider.providerId == bestQuote.provider.providerId) {
|
||||
quote.copy(
|
||||
diffPercent = DifferencePercent.Best,
|
||||
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>): SwapQuoteUM? {
|
||||
return quotes
|
||||
.sortedWith(SwapQuotesComparator)
|
||||
.firstOrNull()
|
||||
private fun findBestQuote(quotes: List<SwapQuoteUM>, comparator: Comparator<SwapQuoteUM>): SwapQuoteUM? {
|
||||
return quotes.sortedWith(comparator).firstOrNull()
|
||||
}
|
||||
|
||||
private object SwapQuotesComparator : Comparator<SwapQuoteUM> {
|
||||
private class SwapQuotesComparator(private val selectedAmountType: SwapAmountType) : Comparator<SwapQuoteUM> {
|
||||
override fun compare(p0: SwapQuoteUM?, p1: SwapQuoteUM?): Int {
|
||||
return when {
|
||||
p0 is SwapQuoteUM.Content && p1 !is SwapQuoteUM.Content -> -1
|
||||
p0 !is SwapQuoteUM.Content && p1 is SwapQuoteUM.Content -> 1
|
||||
p0 is SwapQuoteUM.Error && p1 is SwapQuoteUM.Error -> compareErrorQuote(p0 = p0, p1 = p1)
|
||||
p0 is SwapQuoteUM.Content && p1 is SwapQuoteUM.Content -> p1.quoteAmount.compareTo(p0.quoteAmount)
|
||||
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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,6 +10,8 @@ internal object SwapAmountClickIntentsStub : SwapAmountClickIntents {
|
|||
|
||||
override fun onSelectTokenClick() {}
|
||||
|
||||
override fun onRateClick() {}
|
||||
|
||||
override fun onSeparatorClick() {}
|
||||
|
||||
override fun onProviderClick() {}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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<SwapCh
|
|||
TangemTheme.colors.text.tertiary
|
||||
},
|
||||
)
|
||||
if (network.hasFixedRate) {
|
||||
SpacerWMax()
|
||||
Text(
|
||||
text = stringResourceSafe(R.string.swap_fixed_rate),
|
||||
style = TangemTheme.typography.body2,
|
||||
color = TangemTheme.colors.text.tertiary,
|
||||
modifier = Modifier
|
||||
.border(
|
||||
width = 1.dp,
|
||||
color = TangemTheme.colors.stroke.primary,
|
||||
shape = RoundedCornerShape(6.dp),
|
||||
)
|
||||
.padding(vertical = 2.dp, horizontal = 4.dp),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -31,8 +31,10 @@ internal sealed class SwapQuoteUM {
|
|||
|
||||
data class Content(
|
||||
override val provider: ExpressProvider,
|
||||
val quoteAmount: BigDecimal,
|
||||
val quoteAmountValue: TextReference,
|
||||
val toAmount: BigDecimal,
|
||||
val fromAmount: BigDecimal?,
|
||||
val toAmountValue: TextReference,
|
||||
val fromAmountValue: TextReference,
|
||||
val diffPercent: DifferencePercent,
|
||||
val isSingleProvider: Boolean,
|
||||
val rate: TextReference,
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import androidx.compose.foundation.background
|
|||
import androidx.compose.foundation.gestures.Orientation
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.text.LinkAnnotation
|
||||
|
|
@ -17,6 +18,7 @@ import com.arkivanov.decompose.extensions.compose.stack.animation.slide
|
|||
import com.arkivanov.decompose.extensions.compose.stack.animation.stackAnimation
|
||||
import com.arkivanov.decompose.router.stack.ChildStack
|
||||
import com.tangem.common.ui.footers.SendingText
|
||||
import com.tangem.common.ui.navigationButtons.NavigationButton
|
||||
import com.tangem.common.ui.navigationButtons.NavigationPrimaryButton
|
||||
import com.tangem.common.ui.navigationButtons.NavigationUM
|
||||
import com.tangem.core.ui.components.Fade
|
||||
|
|
@ -75,38 +77,62 @@ internal fun SendWithSwapContent(
|
|||
}
|
||||
}
|
||||
if (stackState.active.configuration != SendWithSwapRoute.Success) {
|
||||
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(confirmContentUM?.tosUM, onClick = onLinkClick)
|
||||
SendingText(
|
||||
footerText = if (sendFooter != TextReference.EMPTY || legalFooter != TextReference.EMPTY) {
|
||||
combinedReference(sendFooter, legalFooter)
|
||||
} else {
|
||||
TextReference.EMPTY
|
||||
},
|
||||
)
|
||||
}
|
||||
NavigationPrimaryButton(
|
||||
navigationUMContent.primaryButton,
|
||||
modifier = Modifier.padding(
|
||||
start = 16.dp,
|
||||
end = 16.dp,
|
||||
bottom = 16.dp,
|
||||
),
|
||||
)
|
||||
}
|
||||
SendWithSwapFooter(
|
||||
confirmUM = confirmUM,
|
||||
stackState = stackState,
|
||||
primaryButton = navigationUMContent.primaryButton,
|
||||
onLinkClick = onLinkClick,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun getAnnotatedStringForLegals(tosUM: ConfirmUM.Content.TosUM?, onClick: (String) -> Unit): TextReference {
|
||||
private fun SendWithSwapFooter(
|
||||
confirmUM: ConfirmUM,
|
||||
stackState: ChildStack<SendWithSwapRoute, ComposableContentComponent>,
|
||||
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(
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue