Updated on 2026-08-14
This commit is contained in:
commit
43c11d0460
384 changed files with 19228 additions and 3308 deletions
|
|
@ -1,5 +1,23 @@
|
|||
package com.tangem.feature.swap
|
||||
|
||||
import com.tangem.core.configtoggle.FeatureToggles
|
||||
import com.tangem.core.configtoggle.feature.FeatureTogglesManager
|
||||
import com.tangem.features.swap.SwapFeatureToggles
|
||||
import javax.inject.Inject
|
||||
|
||||
internal class DefaultSwapFeatureToggles : SwapFeatureToggles
|
||||
internal class DefaultSwapFeatureToggles @Inject constructor(
|
||||
featureTogglesManager: FeatureTogglesManager,
|
||||
) : SwapFeatureToggles {
|
||||
|
||||
override val isSwapSwitchToTransferEnabled: Boolean = featureTogglesManager.isFeatureEnabled(
|
||||
toggle = FeatureToggles.SWAP_SWITCH_TO_TRANSFER_ENABLED,
|
||||
)
|
||||
|
||||
override val isSwapIntegratedApproveEnabled: Boolean = featureTogglesManager.isFeatureEnabled(
|
||||
toggle = FeatureToggles.SWAP_INTEGRATED_APPROVE,
|
||||
)
|
||||
|
||||
override val isSwapAbEnabled: Boolean = featureTogglesManager.isFeatureEnabled(
|
||||
toggle = FeatureToggles.SWAP_AB_ENABLED,
|
||||
)
|
||||
}
|
||||
|
|
@ -0,0 +1,172 @@
|
|||
package com.tangem.feature.swap.converters
|
||||
|
||||
import com.tangem.common.ui.swap.SwapRateFormatter
|
||||
import com.tangem.core.ui.extensions.TextReference
|
||||
import com.tangem.core.ui.extensions.stringReference
|
||||
import com.tangem.core.ui.format.bigdecimal.crypto
|
||||
import com.tangem.core.ui.format.bigdecimal.format
|
||||
import com.tangem.feature.swap.domain.models.domain.SwapProvider
|
||||
import com.tangem.feature.swap.domain.models.ui.PermissionDataState
|
||||
import com.tangem.feature.swap.domain.models.ui.TokenSwapInfo
|
||||
import com.tangem.feature.swap.models.states.PercentDifference
|
||||
import com.tangem.feature.swap.models.states.ProviderState
|
||||
|
||||
/**
|
||||
* Builds [ProviderState.Content] for the swap provider list / row.
|
||||
*
|
||||
* Pure: takes everything it needs as parameters. Designed to be unit-tested in isolation.
|
||||
*/
|
||||
internal object SwapProviderStateBuilder {
|
||||
|
||||
private val FCA_RESTRICTED_PROVIDER_IDS = setOf(
|
||||
"changelly",
|
||||
"changenow",
|
||||
"okx-cross-chain",
|
||||
"okx-on-chain",
|
||||
"simpleswap",
|
||||
)
|
||||
|
||||
/**
|
||||
* Provider row on the main swap screen — shows the exchange rate `1 base ≈ rate quote`
|
||||
* (see [SwapRateFormatter]) and allows the user to open the provider picker.
|
||||
*/
|
||||
@Suppress("LongParameterList")
|
||||
fun buildContentClickable(
|
||||
provider: SwapProvider,
|
||||
fromTokenInfo: TokenSwapInfo,
|
||||
toTokenInfo: TokenSwapInfo,
|
||||
permissionState: PermissionDataState,
|
||||
selectionType: ProviderState.SelectionType,
|
||||
isBestRate: Boolean,
|
||||
isNeedBestRateBadge: Boolean,
|
||||
needApplyFCARestrictions: Boolean,
|
||||
onProviderClick: (String) -> Unit,
|
||||
): ProviderState.Content {
|
||||
val rateString = SwapRateFormatter.formatRate(
|
||||
from = fromTokenInfo.swapCurrencyStatus.currency,
|
||||
to = toTokenInfo.swapCurrencyStatus.currency,
|
||||
fromAmount = fromTokenInfo.tokenAmount.value,
|
||||
toAmount = toTokenInfo.tokenAmount.value,
|
||||
)
|
||||
return provider.toContent(
|
||||
subtitle = stringReference(rateString),
|
||||
additionalBadge = resolveBadge(
|
||||
provider = provider,
|
||||
needApplyFCARestrictions = needApplyFCARestrictions,
|
||||
permissionState = permissionState,
|
||||
isBestRate = isBestRate,
|
||||
isNeedBestRateBadge = isNeedBestRateBadge,
|
||||
),
|
||||
selectionType = selectionType,
|
||||
percentLowerThenBest = PercentDifference.Empty,
|
||||
onProviderClick = onProviderClick,
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Provider row in the provider-picker bottom sheet. Subtitle shows the formatted *to* amount
|
||||
* (not a rate) and the row carries a percentage delta vs. the best rate.
|
||||
*/
|
||||
@Suppress("LongParameterList")
|
||||
fun buildContentSelectable(
|
||||
provider: SwapProvider,
|
||||
toTokenInfo: TokenSwapInfo,
|
||||
permissionState: PermissionDataState,
|
||||
pricesLowerBest: Map<String, Float>,
|
||||
selectionType: ProviderState.SelectionType,
|
||||
needApplyFCARestrictions: Boolean,
|
||||
onProviderClick: (String) -> Unit,
|
||||
): ProviderState.Content {
|
||||
return provider.toContent(
|
||||
subtitle = buildSelectableSubtitle(toTokenInfo),
|
||||
additionalBadge = resolveBadge(
|
||||
provider = provider,
|
||||
needApplyFCARestrictions = needApplyFCARestrictions,
|
||||
permissionState = permissionState,
|
||||
),
|
||||
selectionType = selectionType,
|
||||
percentLowerThenBest = pricesLowerBest[provider.providerId]
|
||||
?.let(PercentDifference::Value)
|
||||
?: PercentDifference.Value(0f),
|
||||
onProviderClick = onProviderClick,
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Provider row for an unavailable / errored provider — subtitle is the error/alert text
|
||||
* resolved by the caller.
|
||||
*/
|
||||
fun buildAvailableFrom(
|
||||
provider: SwapProvider,
|
||||
alertText: TextReference,
|
||||
selectionType: ProviderState.SelectionType,
|
||||
needApplyFCARestrictions: Boolean,
|
||||
onProviderClick: (String) -> Unit,
|
||||
): ProviderState.Content {
|
||||
return provider.toContent(
|
||||
subtitle = alertText,
|
||||
additionalBadge = resolveBadge(
|
||||
provider = provider,
|
||||
needApplyFCARestrictions = needApplyFCARestrictions,
|
||||
),
|
||||
selectionType = selectionType,
|
||||
percentLowerThenBest = PercentDifference.Empty,
|
||||
onProviderClick = onProviderClick,
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Subtitle (formatted *to* amount) used both for picker rows and when refreshing
|
||||
* the provider-picker bottom sheet. Single source of truth so both paths stay in sync.
|
||||
*/
|
||||
fun buildSelectableSubtitle(toTokenInfo: TokenSwapInfo): TextReference {
|
||||
val toAmount = toTokenInfo.tokenAmount.value.format {
|
||||
crypto(toTokenInfo.swapCurrencyStatus.currency)
|
||||
}
|
||||
return stringReference(toAmount)
|
||||
}
|
||||
|
||||
private fun resolveBadge(
|
||||
provider: SwapProvider,
|
||||
needApplyFCARestrictions: Boolean,
|
||||
permissionState: PermissionDataState? = null,
|
||||
isBestRate: Boolean = false,
|
||||
isNeedBestRateBadge: Boolean = false,
|
||||
): ProviderState.AdditionalBadge {
|
||||
return when {
|
||||
needApplyFCARestrictions && provider.isFCARestricted() ->
|
||||
ProviderState.AdditionalBadge.FCAWarningList
|
||||
permissionState is PermissionDataState.PermissionRequired ->
|
||||
ProviderState.AdditionalBadge.PermissionRequired
|
||||
provider.isRecommended ->
|
||||
ProviderState.AdditionalBadge.Recommended
|
||||
isNeedBestRateBadge && isBestRate && !needApplyFCARestrictions ->
|
||||
ProviderState.AdditionalBadge.BestTrade
|
||||
else ->
|
||||
ProviderState.AdditionalBadge.Empty
|
||||
}
|
||||
}
|
||||
|
||||
private fun SwapProvider.toContent(
|
||||
subtitle: TextReference,
|
||||
additionalBadge: ProviderState.AdditionalBadge,
|
||||
selectionType: ProviderState.SelectionType,
|
||||
percentLowerThenBest: PercentDifference,
|
||||
onProviderClick: (String) -> Unit,
|
||||
): ProviderState.Content {
|
||||
return ProviderState.Content(
|
||||
id = providerId,
|
||||
name = name,
|
||||
iconUrl = imageLarge,
|
||||
type = type.providerName,
|
||||
subtitle = subtitle,
|
||||
additionalBadge = additionalBadge,
|
||||
selectionType = selectionType,
|
||||
percentLowerThenBest = percentLowerThenBest,
|
||||
namePrefix = ProviderState.PrefixType.NONE,
|
||||
onProviderClick = onProviderClick,
|
||||
)
|
||||
}
|
||||
|
||||
private fun SwapProvider.isFCARestricted(): Boolean = providerId in FCA_RESTRICTED_PROVIDER_IDS
|
||||
}
|
||||
|
|
@ -1,5 +1,6 @@
|
|||
package com.tangem.feature.swap.di
|
||||
|
||||
import com.tangem.core.configtoggle.feature.FeatureTogglesManager
|
||||
import com.tangem.feature.swap.DefaultSwapComponent
|
||||
import com.tangem.feature.swap.DefaultSwapFeatureToggles
|
||||
import com.tangem.features.swap.SwapComponent
|
||||
|
|
@ -17,8 +18,8 @@ internal object SwapFeatureModule {
|
|||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideSwapFeatureToggles(): SwapFeatureToggles {
|
||||
return DefaultSwapFeatureToggles()
|
||||
fun provideSwapFeatureToggles(featureTogglesManager: FeatureTogglesManager): SwapFeatureToggles {
|
||||
return DefaultSwapFeatureToggles(featureTogglesManager)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -130,6 +130,7 @@ internal class InitialCurrenciesResolver @Inject constructor(
|
|||
private fun getPaymentAccountCurrencies(accountStatus: AccountStatus.Payment): List<CryptoCurrencyStatus> {
|
||||
val paymentCryptoCurrencyStatus = when (val statusValue = accountStatus.value) {
|
||||
is PaymentAccountStatusValue.Loaded -> statusValue.cryptoCurrencyStatus
|
||||
is PaymentAccountStatusValue.Deactivated -> statusValue.cryptoCurrencyStatus
|
||||
else -> null
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -73,6 +73,8 @@ import com.tangem.feature.swap.analytics.SwapQuotePerformanceTracker
|
|||
import com.tangem.feature.swap.component.SwapFeeSelectorBlockComponent
|
||||
import com.tangem.feature.swap.converters.SwapTransactionErrorStateConverter
|
||||
import com.tangem.feature.swap.domain.AllowPermissionsHandler
|
||||
import com.tangem.feature.swap.domain.GetSwapUiModeUseCase
|
||||
import com.tangem.feature.swap.domain.SetSwapUiModeUseCase
|
||||
import com.tangem.feature.swap.domain.SwapInteractor
|
||||
import com.tangem.feature.swap.domain.TransactionFeeResult
|
||||
import com.tangem.feature.swap.domain.TxFeeSealedState
|
||||
|
|
@ -80,15 +82,16 @@ import com.tangem.feature.swap.domain.models.ExpressDataError
|
|||
import com.tangem.feature.swap.domain.models.SwapAmount
|
||||
import com.tangem.feature.swap.domain.models.domain.ExchangeProviderType
|
||||
import com.tangem.feature.swap.domain.models.domain.SwapDataModel
|
||||
import com.tangem.feature.swap.domain.models.domain.SwapPairLeast
|
||||
import com.tangem.feature.swap.domain.models.domain.SwapProvider
|
||||
import com.tangem.feature.swap.domain.models.domain.SwapUIMode
|
||||
import com.tangem.feature.swap.domain.models.ui.*
|
||||
import com.tangem.feature.swap.models.SwapAlertUM
|
||||
import com.tangem.feature.swap.models.SwapStateHolder
|
||||
import com.tangem.feature.swap.models.TokenSelectionDirection
|
||||
import com.tangem.feature.swap.models.UiActions
|
||||
import com.tangem.feature.swap.domain.transfer.SwapTransferInteractor
|
||||
import com.tangem.feature.swap.models.*
|
||||
import com.tangem.feature.swap.models.states.SwapNotificationUM
|
||||
import com.tangem.feature.swap.router.SwapRoute
|
||||
import com.tangem.feature.swap.ui.StateBuilder
|
||||
import com.tangem.feature.swap.ui.transfer.SwapTransferStateBuilder
|
||||
import com.tangem.feature.swap.utils.formatToUIRepresentation
|
||||
import com.tangem.feature.swap.utils.getContractAddress
|
||||
import com.tangem.features.approval.api.GiveApprovalComponent
|
||||
|
|
@ -98,6 +101,7 @@ import com.tangem.features.commonfeatures.api.choosetoken.ChooseTokenResult
|
|||
import com.tangem.features.send.v2.api.entity.FeeSelectorUM
|
||||
import com.tangem.features.send.v2.api.subcomponents.feeSelector.FeeSelectorReloadTrigger
|
||||
import com.tangem.features.swap.SwapComponent
|
||||
import com.tangem.features.swap.SwapFeatureToggles
|
||||
import com.tangem.utils.Provider
|
||||
import com.tangem.utils.coroutines.*
|
||||
import com.tangem.utils.isNullOrZero
|
||||
|
|
@ -140,17 +144,22 @@ internal class SwapModel @Inject constructor(
|
|||
private val shouldShowStoriesUseCase: ShouldShowStoriesUseCase,
|
||||
private val isAccountsModeEnabledUseCase: IsAccountsModeEnabledUseCase,
|
||||
private val swapInteractor: SwapInteractor,
|
||||
private val swapTransferInteractor: SwapTransferInteractor,
|
||||
private val swapTransferStateBuilder: SwapTransferStateBuilder,
|
||||
private val urlOpener: UrlOpener,
|
||||
private val getAccountCurrencyStatusUseCase: GetAccountCurrencyStatusUseCase,
|
||||
private val getPaymentAccountCryptoCurrencyStatusUseCase: GetPaymentAccountCryptoCurrencyStatusUseCase,
|
||||
private val tangemPayWithdrawUseCase: TangemPayWithdrawUseCase,
|
||||
private val iGaslessFeeSupportedForNetwork: IsGaslessFeeSupportedForNetwork,
|
||||
private val isGaslessFeeSupportedForNetwork: IsGaslessFeeSupportedForNetwork,
|
||||
private val feeSelectorReloadTrigger: FeeSelectorReloadTrigger,
|
||||
private val getTangemPayCustomerIdUseCase: GetTangemPayCustomerIdUseCase,
|
||||
private val appsFlyerStore: AppsFlyerStore,
|
||||
private val messageSender: UiMessageSender,
|
||||
private val initialCurrenciesResolver: InitialCurrenciesResolver,
|
||||
private val allowPermissionsHandler: AllowPermissionsHandler,
|
||||
private val swapFeatureToggles: SwapFeatureToggles,
|
||||
private val getSwapUiModeUseCase: GetSwapUiModeUseCase,
|
||||
private val setSwapUiModeUseCase: SetSwapUiModeUseCase,
|
||||
) : Model() {
|
||||
|
||||
private val params = paramsContainer.require<SwapComponent.Params>()
|
||||
|
|
@ -179,12 +188,14 @@ internal class SwapModel @Inject constructor(
|
|||
),
|
||||
)
|
||||
|
||||
private val actions = createUiActions()
|
||||
private val stateBuilder = StateBuilder(
|
||||
actions = createUiActions(),
|
||||
actions = actions,
|
||||
isBalanceHiddenProvider = Provider { isBalanceHidden },
|
||||
appCurrencyProvider = Provider(selectedAppCurrencyFlow::value),
|
||||
isAccountsModeProvider = Provider { isAccountsMode },
|
||||
iGaslessFeeSupportedForNetwork = iGaslessFeeSupportedForNetwork,
|
||||
isGaslessFeeSupportedForNetwork = isGaslessFeeSupportedForNetwork,
|
||||
shouldShowAbMenu = swapFeatureToggles.isSwapAbEnabled,
|
||||
)
|
||||
|
||||
private val inputNumberFormatter = InputNumberFormatter(
|
||||
|
|
@ -282,6 +293,10 @@ internal class SwapModel @Inject constructor(
|
|||
isBalanceHidden = settings.isBalanceHidden
|
||||
uiState = stateBuilder.updateBalanceHiddenState(uiState, isBalanceHidden)
|
||||
}.launchIn(modelScope)
|
||||
|
||||
modelScope.launch {
|
||||
uiState = uiState.copy(swapUIMode = getSwapUiModeUseCase())
|
||||
}
|
||||
}
|
||||
|
||||
fun onStart() {
|
||||
|
|
@ -557,6 +572,12 @@ internal class SwapModel @Inject constructor(
|
|||
toSwapCurrencyStatus = newToSwapCurrencyStatus,
|
||||
pairs = dataState.pairs,
|
||||
)
|
||||
val isUpdatedToTransferMode = isUpdatedToTransferMode(
|
||||
fromSwapCurrencyStatus = newFromSwapCurrencyStatus,
|
||||
toSwapCurrencyStatus = newToSwapCurrencyStatus,
|
||||
fromTokenAmount = lastAmount.value,
|
||||
)
|
||||
if (isUpdatedToTransferMode) return@launch
|
||||
if (toProvidersList.isEmpty()) {
|
||||
handleSwapNotSupported(
|
||||
fromSwapCurrencyStatus = newFromSwapCurrencyStatus,
|
||||
|
|
@ -576,6 +597,12 @@ internal class SwapModel @Inject constructor(
|
|||
}
|
||||
|
||||
private fun initSwapPairs(fromSwapCurrencyStatus: SwapCurrencyStatus, toSwapCurrencyStatus: SwapCurrencyStatus) {
|
||||
val isUpdatedToTransferMode = isUpdatedToTransferMode(
|
||||
fromSwapCurrencyStatus = fromSwapCurrencyStatus,
|
||||
toSwapCurrencyStatus = toSwapCurrencyStatus,
|
||||
fromTokenAmount = lastAmount.value,
|
||||
)
|
||||
if (isUpdatedToTransferMode) return
|
||||
modelScope.launch {
|
||||
uiState = stateBuilder.createInitialLoadingState(
|
||||
uiStateHolder = uiState,
|
||||
|
|
@ -612,32 +639,11 @@ internal class SwapModel @Inject constructor(
|
|||
toSwapCurrencyStatus = toSwapCurrencyStatus,
|
||||
)
|
||||
} else {
|
||||
uiState = stateBuilder.updateCurrenciesState(
|
||||
uiStateHolder = uiState,
|
||||
emptyAmountState = SwapState.EmptyAmountState(
|
||||
zeroAmountEquivalent = stringReference(
|
||||
BigDecimal.ZERO.format {
|
||||
fiat(
|
||||
fiatCurrencyCode = selectedAppCurrencyFlow.value.code,
|
||||
fiatCurrencySymbol = selectedAppCurrencyFlow.value.symbol,
|
||||
)
|
||||
},
|
||||
),
|
||||
),
|
||||
updateCurrenciesStateAndStartLoadingQuotes(
|
||||
fromSwapCurrencyStatus = fromSwapCurrencyStatus,
|
||||
toSwapCurrencyStatus = toSwapCurrencyStatus,
|
||||
shouldResetAmount = false,
|
||||
)
|
||||
dataState = dataState.copy(
|
||||
pairs = pairs,
|
||||
selectedPairProviders = providerList,
|
||||
)
|
||||
startLoadingQuotes(
|
||||
amount = lastAmount.value,
|
||||
reduceBalanceBy = lastReducedBalanceBy.value,
|
||||
fromSwapCurrencyStatus = fromSwapCurrencyStatus,
|
||||
toSwapCurrencyStatus = toSwapCurrencyStatus,
|
||||
toProvidersList = providerList,
|
||||
providerList = providerList,
|
||||
)
|
||||
}
|
||||
},
|
||||
|
|
@ -645,6 +651,81 @@ internal class SwapModel @Inject constructor(
|
|||
}.saveIn(swapPairsJobHolder)
|
||||
}
|
||||
|
||||
private fun updateCurrenciesStateAndStartLoadingQuotes(
|
||||
fromSwapCurrencyStatus: SwapCurrencyStatus,
|
||||
toSwapCurrencyStatus: SwapCurrencyStatus,
|
||||
pairs: List<SwapPairLeast>,
|
||||
providerList: List<SwapProvider>,
|
||||
) {
|
||||
uiState = stateBuilder.updateCurrenciesState(
|
||||
uiStateHolder = uiState,
|
||||
emptyAmountState = SwapState.EmptyAmountState(
|
||||
zeroAmountEquivalent = stringReference(
|
||||
BigDecimal.ZERO.format {
|
||||
fiat(
|
||||
fiatCurrencyCode = selectedAppCurrencyFlow.value.code,
|
||||
fiatCurrencySymbol = selectedAppCurrencyFlow.value.symbol,
|
||||
)
|
||||
},
|
||||
),
|
||||
),
|
||||
fromSwapCurrencyStatus = fromSwapCurrencyStatus,
|
||||
toSwapCurrencyStatus = toSwapCurrencyStatus,
|
||||
shouldResetAmount = false,
|
||||
)
|
||||
dataState = dataState.copy(
|
||||
pairs = pairs,
|
||||
selectedPairProviders = providerList,
|
||||
)
|
||||
startLoadingQuotes(
|
||||
amount = lastAmount.value,
|
||||
reduceBalanceBy = lastReducedBalanceBy.value,
|
||||
fromSwapCurrencyStatus = fromSwapCurrencyStatus,
|
||||
toSwapCurrencyStatus = toSwapCurrencyStatus,
|
||||
toProvidersList = providerList,
|
||||
)
|
||||
}
|
||||
|
||||
private fun isUpdatedToTransferMode(
|
||||
fromSwapCurrencyStatus: SwapCurrencyStatus,
|
||||
toSwapCurrencyStatus: SwapCurrencyStatus,
|
||||
fromTokenAmount: String,
|
||||
): Boolean {
|
||||
val shouldTransferInsteadOfSwap = swapTransferInteractor.shouldTransferInsteadOfSwap(
|
||||
fromSwapCurrencyStatus.currency,
|
||||
toSwapCurrencyStatus.currency,
|
||||
)
|
||||
if (shouldTransferInsteadOfSwap) {
|
||||
modelScope.launch {
|
||||
updateTransferUIState(fromSwapCurrencyStatus, toSwapCurrencyStatus, fromTokenAmount)
|
||||
}
|
||||
}
|
||||
return shouldTransferInsteadOfSwap
|
||||
}
|
||||
|
||||
private suspend fun updateTransferUIState(
|
||||
fromSwapCurrencyStatus: SwapCurrencyStatus,
|
||||
toSwapCurrencyStatus: SwapCurrencyStatus,
|
||||
fromTokenAmount: String,
|
||||
) {
|
||||
val swapState = swapTransferInteractor.updateTransfer(
|
||||
fromSwapCurrencyStatus,
|
||||
toSwapCurrencyStatus,
|
||||
fromTokenAmount,
|
||||
)
|
||||
when (swapState) {
|
||||
is SwapState.EmptyAmountState -> setupEmptyAmountUiState(swapState, fromSwapCurrencyStatus)
|
||||
is SwapState.Transfer -> {
|
||||
uiState = swapTransferStateBuilder.createTransferState(
|
||||
actions = actions,
|
||||
transferState = swapState,
|
||||
uiStateHolder = uiState,
|
||||
)
|
||||
}
|
||||
is SwapState.QuotesLoadedState, is SwapState.SwapError -> Unit
|
||||
}
|
||||
}
|
||||
|
||||
private fun retrySwapPairs(fromSwapCurrencyStatus: SwapCurrencyStatus, toSwapCurrencyStatus: SwapCurrencyStatus) {
|
||||
if (swapPairsJobHolder.isActive) return
|
||||
initSwapPairs(fromSwapCurrencyStatus, toSwapCurrencyStatus)
|
||||
|
|
@ -707,6 +788,12 @@ internal class SwapModel @Inject constructor(
|
|||
val toSwapCurrencyStatus = dataState.toSwapCurrencyStatus
|
||||
val amount = dataState.amount
|
||||
if (fromSwapCurrencyStatus != null && toSwapCurrencyStatus != null && amount != null) {
|
||||
val isUpdatedToTransferMode = isUpdatedToTransferMode(
|
||||
fromSwapCurrencyStatus = fromSwapCurrencyStatus,
|
||||
toSwapCurrencyStatus = toSwapCurrencyStatus,
|
||||
fromTokenAmount = lastAmount.value,
|
||||
)
|
||||
if (isUpdatedToTransferMode) return
|
||||
startLoadingQuotes(
|
||||
fromSwapCurrencyStatus = fromSwapCurrencyStatus,
|
||||
toSwapCurrencyStatus = toSwapCurrencyStatus,
|
||||
|
|
@ -826,6 +913,7 @@ internal class SwapModel @Inject constructor(
|
|||
sendAnalyticsForNotifications(provider, fromSwapCurrencyStatus.status, toSwapCurrencyStatus.status)
|
||||
updatePermissionNotificationState(state)
|
||||
}
|
||||
is SwapState.Transfer -> Unit
|
||||
is SwapState.EmptyAmountState -> {
|
||||
setupEmptyAmountUiState(state, fromSwapCurrencyStatus)
|
||||
lastPermissionNotificationTokens = null
|
||||
|
|
@ -1269,6 +1357,12 @@ internal class SwapModel @Inject constructor(
|
|||
)
|
||||
|
||||
if (toSwapCurrencyStatus != null) {
|
||||
val isUpdatedToTransferMode = isUpdatedToTransferMode(
|
||||
fromSwapCurrencyStatus = fromSwapCurrencyStatus,
|
||||
toSwapCurrencyStatus = toSwapCurrencyStatus,
|
||||
fromTokenAmount = lastAmount.value,
|
||||
)
|
||||
if (isUpdatedToTransferMode) return@launch
|
||||
if (toSwapCurrencyStatus.status.value.amount != null) {
|
||||
isAmountChangedByUser = true
|
||||
}
|
||||
|
|
@ -1422,6 +1516,9 @@ internal class SwapModel @Inject constructor(
|
|||
)
|
||||
}
|
||||
},
|
||||
onTransferClick = {
|
||||
// TODO: Will be implemented in [REDACTED_TASK_KEY]
|
||||
},
|
||||
onChangeCardsClicked = {
|
||||
onChangeCardsClicked()
|
||||
analyticsEventHandler.send(SwapEvents.ButtonSwipeClicked())
|
||||
|
|
@ -1528,9 +1625,16 @@ internal class SwapModel @Inject constructor(
|
|||
onSuccess = {
|
||||
router.replaceAll(SwapRoute.Success)
|
||||
},
|
||||
onSwapUIModeChange = ::onSwapUIModeChange,
|
||||
)
|
||||
}
|
||||
|
||||
private fun onSwapUIModeChange(mode: SwapUIMode) {
|
||||
if (uiState.swapUIMode == mode) return
|
||||
uiState = uiState.copy(swapUIMode = mode)
|
||||
modelScope.launch { setSwapUiModeUseCase(mode) }
|
||||
}
|
||||
|
||||
private fun selectWalletInSelector(
|
||||
fromSwapCurrencyStatus: SwapCurrencyStatus?,
|
||||
toSwapCurrencyStatus: SwapCurrencyStatus?,
|
||||
|
|
@ -1554,11 +1658,15 @@ internal class SwapModel @Inject constructor(
|
|||
} else {
|
||||
val fromSwapCurrencyStatus = dataState.fromSwapCurrencyStatus
|
||||
val toSwapCurrencyStatus = dataState.toSwapCurrencyStatus
|
||||
val shouldShowSameCoinsWithDifferentAddress = swapFeatureToggles.isSwapSwitchToTransferEnabled &&
|
||||
fromSwapCurrencyStatus?.account?.accountId != accountStatus.accountId &&
|
||||
fromSwapCurrencyStatus?.currency?.network?.rawId == toSwapCurrencyStatus?.currency?.network?.rawId
|
||||
|
||||
(fromSwapCurrencyStatus?.account?.accountId != accountStatus.accountId ||
|
||||
fromSwapCurrencyStatus.currency.id != currencyStatus.currency.id) &&
|
||||
(toSwapCurrencyStatus?.account?.accountId != accountStatus.accountId ||
|
||||
toSwapCurrencyStatus.currency.id != currencyStatus.currency.id)
|
||||
toSwapCurrencyStatus.currency.id != currencyStatus.currency.id) ||
|
||||
shouldShowSameCoinsWithDifferentAddress
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -1858,6 +1966,7 @@ internal class SwapModel @Inject constructor(
|
|||
override suspend fun loadFeeExtended(
|
||||
selectedToken: CryptoCurrencyStatus?,
|
||||
): Either<GetFeeError, TransactionFeeExtended> {
|
||||
// TODO use getFeeGaselessUsecase in transfer. Will be implemented in [REDACTED_TASK_KEY]
|
||||
val fromSwapCurrencyStatus =
|
||||
dataState.fromSwapCurrencyStatus ?: return Either.Left(GetFeeError.UnknownError)
|
||||
val selectedProvider = dataStateStateFlow.first { it.selectedProvider != null }.selectedProvider!!
|
||||
|
|
@ -1911,7 +2020,7 @@ internal class SwapModel @Inject constructor(
|
|||
uiState = uiState.copy(
|
||||
swapButton = uiState.swapButton.copy(
|
||||
isEnabled = false,
|
||||
isInProgress = false,
|
||||
mode = SwapButton.Mode.SWAP_PROGRESSING,
|
||||
),
|
||||
)
|
||||
modelScope.launch {
|
||||
|
|
@ -1930,7 +2039,7 @@ internal class SwapModel @Inject constructor(
|
|||
|
||||
override suspend fun loadFee(): Either<GetFeeError, TransactionFee> {
|
||||
TangemLogger.e("loadFee: Start loading fee")
|
||||
|
||||
// TODO use getFeeUsecase in transfer. Will be implemented in [REDACTED_TASK_KEY]
|
||||
val fromSwapCurrencyStatus =
|
||||
dataState.fromSwapCurrencyStatus ?: return Either.Left(GetFeeError.UnknownError)
|
||||
val toSwapCurrencyStatus =
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
package com.tangem.feature.swap.model
|
||||
|
||||
import com.tangem.common.TangemSiteUrlBuilder
|
||||
import com.tangem.common.ui.notifications.NotificationUM
|
||||
import com.tangem.common.ui.notifications.NotificationsFactory.addDustWarningNotification
|
||||
import com.tangem.common.ui.notifications.NotificationsFactory.addExistentialWarningNotification
|
||||
|
|
@ -35,7 +36,7 @@ import java.math.BigDecimal
|
|||
@Suppress("LargeClass")
|
||||
internal class SwapNotificationsFactory(
|
||||
private val actions: UiActions,
|
||||
private val iGaslessFeeSupportedForNetwork: IsGaslessFeeSupportedForNetwork,
|
||||
private val isGaslessFeeSupportedForNetwork: IsGaslessFeeSupportedForNetwork,
|
||||
) {
|
||||
|
||||
fun getGeneralErrorStateNotifications(
|
||||
|
|
@ -104,14 +105,13 @@ internal class SwapNotificationsFactory(
|
|||
quoteModel: SwapState.QuotesLoadedState,
|
||||
feeCryptoCurrencyStatus: CryptoCurrencyStatus?,
|
||||
selectedFeeType: FeeType,
|
||||
providerName: String,
|
||||
hideFee: Boolean,
|
||||
): ImmutableList<NotificationUM> {
|
||||
val warnings = buildList {
|
||||
maybeAddRentExemptionError(quoteModel)
|
||||
maybeAddDomainWarnings(quoteModel, feeCryptoCurrencyStatus, selectedFeeType)
|
||||
maybeAddNeedReserveToCreateAccountWarning(quoteModel)
|
||||
maybeAddPermissionNeededWarning(quoteModel, providerName)
|
||||
maybeAddPermissionNeededWarning(quoteModel)
|
||||
maybeAddNetworkFeeCoverageWarning(quoteModel, selectedFeeType)
|
||||
maybeAddUnableCoverFeeWarning(quoteModel, feeCryptoCurrencyStatus, hideFee)
|
||||
maybeAddTransactionInProgressWarning(quoteModel)
|
||||
|
|
@ -253,16 +253,12 @@ internal class SwapNotificationsFactory(
|
|||
}
|
||||
}
|
||||
|
||||
private fun MutableList<NotificationUM>.maybeAddPermissionNeededWarning(
|
||||
quoteModel: SwapState.QuotesLoadedState,
|
||||
providerName: String,
|
||||
) {
|
||||
private fun MutableList<NotificationUM>.maybeAddPermissionNeededWarning(quoteModel: SwapState.QuotesLoadedState) {
|
||||
if (quoteModel.permissionState is PermissionDataState.PermissionRequired) {
|
||||
add(
|
||||
SwapNotificationUM.Info.PermissionNeeded(
|
||||
providerName = providerName,
|
||||
fromTokenSymbol = quoteModel.fromTokenInfo.swapCurrencyStatus.currency.symbol,
|
||||
onApproveClick = actions.openPermissionBottomSheet,
|
||||
onLearnMoreClick = { actions.onLinkClick(TangemSiteUrlBuilder.HELP_CENTER_SWAP_URL) },
|
||||
),
|
||||
)
|
||||
}
|
||||
|
|
@ -316,8 +312,7 @@ internal class SwapNotificationsFactory(
|
|||
val isNotEnoughFee = feeEnoughState is SwapFeeState.NotEnough && !isCEXProvider ||
|
||||
quoteModel.preparedSwapConfigState.includeFeeInAmount is IncludeFeeInAmount.BalanceNotEnough
|
||||
|
||||
val isGaslessAvailable = iGaslessFeeSupportedForNetwork(fromCurrency.network) && isCEXProvider
|
||||
|
||||
val isGaslessAvailable = isGaslessFeeSupportedForNetwork(fromCurrency.network) && isCEXProvider
|
||||
if (shouldShowCoverWarning && !isGaslessAvailable || isNotEnoughFee) {
|
||||
add(
|
||||
SwapNotificationUM.Error.UnableToCoverFeeWarning(
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ import com.tangem.common.ui.notifications.NotificationUM
|
|||
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig
|
||||
import com.tangem.core.ui.components.currency.icon.CurrencyIconState
|
||||
import com.tangem.core.ui.extensions.TextReference
|
||||
import com.tangem.feature.swap.domain.models.domain.SwapUIMode
|
||||
import com.tangem.feature.swap.domain.models.ui.PriceImpact
|
||||
import com.tangem.feature.swap.models.states.FeeItemState
|
||||
import com.tangem.feature.swap.models.states.ProviderState
|
||||
|
|
@ -31,6 +32,8 @@ internal data class SwapStateHolder(
|
|||
val swapButton: SwapButton,
|
||||
val shouldShowMaxAmount: Boolean,
|
||||
val tosState: TosState? = null,
|
||||
val swapUIMode: SwapUIMode = SwapUIMode.Detailed,
|
||||
val shouldShowAbMenu: Boolean = false,
|
||||
|
||||
val onRefresh: () -> Unit,
|
||||
val onBackClicked: () -> Unit,
|
||||
|
|
@ -39,6 +42,7 @@ internal data class SwapStateHolder(
|
|||
val onSuccess: (() -> Unit),
|
||||
val onMaxAmountSelected: (() -> Unit)? = null,
|
||||
val onShowPermissionBottomSheet: () -> Unit = {},
|
||||
val onSwapUIModeChange: (SwapUIMode) -> Unit = {},
|
||||
)
|
||||
|
||||
@Immutable
|
||||
|
|
@ -70,10 +74,20 @@ sealed class SwapCardState {
|
|||
data class SwapButton(
|
||||
@DrawableRes val walletInteractionIcon: Int?,
|
||||
val isEnabled: Boolean,
|
||||
val isInProgress: Boolean = false,
|
||||
val mode: Mode = Mode.SWAP,
|
||||
val isHoldToConfirm: Boolean = false,
|
||||
val onClick: () -> Unit,
|
||||
)
|
||||
) {
|
||||
enum class Mode {
|
||||
SWAP_PROGRESSING,
|
||||
SWAP,
|
||||
TRANSFER,
|
||||
TRANSFER_PROGRESSING,
|
||||
}
|
||||
|
||||
val isInProgress
|
||||
get() = mode == Mode.SWAP_PROGRESSING || mode == Mode.TRANSFER_PROGRESSING
|
||||
}
|
||||
|
||||
@Immutable
|
||||
sealed interface TransactionCardType {
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
package com.tangem.feature.swap.models
|
||||
|
||||
import com.tangem.feature.swap.domain.models.SwapAmount
|
||||
import com.tangem.feature.swap.domain.models.domain.SwapUIMode
|
||||
import com.tangem.feature.swap.domain.models.ui.TxFee
|
||||
import java.math.BigDecimal
|
||||
|
||||
|
|
@ -8,6 +9,7 @@ internal data class UiActions(
|
|||
val onAmountChanged: (String) -> Unit,
|
||||
val onAmountSelected: (Boolean) -> Unit,
|
||||
val onSwapClick: () -> Unit,
|
||||
val onTransferClick: () -> Unit,
|
||||
val onChangeCardsClicked: () -> Unit,
|
||||
val onBackClicked: () -> Unit,
|
||||
val onMaxAmountSelected: () -> Unit,
|
||||
|
|
@ -25,4 +27,5 @@ internal data class UiActions(
|
|||
val onSuccess: () -> Unit,
|
||||
val onLinkClick: (String) -> Unit,
|
||||
val onReceiveCardWarningClick: () -> Unit,
|
||||
val onSwapUIModeChange: (SwapUIMode) -> Unit,
|
||||
)
|
||||
|
|
@ -5,8 +5,11 @@ import com.tangem.common.ui.extensions.networkIconResId
|
|||
import com.tangem.common.ui.notifications.NotificationUM
|
||||
import com.tangem.core.ui.components.notifications.NotificationConfig
|
||||
import com.tangem.core.ui.extensions.TextReference
|
||||
import com.tangem.core.ui.extensions.combinedReference
|
||||
import com.tangem.core.ui.extensions.resourceReference
|
||||
import com.tangem.core.ui.extensions.styledResourceReference
|
||||
import com.tangem.core.ui.extensions.wrappedList
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.domain.express.models.ExpressError
|
||||
import com.tangem.domain.models.currency.CryptoCurrency
|
||||
import com.tangem.feature.swap.utils.getExpressErrorMessage
|
||||
|
|
@ -217,14 +220,25 @@ internal object SwapNotificationUM {
|
|||
iconResId = iconResId,
|
||||
) {
|
||||
data class PermissionNeeded(
|
||||
val providerName: String,
|
||||
val fromTokenSymbol: String,
|
||||
val onApproveClick: () -> Unit,
|
||||
val onLearnMoreClick: () -> Unit,
|
||||
) : Info(
|
||||
title = resourceReference(R.string.express_provider_permission_needed),
|
||||
subtitle = resourceReference(
|
||||
id = R.string.give_permission_swap_subtitle,
|
||||
formatArgs = wrappedList(providerName, fromTokenSymbol),
|
||||
subtitle = combinedReference(
|
||||
resourceReference(
|
||||
id = R.string.give_permission_swap_subtitle_v2,
|
||||
// Arg is only used in iOS
|
||||
formatArgs = wrappedList(""),
|
||||
),
|
||||
styledResourceReference(
|
||||
id = R.string.common_learn_more,
|
||||
spanStyleReference = {
|
||||
TangemTheme.typography.caption2
|
||||
.copy(color = TangemTheme.colors.text.accent)
|
||||
.toSpanStyle()
|
||||
},
|
||||
onClick = onLearnMoreClick,
|
||||
),
|
||||
),
|
||||
iconResId = R.drawable.ic_locked_24,
|
||||
buttonsState = NotificationConfig.ButtonsState.SecondaryButtonConfig(
|
||||
|
|
|
|||
|
|
@ -0,0 +1,156 @@
|
|||
package com.tangem.feature.swap.ui
|
||||
|
||||
import android.content.res.Configuration
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
||||
import androidx.compose.ui.unit.dp
|
||||
import coil.compose.SubcomposeAsyncImage
|
||||
import coil.request.ImageRequest
|
||||
import com.tangem.core.ui.R
|
||||
import com.tangem.core.ui.components.RectangleShimmer
|
||||
import com.tangem.core.ui.components.SpacerW8
|
||||
import com.tangem.core.ui.components.SpacerWMax
|
||||
import com.tangem.core.ui.extensions.resolveReference
|
||||
import com.tangem.core.ui.extensions.stringReference
|
||||
import com.tangem.core.ui.extensions.stringResourceSafe
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.core.ui.res.TangemThemePreview
|
||||
import com.tangem.feature.swap.models.states.PercentDifference
|
||||
import com.tangem.feature.swap.models.states.ProviderState
|
||||
|
||||
// TODO: [REDACTED_TASK_KEY] — remove this UI after swap migrates to swap-v2.
|
||||
// Layout copied from V2 `SwapChooseProviderContent`:
|
||||
// features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/chooseprovider/ui/SwapChooseProviderContent.kt
|
||||
@Composable
|
||||
internal fun ProviderItemBlockSimple(state: ProviderState, modifier: Modifier = Modifier) {
|
||||
if (state is ProviderState.Empty) return
|
||||
|
||||
Row(
|
||||
modifier = modifier
|
||||
.clip(RoundedCornerShape(TangemTheme.dimens.radius16))
|
||||
.background(TangemTheme.colors.background.primary)
|
||||
.clickable(
|
||||
enabled = state.onProviderClick != null,
|
||||
onClick = { state.onProviderClick?.invoke(state.id) },
|
||||
)
|
||||
.fillMaxWidth()
|
||||
.padding(TangemTheme.dimens.spacing12),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
Icon(
|
||||
painter = painterResource(id = R.drawable.ic_stack_new_24),
|
||||
tint = TangemTheme.colors.icon.accent,
|
||||
contentDescription = null,
|
||||
modifier = Modifier.size(TangemTheme.dimens.size24),
|
||||
)
|
||||
SpacerW8()
|
||||
Text(
|
||||
text = stringResourceSafe(R.string.express_provider),
|
||||
style = TangemTheme.typography.body2,
|
||||
color = TangemTheme.colors.text.primary1,
|
||||
)
|
||||
SpacerWMax()
|
||||
SimpleProviderTrailing(state = state)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun SimpleProviderTrailing(state: ProviderState) {
|
||||
when (state) {
|
||||
is ProviderState.Content -> {
|
||||
SubcomposeAsyncImage(
|
||||
model = ImageRequest.Builder(context = LocalContext.current)
|
||||
.data(state.iconUrl)
|
||||
.crossfade(enable = true)
|
||||
.allowHardware(false)
|
||||
.build(),
|
||||
loading = { RectangleShimmer(radius = 4.dp) },
|
||||
error = { RectangleShimmer(radius = 4.dp) },
|
||||
contentDescription = null,
|
||||
modifier = Modifier
|
||||
.size(TangemTheme.dimens.size20)
|
||||
.clip(RoundedCornerShape(TangemTheme.dimens.radius4)),
|
||||
)
|
||||
Text(
|
||||
text = state.name,
|
||||
style = TangemTheme.typography.body2,
|
||||
color = TangemTheme.colors.text.tertiary,
|
||||
modifier = Modifier.padding(start = TangemTheme.dimens.spacing6),
|
||||
)
|
||||
Icon(
|
||||
painter = painterResource(id = R.drawable.ic_chevron_24),
|
||||
tint = TangemTheme.colors.icon.secondary,
|
||||
contentDescription = null,
|
||||
modifier = Modifier.size(TangemTheme.dimens.size24),
|
||||
)
|
||||
}
|
||||
is ProviderState.Loading -> {
|
||||
RectangleShimmer(
|
||||
modifier = Modifier
|
||||
.size(width = TangemTheme.dimens.size80, height = TangemTheme.dimens.size20),
|
||||
radius = TangemTheme.dimens.radius4,
|
||||
)
|
||||
}
|
||||
is ProviderState.Unavailable -> {
|
||||
Text(
|
||||
text = state.alertText.resolveReference(),
|
||||
style = TangemTheme.typography.body2,
|
||||
color = TangemTheme.colors.text.warning,
|
||||
)
|
||||
}
|
||||
is ProviderState.Empty -> Unit
|
||||
}
|
||||
}
|
||||
|
||||
// region Preview
|
||||
@Preview(showBackground = true, widthDp = 360)
|
||||
@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES)
|
||||
@Composable
|
||||
private fun ProviderItemBlockSimple_Preview(@PreviewParameter(SimpleProviderPreview::class) state: ProviderState) {
|
||||
TangemThemePreview {
|
||||
ProviderItemBlockSimple(state = state)
|
||||
}
|
||||
}
|
||||
|
||||
private class SimpleProviderPreview : PreviewParameterProvider<ProviderState> {
|
||||
override val values: Sequence<ProviderState> = sequenceOf(
|
||||
ProviderState.Content(
|
||||
id = "1",
|
||||
name = "Changelly",
|
||||
type = "CEX",
|
||||
iconUrl = "",
|
||||
subtitle = stringReference("1 SOL ≈ 0.0011337 BTC"),
|
||||
selectionType = ProviderState.SelectionType.CLICK,
|
||||
additionalBadge = ProviderState.AdditionalBadge.Empty,
|
||||
percentLowerThenBest = PercentDifference.Empty,
|
||||
namePrefix = ProviderState.PrefixType.NONE,
|
||||
onProviderClick = {},
|
||||
),
|
||||
ProviderState.Loading(),
|
||||
ProviderState.Unavailable(
|
||||
id = "2",
|
||||
name = "1inch",
|
||||
type = "DEX",
|
||||
iconUrl = "",
|
||||
alertText = stringReference("Unavailable"),
|
||||
selectionType = ProviderState.SelectionType.NONE,
|
||||
),
|
||||
)
|
||||
}
|
||||
// endregion
|
||||
|
|
@ -28,11 +28,14 @@ import com.tangem.feature.swap.domain.models.SwapAmount
|
|||
import com.tangem.feature.swap.domain.models.domain.ExchangeProviderType
|
||||
import com.tangem.feature.swap.domain.models.domain.IncludeFeeInAmount
|
||||
import com.tangem.feature.swap.domain.models.domain.RateType
|
||||
import com.tangem.feature.swap.converters.SwapProviderStateBuilder
|
||||
import com.tangem.feature.swap.domain.models.domain.SwapProvider
|
||||
import com.tangem.feature.swap.domain.models.domain.SwapUIMode
|
||||
import com.tangem.feature.swap.domain.models.ui.*
|
||||
import com.tangem.feature.swap.model.SwapNotificationsFactory
|
||||
import com.tangem.feature.swap.model.SwapProcessDataState
|
||||
import com.tangem.feature.swap.models.*
|
||||
import com.tangem.feature.swap.models.SwapButton.Mode
|
||||
import com.tangem.feature.swap.models.states.*
|
||||
import com.tangem.feature.swap.presentation.R
|
||||
import com.tangem.feature.swap.utils.formatToUIRepresentation
|
||||
|
|
@ -45,8 +48,6 @@ import kotlinx.collections.immutable.ImmutableList
|
|||
import kotlinx.collections.immutable.persistentListOf
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
import java.math.BigDecimal
|
||||
import java.math.RoundingMode
|
||||
import kotlin.math.min
|
||||
|
||||
/**
|
||||
* State builder creates a specific states for SwapScreen
|
||||
|
|
@ -57,15 +58,16 @@ internal class StateBuilder(
|
|||
private val isBalanceHiddenProvider: Provider<Boolean>,
|
||||
private val appCurrencyProvider: Provider<AppCurrency>,
|
||||
private val isAccountsModeProvider: Provider<Boolean>,
|
||||
private val iGaslessFeeSupportedForNetwork: IsGaslessFeeSupportedForNetwork,
|
||||
private val isGaslessFeeSupportedForNetwork: IsGaslessFeeSupportedForNetwork,
|
||||
private val shouldShowAbMenu: Boolean,
|
||||
) {
|
||||
private val iconStateConverter by lazy(::CryptoCurrencyToIconStateConverter)
|
||||
|
||||
private val notificationsFactory by lazy(LazyThreadSafetyMode.NONE) {
|
||||
SwapNotificationsFactory(actions, iGaslessFeeSupportedForNetwork)
|
||||
SwapNotificationsFactory(actions, isGaslessFeeSupportedForNetwork)
|
||||
}
|
||||
|
||||
fun createInitialLoadingState(): SwapStateHolder {
|
||||
fun createInitialLoadingState(swapUIMode: SwapUIMode = SwapUIMode.Detailed): SwapStateHolder {
|
||||
return SwapStateHolder(
|
||||
sendCardData = getEmptyCardState(
|
||||
isFromCard = true,
|
||||
|
|
@ -79,7 +81,7 @@ internal class StateBuilder(
|
|||
swapButton = SwapButton(
|
||||
walletInteractionIcon = null,
|
||||
isEnabled = false,
|
||||
isInProgress = true,
|
||||
mode = Mode.SWAP_PROGRESSING,
|
||||
isHoldToConfirm = false,
|
||||
onClick = {},
|
||||
),
|
||||
|
|
@ -95,6 +97,9 @@ internal class StateBuilder(
|
|||
shouldShowMaxAmount = false,
|
||||
priceImpact = PriceImpact.Empty,
|
||||
isInsufficientFunds = false,
|
||||
swapUIMode = swapUIMode,
|
||||
onSwapUIModeChange = actions.onSwapUIModeChange,
|
||||
shouldShowAbMenu = shouldShowAbMenu,
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -461,7 +466,6 @@ internal class StateBuilder(
|
|||
quoteModel = quoteModel,
|
||||
feeCryptoCurrencyStatus = feeCryptoCurrencyStatus,
|
||||
selectedFeeType = selectedFeeType,
|
||||
providerName = swapProvider.name,
|
||||
hideFee = hideFee,
|
||||
)
|
||||
|
||||
|
|
@ -547,15 +551,16 @@ internal class StateBuilder(
|
|||
onClick = actions.onSwapClick,
|
||||
),
|
||||
changeCardsButtonState = ChangeCardsButtonState.ENABLED,
|
||||
providerState = swapProvider.convertToContentClickableProviderState(
|
||||
isBestRate = bestRatedProviderId == swapProvider.providerId && !priceImpact.shouldShowWarning(),
|
||||
providerState = SwapProviderStateBuilder.buildContentClickable(
|
||||
provider = swapProvider,
|
||||
fromTokenInfo = quoteModel.fromTokenInfo,
|
||||
toTokenInfo = quoteModel.toTokenInfo,
|
||||
isNeedBestRateBadge = isNeedBestRateBadge,
|
||||
selectionType = ProviderState.SelectionType.CLICK,
|
||||
onProviderClick = actions.onProviderClick,
|
||||
needApplyFCARestrictions = needApplyFCARestrictions,
|
||||
permissionState = quoteModel.permissionState,
|
||||
selectionType = ProviderState.SelectionType.CLICK,
|
||||
isBestRate = bestRatedProviderId == swapProvider.providerId && !priceImpact.shouldShowWarning(),
|
||||
isNeedBestRateBadge = isNeedBestRateBadge,
|
||||
needApplyFCARestrictions = needApplyFCARestrictions,
|
||||
onProviderClick = actions.onProviderClick,
|
||||
),
|
||||
priceImpact = priceImpact,
|
||||
tosState = createTosState(swapProvider),
|
||||
|
|
@ -684,27 +689,27 @@ internal class StateBuilder(
|
|||
): ProviderState {
|
||||
return when (expressDataError) {
|
||||
is ExpressDataError.ExchangeTooSmallAmountError -> {
|
||||
swapProvider.convertToAvailableFromProviderState(
|
||||
swapProvider = swapProvider,
|
||||
SwapProviderStateBuilder.buildAvailableFrom(
|
||||
provider = swapProvider,
|
||||
alertText = resourceReference(
|
||||
R.string.express_provider_min_amount,
|
||||
wrappedList(expressDataError.amount.getFormattedCryptoAmount(fromToken)),
|
||||
),
|
||||
selectionType = selectionType,
|
||||
onProviderClick = onProviderClick,
|
||||
needApplyFCARestrictions = needApplyFCARestrictions,
|
||||
onProviderClick = onProviderClick,
|
||||
)
|
||||
}
|
||||
is ExpressDataError.ExchangeTooBigAmountError -> {
|
||||
swapProvider.convertToAvailableFromProviderState(
|
||||
swapProvider = swapProvider,
|
||||
SwapProviderStateBuilder.buildAvailableFrom(
|
||||
provider = swapProvider,
|
||||
alertText = resourceReference(
|
||||
R.string.express_provider_max_amount,
|
||||
wrappedList(expressDataError.amount.getFormattedCryptoAmount(fromToken)),
|
||||
),
|
||||
selectionType = selectionType,
|
||||
onProviderClick = onProviderClick,
|
||||
needApplyFCARestrictions = needApplyFCARestrictions,
|
||||
onProviderClick = onProviderClick,
|
||||
)
|
||||
}
|
||||
else -> {
|
||||
|
|
@ -735,6 +740,7 @@ internal class StateBuilder(
|
|||
swapButton = SwapButton(
|
||||
walletInteractionIcon = fromSwapCurrencyStatus?.userWallet?.let(::walletInterationIcon),
|
||||
isEnabled = false,
|
||||
mode = if (emptyAmountState.isTransferMode) Mode.TRANSFER else Mode.SWAP,
|
||||
isHoldToConfirm = fromSwapCurrencyStatus?.userWallet?.isHotWallet == true,
|
||||
onClick = { },
|
||||
),
|
||||
|
|
@ -748,7 +754,7 @@ internal class StateBuilder(
|
|||
return uiState.copy(
|
||||
swapButton = uiState.swapButton.copy(
|
||||
isEnabled = false,
|
||||
isInProgress = true,
|
||||
mode = Mode.SWAP_PROGRESSING,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
|
@ -879,7 +885,7 @@ internal class StateBuilder(
|
|||
return uiState.copy(
|
||||
swapButton = uiState.swapButton.copy(
|
||||
isEnabled = false,
|
||||
isInProgress = false,
|
||||
mode = Mode.SWAP,
|
||||
),
|
||||
notifications = notificationsFactory.getApprovalInProgressStateNotification(uiState.notifications),
|
||||
)
|
||||
|
|
@ -1051,10 +1057,8 @@ internal class StateBuilder(
|
|||
providers = providers.map { providerState ->
|
||||
val tokenInfo = tokenSwapInfoForProviders[providerState.id]
|
||||
if (providerState is ProviderState.Content && tokenInfo != null) {
|
||||
val rateString = tokenInfo.tokenAmount
|
||||
.getFormattedCryptoAmount(tokenInfo.swapCurrencyStatus.currency)
|
||||
providerState.copy(
|
||||
subtitle = stringReference(rateString),
|
||||
subtitle = SwapProviderStateBuilder.buildSelectableSubtitle(tokenInfo),
|
||||
percentLowerThenBest = pricesLowerBest[providerState.id]?.let { percent ->
|
||||
PercentDifference.Value(percent)
|
||||
} ?: PercentDifference.Value(0f),
|
||||
|
|
@ -1131,14 +1135,16 @@ internal class StateBuilder(
|
|||
): ProviderState? {
|
||||
val provider = this.key
|
||||
return when (val state = this.value) {
|
||||
is SwapState.EmptyAmountState -> null
|
||||
is SwapState.EmptyAmountState, is SwapState.Transfer -> null
|
||||
is SwapState.QuotesLoadedState -> {
|
||||
provider.convertToContentSelectableProviderState(
|
||||
state = state,
|
||||
onProviderClick = onProviderSelect,
|
||||
SwapProviderStateBuilder.buildContentSelectable(
|
||||
provider = provider,
|
||||
toTokenInfo = state.toTokenInfo,
|
||||
permissionState = state.permissionState,
|
||||
pricesLowerBest = pricesLowerBest,
|
||||
selectionType = ProviderState.SelectionType.SELECT,
|
||||
needApplyFCARestrictions = needApplyFCARestrictions,
|
||||
onProviderClick = onProviderSelect,
|
||||
)
|
||||
}
|
||||
is SwapState.SwapError -> getProviderStateForError(
|
||||
|
|
@ -1152,113 +1158,6 @@ internal class StateBuilder(
|
|||
}
|
||||
}
|
||||
|
||||
@Suppress("LongParameterList")
|
||||
private fun SwapProvider.convertToContentClickableProviderState(
|
||||
isBestRate: Boolean,
|
||||
fromTokenInfo: TokenSwapInfo,
|
||||
toTokenInfo: TokenSwapInfo,
|
||||
selectionType: ProviderState.SelectionType,
|
||||
isNeedBestRateBadge: Boolean,
|
||||
onProviderClick: (String) -> Unit,
|
||||
needApplyFCARestrictions: Boolean,
|
||||
permissionState: PermissionDataState,
|
||||
): ProviderState {
|
||||
val rate = toTokenInfo.tokenAmount.value.calculateRate(
|
||||
fromTokenInfo.tokenAmount.value,
|
||||
toTokenInfo.swapCurrencyStatus.currency.decimals,
|
||||
)
|
||||
val fromCurrencySymbol = fromTokenInfo.swapCurrencyStatus.currency.symbol
|
||||
val rateString = buildString {
|
||||
append(BigDecimal.ONE.format { crypto(symbol = fromCurrencySymbol, decimals = 0).anyDecimals() })
|
||||
append(" ≈ ")
|
||||
append(rate.format { crypto(toTokenInfo.swapCurrencyStatus.currency) })
|
||||
}
|
||||
|
||||
val additionalBadge = when {
|
||||
needApplyFCARestrictions && isFCARestrictedProvider() -> ProviderState.AdditionalBadge.FCAWarningList
|
||||
permissionState is PermissionDataState.PermissionRequired ->
|
||||
ProviderState.AdditionalBadge.PermissionRequired
|
||||
isRecommended -> ProviderState.AdditionalBadge.Recommended
|
||||
isNeedBestRateBadge && isBestRate && !needApplyFCARestrictions -> ProviderState.AdditionalBadge.BestTrade
|
||||
else -> ProviderState.AdditionalBadge.Empty
|
||||
}
|
||||
|
||||
return ProviderState.Content(
|
||||
id = this.providerId,
|
||||
name = this.name,
|
||||
iconUrl = this.imageLarge,
|
||||
type = this.type.providerName,
|
||||
subtitle = stringReference(rateString),
|
||||
additionalBadge = additionalBadge,
|
||||
selectionType = selectionType,
|
||||
percentLowerThenBest = PercentDifference.Empty,
|
||||
namePrefix = ProviderState.PrefixType.NONE,
|
||||
onProviderClick = onProviderClick,
|
||||
)
|
||||
}
|
||||
|
||||
private fun SwapProvider.convertToContentSelectableProviderState(
|
||||
state: SwapState.QuotesLoadedState,
|
||||
selectionType: ProviderState.SelectionType,
|
||||
pricesLowerBest: Map<String, Float>,
|
||||
onProviderClick: (String) -> Unit,
|
||||
needApplyFCARestrictions: Boolean,
|
||||
): ProviderState {
|
||||
val toTokenInfo = state.toTokenInfo
|
||||
val rateString = toTokenInfo.tokenAmount.getFormattedCryptoAmount(toTokenInfo.swapCurrencyStatus.currency)
|
||||
|
||||
val additionalBadge = when {
|
||||
needApplyFCARestrictions && isFCARestrictedProvider() -> ProviderState.AdditionalBadge.FCAWarningList
|
||||
state.permissionState is PermissionDataState.PermissionRequired -> {
|
||||
ProviderState.AdditionalBadge.PermissionRequired
|
||||
}
|
||||
isRecommended -> ProviderState.AdditionalBadge.Recommended
|
||||
else -> ProviderState.AdditionalBadge.Empty
|
||||
}
|
||||
|
||||
return ProviderState.Content(
|
||||
id = this.providerId,
|
||||
name = this.name,
|
||||
iconUrl = this.imageLarge,
|
||||
type = this.type.providerName,
|
||||
subtitle = stringReference(rateString),
|
||||
additionalBadge = additionalBadge,
|
||||
selectionType = selectionType,
|
||||
percentLowerThenBest = pricesLowerBest[this.providerId]?.let { percent ->
|
||||
PercentDifference.Value(percent)
|
||||
} ?: PercentDifference.Value(0f),
|
||||
namePrefix = ProviderState.PrefixType.NONE,
|
||||
onProviderClick = onProviderClick,
|
||||
)
|
||||
}
|
||||
|
||||
private fun SwapProvider.convertToAvailableFromProviderState(
|
||||
swapProvider: SwapProvider,
|
||||
alertText: TextReference,
|
||||
selectionType: ProviderState.SelectionType,
|
||||
onProviderClick: (String) -> Unit,
|
||||
needApplyFCARestrictions: Boolean,
|
||||
): ProviderState {
|
||||
val additionalBadge = when {
|
||||
needApplyFCARestrictions && isFCARestrictedProvider() -> ProviderState.AdditionalBadge.FCAWarningList
|
||||
swapProvider.isRecommended -> ProviderState.AdditionalBadge.Recommended
|
||||
else -> ProviderState.AdditionalBadge.Empty
|
||||
}
|
||||
|
||||
return ProviderState.Content(
|
||||
id = this.providerId,
|
||||
name = this.name,
|
||||
iconUrl = this.imageLarge,
|
||||
type = this.type.providerName,
|
||||
selectionType = selectionType,
|
||||
subtitle = alertText,
|
||||
additionalBadge = additionalBadge,
|
||||
percentLowerThenBest = PercentDifference.Empty,
|
||||
namePrefix = ProviderState.PrefixType.NONE,
|
||||
onProviderClick = onProviderClick,
|
||||
)
|
||||
}
|
||||
|
||||
private fun CryptoCurrencyStatus?.getFormattedAmount(isNeedSymbol: Boolean): String {
|
||||
val amount = this?.value?.amount ?: return DASH_SIGN
|
||||
val symbol = if (isNeedSymbol) currency.symbol else ""
|
||||
|
|
@ -1282,19 +1181,10 @@ internal class StateBuilder(
|
|||
return value.format { crypto(token) }
|
||||
}
|
||||
|
||||
private fun BigDecimal.calculateRate(to: BigDecimal, decimals: Int): BigDecimal {
|
||||
val rateDecimals = if (decimals == 0) IF_ZERO_DECIMALS_TO_SHOW else decimals
|
||||
return this.divide(to, min(rateDecimals, MAX_DECIMALS_TO_SHOW), RoundingMode.HALF_UP)
|
||||
}
|
||||
|
||||
private fun String.appendApproximateSign(): String {
|
||||
return "$TILDE_SIGN $this"
|
||||
}
|
||||
|
||||
private fun SwapProvider.isFCARestrictedProvider(): Boolean {
|
||||
return FCA_RESTRICTED_PROVIDER_IDS.contains(providerId)
|
||||
}
|
||||
|
||||
private fun getCardAccountTitle(account: Account?, isFromCard: Boolean): AccountTitleUM {
|
||||
val (prefix, placeholder) = if (isFromCard) {
|
||||
R.string.swapping_from_account_title to R.string.swapping_from_title_v2
|
||||
|
|
@ -1318,17 +1208,4 @@ internal class StateBuilder(
|
|||
is Account.Payment -> AccountIconUM.Payment
|
||||
}
|
||||
}
|
||||
|
||||
private companion object {
|
||||
private const val MAX_DECIMALS_TO_SHOW = 8
|
||||
private const val IF_ZERO_DECIMALS_TO_SHOW = 2
|
||||
|
||||
private val FCA_RESTRICTED_PROVIDER_IDS = setOf(
|
||||
"changelly",
|
||||
"changenow",
|
||||
"okx-cross-chain",
|
||||
"okx-on-chain",
|
||||
"simpleswap",
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -2,19 +2,39 @@ package com.tangem.feature.swap.ui
|
|||
|
||||
import androidx.activity.compose.BackHandler
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.systemBarsPadding
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.saveable.rememberSaveable
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.platform.testTag
|
||||
import com.tangem.core.ui.components.appbar.AppBarWithBackButton
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.unit.DpOffset
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.tangem.core.ui.components.appbar.AppBarWithBackButtonAndIcon
|
||||
import com.tangem.core.ui.components.dropdownmenu.TangemDropdownMenu
|
||||
import com.tangem.core.ui.extensions.stringResourceSafe
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.core.ui.test.SwapTokenScreenTestTags
|
||||
import com.tangem.core.ui.utils.WindowInsetsZero
|
||||
import com.tangem.feature.swap.component.SwapFeeSelectorBlockComponent
|
||||
import com.tangem.feature.swap.domain.models.domain.SwapUIMode
|
||||
import com.tangem.feature.swap.models.SwapStateHolder
|
||||
import com.tangem.feature.swap.models.states.ChooseFeeBottomSheetConfig
|
||||
import com.tangem.feature.swap.models.states.ChooseProviderBottomSheetConfig
|
||||
|
|
@ -26,13 +46,7 @@ internal fun SwapScreen(stateHolder: SwapStateHolder, feeSelectorBlockComponent:
|
|||
|
||||
Scaffold(
|
||||
modifier = Modifier.systemBarsPadding(),
|
||||
topBar = {
|
||||
AppBarWithBackButton(
|
||||
text = stringResourceSafe(R.string.common_swap),
|
||||
onBackClick = stateHolder.onBackClicked,
|
||||
iconRes = R.drawable.ic_close_24,
|
||||
)
|
||||
},
|
||||
topBar = { SwapTopBar(stateHolder = stateHolder) },
|
||||
contentWindowInsets = WindowInsetsZero,
|
||||
containerColor = TangemTheme.colors.background.secondary,
|
||||
) { scaffoldPaddings ->
|
||||
|
|
@ -64,4 +78,79 @@ internal fun SwapScreen(stateHolder: SwapStateHolder, feeSelectorBlockComponent:
|
|||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun SwapTopBar(stateHolder: SwapStateHolder) {
|
||||
var shouldShowModeMenu by rememberSaveable { mutableStateOf(false) }
|
||||
Box(modifier = Modifier.fillMaxWidth()) {
|
||||
AppBarWithBackButtonAndIcon(
|
||||
text = stringResourceSafe(R.string.common_swap),
|
||||
backIconRes = R.drawable.ic_close_24,
|
||||
iconRes = if (stateHolder.shouldShowAbMenu) R.drawable.ic_more_vertical_24 else null,
|
||||
onIconClick = if (stateHolder.shouldShowAbMenu) {
|
||||
{ shouldShowModeMenu = true }
|
||||
} else {
|
||||
null
|
||||
},
|
||||
onBackClick = stateHolder.onBackClicked,
|
||||
)
|
||||
if (stateHolder.shouldShowAbMenu) {
|
||||
Box(modifier = Modifier.align(Alignment.TopEnd)) {
|
||||
TangemDropdownMenu(
|
||||
expanded = shouldShowModeMenu,
|
||||
modifier = Modifier.background(TangemTheme.colors.background.primary),
|
||||
offset = DpOffset(x = TangemTheme.dimens.spacing20, y = 44.dp),
|
||||
onDismissRequest = { shouldShowModeMenu = false },
|
||||
content = {
|
||||
SwapUiModeMenuItem(
|
||||
title = stringResourceSafe(R.string.swap_simple_mode),
|
||||
isSelected = stateHolder.swapUIMode == SwapUIMode.Simple,
|
||||
onClick = {
|
||||
shouldShowModeMenu = false
|
||||
stateHolder.onSwapUIModeChange(SwapUIMode.Simple)
|
||||
},
|
||||
)
|
||||
SwapUiModeMenuItem(
|
||||
title = stringResourceSafe(R.string.swap_detailed_mode),
|
||||
isSelected = stateHolder.swapUIMode == SwapUIMode.Detailed,
|
||||
onClick = {
|
||||
shouldShowModeMenu = false
|
||||
stateHolder.onSwapUIModeChange(SwapUIMode.Detailed)
|
||||
},
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun SwapUiModeMenuItem(title: String, isSelected: Boolean, onClick: () -> Unit) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.clickable(onClick = onClick)
|
||||
.padding(horizontal = 16.dp, vertical = 8.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(16.dp),
|
||||
) {
|
||||
Text(
|
||||
text = title,
|
||||
style = TangemTheme.typography.button,
|
||||
color = TangemTheme.colors.text.primary1,
|
||||
modifier = Modifier.weight(1f),
|
||||
)
|
||||
if (isSelected) {
|
||||
Icon(
|
||||
painter = painterResource(id = R.drawable.ic_check_24),
|
||||
tint = TangemTheme.colors.icon.primary1,
|
||||
contentDescription = null,
|
||||
modifier = Modifier.size(16.dp),
|
||||
)
|
||||
} else {
|
||||
Spacer(modifier = Modifier.width(16.dp))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -14,6 +14,7 @@ import androidx.compose.material3.Icon
|
|||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.ripple
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.ReadOnlyComposable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Alignment
|
||||
|
|
@ -38,6 +39,7 @@ import com.tangem.core.ui.extensions.stringResourceSafe
|
|||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.core.ui.res.TangemThemePreview
|
||||
import com.tangem.core.ui.test.SwapTokenScreenTestTags
|
||||
import com.tangem.feature.swap.domain.models.domain.SwapUIMode
|
||||
import com.tangem.feature.swap.domain.models.ui.FeeType
|
||||
import com.tangem.feature.swap.domain.models.ui.PriceImpact
|
||||
import com.tangem.feature.swap.models.*
|
||||
|
|
@ -78,7 +80,11 @@ internal fun SwapScreenContent(
|
|||
) {
|
||||
MainInfo(state)
|
||||
|
||||
ProviderItemBlock(state = state.providerState)
|
||||
if (state.swapUIMode == SwapUIMode.Simple) {
|
||||
ProviderItemBlockSimple(state = state.providerState)
|
||||
} else {
|
||||
ProviderItemBlock(state = state.providerState)
|
||||
}
|
||||
|
||||
if (feeBlock != null) {
|
||||
feeBlock(Modifier.fillMaxWidth())
|
||||
|
|
@ -138,14 +144,25 @@ private fun MainInfo(state: SwapStateHolder) {
|
|||
onSelectTokenClick = { state.onSelectTokenClick(TokenSelectionDirection.FROM) },
|
||||
)
|
||||
val marginCard = TangemTheme.dimens.spacing12
|
||||
TransactionCard(
|
||||
priceImpact = priceImpact,
|
||||
swapCardState = state.receiveCardData,
|
||||
modifier = Modifier.constrainAs(bottomCard) {
|
||||
top.linkTo(topCard.bottom, margin = marginCard)
|
||||
},
|
||||
onSelectTokenClick = { state.onSelectTokenClick(TokenSelectionDirection.TO) },
|
||||
)
|
||||
if (state.swapUIMode == SwapUIMode.Simple) {
|
||||
TransactionCardSimple(
|
||||
priceImpact = priceImpact,
|
||||
swapCardState = state.receiveCardData,
|
||||
modifier = Modifier.constrainAs(bottomCard) {
|
||||
top.linkTo(topCard.bottom, margin = marginCard)
|
||||
},
|
||||
onSelectTokenClick = { state.onSelectTokenClick(TokenSelectionDirection.TO) },
|
||||
)
|
||||
} else {
|
||||
TransactionCard(
|
||||
priceImpact = priceImpact,
|
||||
swapCardState = state.receiveCardData,
|
||||
modifier = Modifier.constrainAs(bottomCard) {
|
||||
top.linkTo(topCard.bottom, margin = marginCard)
|
||||
},
|
||||
onSelectTokenClick = { state.onSelectTokenClick(TokenSelectionDirection.TO) },
|
||||
)
|
||||
}
|
||||
val marginButton = TangemTheme.dimens.spacing30
|
||||
SwapButton(
|
||||
state,
|
||||
|
|
@ -345,7 +362,7 @@ private fun MainButton(state: SwapStateHolder) {
|
|||
state.swapButton.isHoldToConfirm -> {
|
||||
HoldToConfirmButton(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
text = stringResourceSafe(R.string.swapping_swap_action),
|
||||
text = getButtonTitle(state.swapButton.mode),
|
||||
enabled = state.swapButton.isEnabled,
|
||||
onConfirm = state.swapButton.onClick,
|
||||
isLoading = state.swapButton.isInProgress,
|
||||
|
|
@ -355,11 +372,7 @@ private fun MainButton(state: SwapStateHolder) {
|
|||
else -> {
|
||||
PrimaryButtonIconEnd(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
text = if (state.swapButton.isInProgress) {
|
||||
stringResourceSafe(id = R.string.swapping_swap_action_in_progress)
|
||||
} else {
|
||||
stringResourceSafe(id = R.string.swapping_swap_action)
|
||||
},
|
||||
text = getButtonTitle(state.swapButton.mode),
|
||||
iconResId = state.swapButton.walletInteractionIcon,
|
||||
enabled = state.swapButton.isEnabled,
|
||||
onClick = state.swapButton.onClick,
|
||||
|
|
@ -368,6 +381,19 @@ private fun MainButton(state: SwapStateHolder) {
|
|||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
@ReadOnlyComposable
|
||||
private fun getButtonTitle(mode: SwapButton.Mode): String {
|
||||
return when (mode) {
|
||||
SwapButton.Mode.SWAP_PROGRESSING -> stringResourceSafe(id = R.string.swapping_swap_action_in_progress)
|
||||
SwapButton.Mode.SWAP -> stringResourceSafe(id = R.string.swapping_swap_action)
|
||||
SwapButton.Mode.TRANSFER -> stringResourceSafe(id = R.string.swapping_transfer_action)
|
||||
SwapButton.Mode.TRANSFER_PROGRESSING -> stringResourceSafe(
|
||||
id = R.string.swapping_transfer_action_in_progress,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// region preview
|
||||
|
||||
private val state = SwapStateHolder(
|
||||
|
|
@ -384,9 +410,8 @@ private val state = SwapStateHolder(
|
|||
),
|
||||
notifications = persistentListOf(
|
||||
SwapNotificationUM.Info.PermissionNeeded(
|
||||
providerName = "Provider",
|
||||
fromTokenSymbol = "POL",
|
||||
onApproveClick = {},
|
||||
onLearnMoreClick = {},
|
||||
),
|
||||
SwapNotificationUM.Warning.NoAvailableTokensToSwap("POLYGON"),
|
||||
),
|
||||
|
|
|
|||
|
|
@ -0,0 +1,418 @@
|
|||
package com.tangem.feature.swap.ui
|
||||
|
||||
import android.content.res.Configuration
|
||||
import androidx.compose.animation.AnimatedContent
|
||||
import androidx.compose.foundation.background
|
||||
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.foundation.text.TextAutoSize
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.ripple
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.focus.FocusRequester
|
||||
import androidx.compose.ui.platform.testTag
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.text.input.TextFieldValue
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import com.tangem.common.ui.account.AccountTitle
|
||||
import com.tangem.core.ui.R
|
||||
import com.tangem.core.ui.components.RectangleShimmer
|
||||
import com.tangem.core.ui.components.SpacerH4
|
||||
import com.tangem.core.ui.components.SpacerW16
|
||||
import com.tangem.core.ui.components.TextShimmer
|
||||
import com.tangem.core.ui.components.buttons.SecondarySmallButton
|
||||
import com.tangem.core.ui.components.buttons.SmallButtonConfig
|
||||
import com.tangem.core.ui.components.buttons.common.TangemButtonIconPosition
|
||||
import com.tangem.core.ui.extensions.orMaskWithStars
|
||||
import com.tangem.core.ui.extensions.resolveAnnotatedReference
|
||||
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.core.ui.test.SwapTokenScreenTestTags
|
||||
import com.tangem.feature.swap.domain.models.ui.PriceImpact
|
||||
import com.tangem.feature.swap.models.SwapCardState
|
||||
import com.tangem.feature.swap.models.TransactionCardType
|
||||
import com.tangem.feature.swap.ui.preview.SwapTransactionCardPreview
|
||||
|
||||
@Composable
|
||||
internal fun TransactionCardSimple(
|
||||
priceImpact: PriceImpact,
|
||||
swapCardState: SwapCardState,
|
||||
onSelectTokenClick: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
val cardTag = when (swapCardState.type) {
|
||||
is TransactionCardType.Inputtable -> SwapTokenScreenTestTags.SWAP_CARD
|
||||
is TransactionCardType.ReadOnly -> SwapTokenScreenTestTags.RECEIVE_CARD
|
||||
}
|
||||
|
||||
when (swapCardState) {
|
||||
is SwapCardState.Empty -> SimpleTransactionCardEmpty(
|
||||
cardState = swapCardState,
|
||||
onChangeTokenClick = onSelectTokenClick,
|
||||
modifier = modifier.testTag(cardTag),
|
||||
)
|
||||
is SwapCardState.SwapCardData -> SimpleTransactionCardData(
|
||||
cardState = swapCardState,
|
||||
priceImpact = priceImpact,
|
||||
onChangeTokenClick = onSelectTokenClick,
|
||||
modifier = modifier.testTag(cardTag),
|
||||
)
|
||||
is SwapCardState.Loading -> SimpleTransactionCardLoading(modifier = modifier.testTag(cardTag))
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun SimpleTransactionCardData(
|
||||
cardState: SwapCardState.SwapCardData,
|
||||
priceImpact: PriceImpact,
|
||||
modifier: Modifier = Modifier,
|
||||
onChangeTokenClick: (() -> Unit)? = null,
|
||||
) {
|
||||
Box(
|
||||
modifier = modifier
|
||||
.background(
|
||||
shape = RoundedCornerShape(TangemTheme.dimens.radius16),
|
||||
color = TangemTheme.colors.background.primary,
|
||||
)
|
||||
.fillMaxWidth(),
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
verticalArrangement = Arrangement.Top,
|
||||
horizontalAlignment = Alignment.Start,
|
||||
) {
|
||||
SimpleHeader(
|
||||
balance = stringResourceSafe(
|
||||
R.string.common_balance,
|
||||
cardState.balance,
|
||||
).orMaskWithStars(cardState.isBalanceHidden),
|
||||
type = cardState.type,
|
||||
)
|
||||
|
||||
SimpleContent(
|
||||
type = cardState.type,
|
||||
textFieldValue = cardState.amountTextFieldValue,
|
||||
priceImpact = priceImpact,
|
||||
)
|
||||
}
|
||||
|
||||
Box(modifier = Modifier.align(Alignment.BottomEnd)) {
|
||||
Token(
|
||||
currencyIconState = cardState.currencyIconState,
|
||||
tokenSymbol = cardState.tokenSymbol,
|
||||
)
|
||||
}
|
||||
|
||||
if (onChangeTokenClick != null) {
|
||||
Box(modifier = Modifier.align(Alignment.CenterEnd)) {
|
||||
ChangeTokenSelector()
|
||||
}
|
||||
Box(
|
||||
Modifier
|
||||
.align(Alignment.CenterEnd)
|
||||
.height(TangemTheme.dimens.size116)
|
||||
.width(TangemTheme.dimens.size102)
|
||||
.clickable(
|
||||
indication = ripple(bounded = false),
|
||||
interactionSource = remember { MutableInteractionSource() },
|
||||
) { onChangeTokenClick() },
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun SimpleTransactionCardEmpty(
|
||||
cardState: SwapCardState.Empty,
|
||||
modifier: Modifier = Modifier,
|
||||
onChangeTokenClick: () -> Unit,
|
||||
) {
|
||||
Column(
|
||||
modifier = modifier
|
||||
.background(
|
||||
shape = RoundedCornerShape(TangemTheme.dimens.radius12),
|
||||
color = TangemTheme.colors.background.primary,
|
||||
)
|
||||
.padding(
|
||||
top = 12.dp,
|
||||
start = 12.dp,
|
||||
end = 12.dp,
|
||||
bottom = 16.dp,
|
||||
)
|
||||
.fillMaxWidth(),
|
||||
horizontalAlignment = Alignment.Start,
|
||||
verticalArrangement = Arrangement.spacedBy(6.dp),
|
||||
) {
|
||||
AccountTitle(
|
||||
accountTitleUM = cardState.type.accountTitleUM,
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
)
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier.weight(1f),
|
||||
verticalArrangement = Arrangement.spacedBy(4.dp),
|
||||
) {
|
||||
Text(
|
||||
text = cardState.amountTextFieldValue?.text.orEmpty(),
|
||||
color = TangemTheme.colors.text.disabled,
|
||||
style = TangemTheme.typography.h2,
|
||||
autoSize = TextAutoSize.StepBased(
|
||||
minFontSize = 16.sp,
|
||||
maxFontSize = TangemTheme.typography.h2.fontSize,
|
||||
),
|
||||
maxLines = 1,
|
||||
modifier = Modifier.testTag(SwapTokenScreenTestTags.SWAP_TEXT_FIELD),
|
||||
)
|
||||
Text(
|
||||
text = cardState.amountEquivalent.resolveAnnotatedReference(),
|
||||
color = TangemTheme.colors.text.tertiary,
|
||||
style = TangemTheme.typography.body2,
|
||||
modifier = Modifier
|
||||
.defaultMinSize(minHeight = TangemTheme.dimens.size20)
|
||||
.testTag(SwapTokenScreenTestTags.SWAP_FIAT_AMOUNT),
|
||||
)
|
||||
}
|
||||
SecondarySmallButton(
|
||||
config = SmallButtonConfig(
|
||||
text = resourceReference(R.string.common_choose_token),
|
||||
icon = TangemButtonIconPosition.End(R.drawable.ic_chevron_24),
|
||||
onClick = onChangeTokenClick,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun SimpleTransactionCardLoading(modifier: Modifier = Modifier) {
|
||||
Column(
|
||||
modifier = modifier
|
||||
.background(
|
||||
shape = RoundedCornerShape(TangemTheme.dimens.radius12),
|
||||
color = TangemTheme.colors.background.primary,
|
||||
)
|
||||
.padding(
|
||||
top = 12.dp,
|
||||
start = 12.dp,
|
||||
end = 12.dp,
|
||||
bottom = 16.dp,
|
||||
)
|
||||
.fillMaxWidth(),
|
||||
horizontalAlignment = Alignment.Start,
|
||||
verticalArrangement = Arrangement.spacedBy(6.dp),
|
||||
) {
|
||||
Column(modifier = Modifier.fillMaxWidth()) {
|
||||
TextShimmer(
|
||||
text = stringResourceSafe(R.string.swapping_to_title),
|
||||
style = TangemTheme.typography.subtitle2,
|
||||
)
|
||||
TextShimmer(
|
||||
style = TangemTheme.typography.body2,
|
||||
modifier = Modifier
|
||||
.testTag(SwapTokenScreenTestTags.BALANCE)
|
||||
.width(60.dp),
|
||||
)
|
||||
}
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier.weight(1f),
|
||||
verticalArrangement = Arrangement.spacedBy(4.dp),
|
||||
) {
|
||||
TextShimmer(
|
||||
style = TangemTheme.typography.h2,
|
||||
modifier = Modifier
|
||||
.width(100.dp)
|
||||
.testTag(SwapTokenScreenTestTags.SWAP_TEXT_FIELD),
|
||||
)
|
||||
TextShimmer(
|
||||
style = TangemTheme.typography.body2,
|
||||
modifier = Modifier
|
||||
.defaultMinSize(minHeight = 20.dp, minWidth = 40.dp)
|
||||
.testTag(SwapTokenScreenTestTags.SWAP_FIAT_AMOUNT),
|
||||
)
|
||||
}
|
||||
SecondarySmallButton(
|
||||
config = SmallButtonConfig(
|
||||
text = resourceReference(R.string.common_choose_token),
|
||||
icon = TangemButtonIconPosition.End(R.drawable.ic_chevron_24),
|
||||
isEnabled = false,
|
||||
onClick = {},
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun SimpleHeader(type: TransactionCardType, balance: String, modifier: Modifier = Modifier) {
|
||||
Column(
|
||||
modifier = modifier
|
||||
.fillMaxWidth()
|
||||
.padding(
|
||||
bottom = TangemTheme.dimens.spacing8,
|
||||
top = TangemTheme.dimens.spacing14,
|
||||
start = TangemTheme.dimens.spacing12,
|
||||
end = TangemTheme.dimens.spacing12,
|
||||
)
|
||||
.testTag(SwapTokenScreenTestTags.SWAP_BLOCK_HEADER),
|
||||
) {
|
||||
val titleColor = if (type.inputError is TransactionCardType.InputError.Empty) {
|
||||
TangemTheme.colors.text.tertiary
|
||||
} else {
|
||||
TangemTheme.colors.text.warning
|
||||
}
|
||||
AccountTitle(
|
||||
accountTitleUM = type.accountTitleUM,
|
||||
textColor = titleColor,
|
||||
)
|
||||
SpacerW16()
|
||||
if (balance.isNotBlank()) {
|
||||
AnimatedContent(targetState = balance, label = "") { balanceText ->
|
||||
Text(
|
||||
text = balanceText,
|
||||
color = TangemTheme.colors.text.tertiary,
|
||||
style = TangemTheme.typography.body2,
|
||||
modifier = Modifier.testTag(SwapTokenScreenTestTags.BALANCE),
|
||||
)
|
||||
}
|
||||
} else {
|
||||
RectangleShimmer(
|
||||
modifier = Modifier
|
||||
.width(TangemTheme.dimens.size80)
|
||||
.height(TangemTheme.dimens.size12),
|
||||
radius = TangemTheme.dimens.radius3,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("LongMethod")
|
||||
@Composable
|
||||
private fun SimpleContent(type: TransactionCardType, priceImpact: PriceImpact, textFieldValue: TextFieldValue?) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.padding(
|
||||
start = TangemTheme.dimens.spacing12,
|
||||
bottom = TangemTheme.dimens.spacing16,
|
||||
),
|
||||
horizontalArrangement = Arrangement.Start,
|
||||
verticalAlignment = Alignment.Top,
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier.padding(end = TangemTheme.dimens.spacing92),
|
||||
verticalArrangement = Arrangement.Top,
|
||||
horizontalAlignment = Alignment.Start,
|
||||
) {
|
||||
val sumTextModifier = Modifier.defaultMinSize(minHeight = TangemTheme.dimens.size32)
|
||||
when (type) {
|
||||
is TransactionCardType.ReadOnly -> {
|
||||
if (textFieldValue != null) {
|
||||
Text(
|
||||
text = textFieldValue.text,
|
||||
color = TangemTheme.colors.text.primary1,
|
||||
style = TangemTheme.typography.h2,
|
||||
autoSize = TextAutoSize.StepBased(
|
||||
minFontSize = 16.sp,
|
||||
maxFontSize = TangemTheme.typography.h2.fontSize,
|
||||
),
|
||||
maxLines = 1,
|
||||
modifier = sumTextModifier.testTag(SwapTokenScreenTestTags.RECEIVE_TEXT_FIELD),
|
||||
)
|
||||
} else {
|
||||
RectangleShimmer(
|
||||
modifier = Modifier
|
||||
.padding(vertical = TangemTheme.dimens.spacing4)
|
||||
.width(TangemTheme.dimens.size102)
|
||||
.height(TangemTheme.dimens.size24),
|
||||
)
|
||||
}
|
||||
}
|
||||
is TransactionCardType.Inputtable -> {
|
||||
val focusRequester = remember { FocusRequester() }
|
||||
AutoSizeTextField(
|
||||
modifier = sumTextModifier.testTag(SwapTokenScreenTestTags.SWAP_TEXT_FIELD),
|
||||
focusRequester = focusRequester,
|
||||
textFieldValue = textFieldValue ?: TextFieldValue(),
|
||||
isEnabled = type.isEnabled,
|
||||
onAmountChange = { type.onAmountChanged(it) },
|
||||
onFocusChange = type.onFocusChanged,
|
||||
)
|
||||
LaunchedEffect(Unit) { focusRequester.requestFocus() }
|
||||
}
|
||||
}
|
||||
SpacerH4()
|
||||
// Keep the same 20dp slot as Detailed (where fiat/shimmer lives)
|
||||
// so that Token (BottomEnd) does not shift when switching modes.
|
||||
Box(modifier = Modifier.defaultMinSize(minHeight = TangemTheme.dimens.size20)) {
|
||||
if (type is TransactionCardType.ReadOnly && type.shouldShowWarning) {
|
||||
WarningIcon(priceImpact = priceImpact, onClick = type.onWarningClick)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun WarningIcon(priceImpact: PriceImpact, onClick: (() -> Unit)?) {
|
||||
IconButton(
|
||||
onClick = { onClick?.invoke() },
|
||||
modifier = Modifier.size(TangemTheme.dimens.size20),
|
||||
) {
|
||||
Icon(
|
||||
painter = painterResource(id = R.drawable.ic_information_24),
|
||||
contentDescription = null,
|
||||
tint = when (priceImpact.type) {
|
||||
PriceImpact.Type.HIGH -> TangemTheme.colors.text.warning
|
||||
PriceImpact.Type.MEDIUM -> TangemTheme.colors.text.attention
|
||||
else -> TangemTheme.colors.text.tertiary
|
||||
},
|
||||
modifier = Modifier.testTag(SwapTokenScreenTestTags.RECEIVE_FIAT_AMOUNT_INFORMATION_ICON),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// region Preview
|
||||
@Preview(showBackground = true, widthDp = 360)
|
||||
@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES)
|
||||
@Composable
|
||||
private fun TransactionCardSimple_Preview(@PreviewParameter(SimplePreviewProvider::class) params: SwapCardState) {
|
||||
TangemThemePreview {
|
||||
TransactionCardSimple(
|
||||
priceImpact = PriceImpact.Empty,
|
||||
swapCardState = params,
|
||||
onSelectTokenClick = {},
|
||||
modifier = Modifier,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private class SimplePreviewProvider : PreviewParameterProvider<SwapCardState> {
|
||||
override val values: Sequence<SwapCardState> = sequenceOf(
|
||||
SwapTransactionCardPreview.sendCard,
|
||||
SwapTransactionCardPreview.receiveCard,
|
||||
SwapTransactionCardPreview.emptyReadOnlyCard,
|
||||
SwapTransactionCardPreview.emptyInputtableCard,
|
||||
SwapTransactionCardPreview.loadingCard,
|
||||
)
|
||||
}
|
||||
// endregion
|
||||
|
|
@ -0,0 +1,188 @@
|
|||
package com.tangem.feature.swap.ui.transfer
|
||||
|
||||
import androidx.compose.ui.text.TextRange
|
||||
import androidx.compose.ui.text.input.TextFieldValue
|
||||
import com.tangem.common.ui.account.AccountIconUM
|
||||
import com.tangem.common.ui.account.AccountTitleUM
|
||||
import com.tangem.common.ui.account.CryptoPortfolioIconConverter
|
||||
import com.tangem.common.ui.account.toUM
|
||||
import com.tangem.common.ui.components.currency.icon.converter.CryptoCurrencyToIconStateConverter
|
||||
import com.tangem.common.ui.userwallet.ext.walletInterationIcon
|
||||
import com.tangem.core.ui.extensions.TextReference
|
||||
import com.tangem.core.ui.extensions.resourceReference
|
||||
import com.tangem.core.ui.extensions.stringReference
|
||||
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
|
||||
import com.tangem.domain.swap.models.SwapCurrencyStatus
|
||||
import com.tangem.feature.swap.domain.models.ui.SwapState
|
||||
import com.tangem.feature.swap.domain.models.ui.TokenSwapInfo
|
||||
import com.tangem.feature.swap.models.*
|
||||
import com.tangem.feature.swap.presentation.R
|
||||
import com.tangem.feature.swap.utils.formatToUIRepresentation
|
||||
import com.tangem.utils.StringsSigns.DASH_SIGN
|
||||
import java.math.BigDecimal
|
||||
import javax.inject.Inject
|
||||
|
||||
internal class SwapTransferStateBuilder @Inject constructor() {
|
||||
|
||||
private val iconConverter by lazy(::CryptoCurrencyToIconStateConverter)
|
||||
|
||||
fun createTransferState(
|
||||
actions: UiActions,
|
||||
transferState: SwapState.Transfer,
|
||||
uiStateHolder: SwapStateHolder,
|
||||
): SwapStateHolder {
|
||||
val fromTokenSwapInfo = transferState.fromTokenInfo
|
||||
val toTokenSwapInfo = transferState.toTokenInfo
|
||||
val isInsufficientBalance = transferState.isInsufficientBalance
|
||||
return uiStateHolder.copy(
|
||||
sendCardData = createSendSwapCardState(
|
||||
actions = actions,
|
||||
tokenSwapInfo = fromTokenSwapInfo,
|
||||
appCurrency = transferState.appCurrency,
|
||||
isAccountsMode = transferState.isAccountsMode,
|
||||
isFromCard = true,
|
||||
isBalanceHidden = transferState.isBalanceHidden,
|
||||
isInsufficientBalance = isInsufficientBalance,
|
||||
),
|
||||
receiveCardData = createSendSwapCardState(
|
||||
actions = actions,
|
||||
tokenSwapInfo = toTokenSwapInfo,
|
||||
appCurrency = transferState.appCurrency,
|
||||
isAccountsMode = transferState.isAccountsMode,
|
||||
isFromCard = false,
|
||||
isBalanceHidden = transferState.isBalanceHidden,
|
||||
isInsufficientBalance = isInsufficientBalance,
|
||||
),
|
||||
isInsufficientFunds = isInsufficientBalance,
|
||||
swapButton = SwapButton(
|
||||
walletInteractionIcon = walletInterationIcon(transferState.userWallet),
|
||||
isEnabled = !isInsufficientBalance,
|
||||
mode = SwapButton.Mode.TRANSFER,
|
||||
onClick = actions.onTransferClick,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
@Suppress("LongParameterList")
|
||||
private fun createSendSwapCardState(
|
||||
actions: UiActions,
|
||||
tokenSwapInfo: TokenSwapInfo,
|
||||
appCurrency: AppCurrency,
|
||||
isAccountsMode: Boolean,
|
||||
isFromCard: Boolean,
|
||||
isBalanceHidden: Boolean,
|
||||
isInsufficientBalance: Boolean,
|
||||
): SwapCardState {
|
||||
val swapCurrencyStatus = tokenSwapInfo.swapCurrencyStatus
|
||||
val formattedSwapAmount = tokenSwapInfo.tokenAmount.formatToUIRepresentation()
|
||||
|
||||
return SwapCardState.SwapCardData(
|
||||
type = createSendTransactionCardType(
|
||||
actions = actions,
|
||||
swapCurrencyStatus = tokenSwapInfo.swapCurrencyStatus,
|
||||
isAccountsMode = isAccountsMode,
|
||||
isFromCard = isFromCard,
|
||||
isInsufficientBalance = isInsufficientBalance,
|
||||
),
|
||||
currencyIconState = iconConverter.convert(
|
||||
value = swapCurrencyStatus.status,
|
||||
),
|
||||
tokenSymbol = stringReference(swapCurrencyStatus.currency.symbol),
|
||||
amountEquivalent = getFormattedFiatAmount(
|
||||
appCurrency = appCurrency,
|
||||
amount = tokenSwapInfo.amountFiat,
|
||||
),
|
||||
amountTextFieldValue = TextFieldValue(
|
||||
text = formattedSwapAmount,
|
||||
selection = TextRange(index = formattedSwapAmount.length),
|
||||
),
|
||||
balance = swapCurrencyStatus.status.getFormattedAmount(),
|
||||
isBalanceHidden = isBalanceHidden,
|
||||
)
|
||||
}
|
||||
|
||||
private fun createSendTransactionCardType(
|
||||
actions: UiActions,
|
||||
swapCurrencyStatus: SwapCurrencyStatus,
|
||||
isAccountsMode: Boolean,
|
||||
isFromCard: Boolean,
|
||||
isInsufficientBalance: Boolean,
|
||||
): TransactionCardType {
|
||||
val type = if (isFromCard) {
|
||||
val accountTitleUM = if (isInsufficientBalance) {
|
||||
AccountTitleUM.Text(resourceReference(R.string.swapping_insufficient_funds))
|
||||
} else {
|
||||
getCardAccountTitle(
|
||||
account = swapCurrencyStatus.account,
|
||||
isAccountsMode = isAccountsMode,
|
||||
isFromCard = true,
|
||||
)
|
||||
}
|
||||
TransactionCardType.Inputtable(
|
||||
onAmountChanged = actions.onAmountChanged,
|
||||
onFocusChanged = actions.onAmountSelected,
|
||||
inputError = if (isInsufficientBalance) {
|
||||
TransactionCardType.InputError.InsufficientFunds
|
||||
} else {
|
||||
TransactionCardType.InputError.Empty
|
||||
},
|
||||
accountTitleUM = accountTitleUM,
|
||||
isEnabled = true,
|
||||
)
|
||||
} else {
|
||||
TransactionCardType.ReadOnly(
|
||||
accountTitleUM = getCardAccountTitle(
|
||||
account = swapCurrencyStatus.account,
|
||||
isAccountsMode = isAccountsMode,
|
||||
isFromCard = false,
|
||||
),
|
||||
)
|
||||
}
|
||||
return type
|
||||
}
|
||||
|
||||
private fun getCardAccountTitle(account: Account?, isAccountsMode: Boolean, isFromCard: Boolean): AccountTitleUM {
|
||||
val (prefix, placeholder) = if (isFromCard) {
|
||||
R.string.swapping_from_account_title to R.string.swapping_from_title_v2
|
||||
} else {
|
||||
R.string.swapping_to_account_title to R.string.swapping_to_title
|
||||
}
|
||||
return if (account != null && isAccountsMode) {
|
||||
AccountTitleUM.Account(
|
||||
prefixText = resourceReference(prefix),
|
||||
name = account.accountName.toUM().value,
|
||||
icon = account.toIconUM(),
|
||||
)
|
||||
} else {
|
||||
AccountTitleUM.Text(resourceReference(placeholder))
|
||||
}
|
||||
}
|
||||
|
||||
private fun getFormattedFiatAmount(appCurrency: AppCurrency, amount: BigDecimal?): TextReference {
|
||||
return stringReference(
|
||||
amount.format {
|
||||
fiat(
|
||||
fiatCurrencyCode = appCurrency.code,
|
||||
fiatCurrencySymbol = appCurrency.symbol,
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
private fun CryptoCurrencyStatus.getFormattedAmount(): String {
|
||||
val amount = this.value.amount ?: return DASH_SIGN
|
||||
return amount.format { crypto(symbol = "", decimals = currency.decimals) }
|
||||
}
|
||||
|
||||
private fun Account.toIconUM(): AccountIconUM {
|
||||
return when (this) {
|
||||
is Account.CryptoPortfolio -> CryptoPortfolioIconConverter.convert(icon)
|
||||
is Account.Payment -> AccountIconUM.Payment
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -30,7 +30,7 @@ internal class StateBuilderInitialStateTest {
|
|||
private val isBalanceHiddenProvider: Provider<Boolean> = mockk()
|
||||
private val appCurrencyProvider: Provider<AppCurrency> = mockk()
|
||||
private val isAccountsModeProvider: Provider<Boolean> = mockk()
|
||||
private val iGaslessFeeSupportedForNetwork: IsGaslessFeeSupportedForNetwork = mockk()
|
||||
private val isGaslessFeeSupportedForNetwork: IsGaslessFeeSupportedForNetwork = mockk()
|
||||
|
||||
private lateinit var sut: StateBuilder
|
||||
|
||||
|
|
@ -47,7 +47,8 @@ internal class StateBuilderInitialStateTest {
|
|||
isBalanceHiddenProvider = isBalanceHiddenProvider,
|
||||
appCurrencyProvider = appCurrencyProvider,
|
||||
isAccountsModeProvider = isAccountsModeProvider,
|
||||
iGaslessFeeSupportedForNetwork = iGaslessFeeSupportedForNetwork,
|
||||
isGaslessFeeSupportedForNetwork = isGaslessFeeSupportedForNetwork,
|
||||
shouldShowAbMenu = false,
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -25,7 +25,7 @@ internal class StateBuilderPairsTest {
|
|||
private val isBalanceHiddenProvider: Provider<Boolean> = mockk()
|
||||
private val appCurrencyProvider: Provider<AppCurrency> = mockk()
|
||||
private val isAccountsModeProvider: Provider<Boolean> = mockk()
|
||||
private val iGaslessFeeSupportedForNetwork: IsGaslessFeeSupportedForNetwork = mockk()
|
||||
private val isGaslessFeeSupportedForNetwork: IsGaslessFeeSupportedForNetwork = mockk()
|
||||
|
||||
private lateinit var sut: StateBuilder
|
||||
|
||||
|
|
@ -52,7 +52,8 @@ internal class StateBuilderPairsTest {
|
|||
isBalanceHiddenProvider = isBalanceHiddenProvider,
|
||||
appCurrencyProvider = appCurrencyProvider,
|
||||
isAccountsModeProvider = isAccountsModeProvider,
|
||||
iGaslessFeeSupportedForNetwork = iGaslessFeeSupportedForNetwork,
|
||||
isGaslessFeeSupportedForNetwork = isGaslessFeeSupportedForNetwork,
|
||||
shouldShowAbMenu = false,
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -2,7 +2,6 @@ package com.tangem.feature.swap
|
|||
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import com.tangem.domain.appcurrency.model.AppCurrency
|
||||
import com.tangem.domain.models.currency.CryptoCurrencyStatus
|
||||
import com.tangem.domain.models.wallet.UserWallet
|
||||
import com.tangem.domain.models.wallet.UserWalletId
|
||||
import com.tangem.domain.swap.models.SwapCurrencyStatus
|
||||
|
|
@ -14,12 +13,10 @@ import com.tangem.feature.swap.domain.models.ui.*
|
|||
import com.tangem.feature.swap.models.*
|
||||
import com.tangem.feature.swap.models.states.FeeItemState
|
||||
import com.tangem.feature.swap.models.states.ProviderState
|
||||
import com.tangem.feature.swap.models.states.SwapNotificationUM
|
||||
import com.tangem.feature.swap.ui.StateBuilder
|
||||
import com.tangem.utils.Provider
|
||||
import io.mockk.every
|
||||
import io.mockk.mockk
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
import org.junit.jupiter.api.BeforeEach
|
||||
import org.junit.jupiter.api.Nested
|
||||
import org.junit.jupiter.api.Test
|
||||
|
|
@ -31,7 +28,7 @@ internal class StateBuilderQuotesTest {
|
|||
private val isBalanceHiddenProvider: Provider<Boolean> = mockk()
|
||||
private val appCurrencyProvider: Provider<AppCurrency> = mockk()
|
||||
private val isAccountsModeProvider: Provider<Boolean> = mockk()
|
||||
private val iGaslessFeeSupportedForNetwork: IsGaslessFeeSupportedForNetwork = mockk()
|
||||
private val isGaslessFeeSupportedForNetwork: IsGaslessFeeSupportedForNetwork = mockk()
|
||||
|
||||
private lateinit var sut: StateBuilder
|
||||
|
||||
|
|
@ -52,14 +49,15 @@ internal class StateBuilderQuotesTest {
|
|||
every { isBalanceHiddenProvider() } returns false
|
||||
every { appCurrencyProvider() } returns AppCurrency.Default
|
||||
every { isAccountsModeProvider() } returns false
|
||||
every { iGaslessFeeSupportedForNetwork(any()) } returns false
|
||||
every { isGaslessFeeSupportedForNetwork(any()) } returns false
|
||||
|
||||
sut = StateBuilder(
|
||||
actions = actions,
|
||||
isBalanceHiddenProvider = isBalanceHiddenProvider,
|
||||
appCurrencyProvider = appCurrencyProvider,
|
||||
isAccountsModeProvider = isAccountsModeProvider,
|
||||
iGaslessFeeSupportedForNetwork = iGaslessFeeSupportedForNetwork,
|
||||
isGaslessFeeSupportedForNetwork = isGaslessFeeSupportedForNetwork,
|
||||
shouldShowAbMenu = false,
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -4,13 +4,11 @@ import com.google.common.truth.Truth.assertThat
|
|||
import com.tangem.domain.appcurrency.model.AppCurrency
|
||||
import com.tangem.domain.models.wallet.UserWallet
|
||||
import com.tangem.domain.models.wallet.UserWalletId
|
||||
import com.tangem.domain.swap.models.SwapCurrencyStatus
|
||||
import com.tangem.domain.transaction.usecase.gasless.IsGaslessFeeSupportedForNetwork
|
||||
import com.tangem.feature.swap.domain.models.domain.*
|
||||
import com.tangem.feature.swap.domain.models.ui.*
|
||||
import com.tangem.feature.swap.model.SwapProcessDataState
|
||||
import com.tangem.feature.swap.models.*
|
||||
import com.tangem.feature.swap.models.states.FeeItemState
|
||||
import com.tangem.feature.swap.models.states.ProviderState
|
||||
import com.tangem.feature.swap.models.states.SwapNotificationUM
|
||||
import com.tangem.feature.swap.ui.StateBuilder
|
||||
|
|
@ -22,6 +20,8 @@ import kotlinx.collections.immutable.toImmutableList
|
|||
import org.junit.jupiter.api.BeforeEach
|
||||
import org.junit.jupiter.api.Nested
|
||||
import org.junit.jupiter.api.Test
|
||||
import org.junit.jupiter.params.ParameterizedTest
|
||||
import org.junit.jupiter.params.provider.EnumSource
|
||||
import java.math.BigDecimal
|
||||
|
||||
internal class StateBuilderSwapDataTest {
|
||||
|
|
@ -30,7 +30,7 @@ internal class StateBuilderSwapDataTest {
|
|||
private val isBalanceHiddenProvider: Provider<Boolean> = mockk()
|
||||
private val appCurrencyProvider: Provider<AppCurrency> = mockk()
|
||||
private val isAccountsModeProvider: Provider<Boolean> = mockk()
|
||||
private val iGaslessFeeSupportedForNetwork: IsGaslessFeeSupportedForNetwork = mockk()
|
||||
private val isGaslessFeeSupportedForNetwork: IsGaslessFeeSupportedForNetwork = mockk()
|
||||
|
||||
private lateinit var sut: StateBuilder
|
||||
|
||||
|
|
@ -51,14 +51,15 @@ internal class StateBuilderSwapDataTest {
|
|||
every { isBalanceHiddenProvider() } returns false
|
||||
every { appCurrencyProvider() } returns AppCurrency.Default
|
||||
every { isAccountsModeProvider() } returns false
|
||||
every { iGaslessFeeSupportedForNetwork(any()) } returns false
|
||||
every { isGaslessFeeSupportedForNetwork(any()) } returns false
|
||||
|
||||
sut = StateBuilder(
|
||||
actions = actions,
|
||||
isBalanceHiddenProvider = isBalanceHiddenProvider,
|
||||
appCurrencyProvider = appCurrencyProvider,
|
||||
isAccountsModeProvider = isAccountsModeProvider,
|
||||
iGaslessFeeSupportedForNetwork = iGaslessFeeSupportedForNetwork,
|
||||
isGaslessFeeSupportedForNetwork = isGaslessFeeSupportedForNetwork,
|
||||
shouldShowAbMenu = false,
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -134,9 +135,8 @@ internal class StateBuilderSwapDataTest {
|
|||
@Test
|
||||
fun `GIVEN notifications with PermissionNeeded WHEN called THEN PermissionNeeded is removed`() {
|
||||
val permissionNeeded = SwapNotificationUM.Info.PermissionNeeded(
|
||||
providerName = "TestProvider",
|
||||
fromTokenSymbol = "ETH",
|
||||
onApproveClick = {},
|
||||
onLearnMoreClick = {},
|
||||
)
|
||||
val otherNotification = SwapNotificationUM.Warning.SwapNotSupported
|
||||
val baseState = buildReadyState(coldWallet).copy(
|
||||
|
|
@ -333,10 +333,17 @@ internal class StateBuilderSwapDataTest {
|
|||
assertThat(result.swapButton.isEnabled).isFalse()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `WHEN called THEN swapButton isInProgress is false`() {
|
||||
@ParameterizedTest
|
||||
@EnumSource(
|
||||
value = SwapButton.Mode::class,
|
||||
mode = EnumSource.Mode.INCLUDE,
|
||||
names = ["SWAP_PROGRESSING", "TRANSFER_PROGRESSING"],
|
||||
)
|
||||
fun `WHEN called THEN swapButton isInProgress is false`(mode: SwapButton.Mode) {
|
||||
val baseState = buildReadyState(coldWallet).copy(
|
||||
swapButton = buildReadyState(coldWallet).swapButton.copy(isInProgress = true),
|
||||
swapButton = buildReadyState(coldWallet).swapButton.copy(
|
||||
mode = mode,
|
||||
),
|
||||
)
|
||||
|
||||
val result = sut.loadingPermissionState(baseState)
|
||||
|
|
@ -359,9 +366,8 @@ internal class StateBuilderSwapDataTest {
|
|||
@Test
|
||||
fun `GIVEN notifications with PermissionNeeded WHEN called THEN PermissionNeeded is replaced by ApprovalInProgressWarning`() {
|
||||
val permissionNeeded = SwapNotificationUM.Info.PermissionNeeded(
|
||||
providerName = "TestProvider",
|
||||
fromTokenSymbol = "ETH",
|
||||
onApproveClick = {},
|
||||
onLearnMoreClick = {},
|
||||
)
|
||||
val baseState = buildReadyState(coldWallet).copy(
|
||||
notifications = persistentListOf(permissionNeeded),
|
||||
|
|
|
|||
|
|
@ -0,0 +1,372 @@
|
|||
package com.tangem.feature.swap.converters
|
||||
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import com.tangem.core.ui.extensions.TextReference
|
||||
import com.tangem.core.ui.extensions.stringReference
|
||||
import com.tangem.domain.models.currency.CryptoCurrency
|
||||
import com.tangem.feature.swap.domain.models.SwapAmount
|
||||
import com.tangem.feature.swap.domain.models.domain.ExchangeProviderType
|
||||
import com.tangem.feature.swap.domain.models.domain.SwapProvider
|
||||
import com.tangem.feature.swap.domain.models.ui.PermissionDataState
|
||||
import com.tangem.feature.swap.domain.models.ui.TokenSwapInfo
|
||||
import com.tangem.domain.swap.models.SwapCurrencyStatus
|
||||
import com.tangem.feature.swap.models.states.PercentDifference
|
||||
import com.tangem.feature.swap.models.states.ProviderState
|
||||
import io.mockk.every
|
||||
import io.mockk.mockk
|
||||
import org.junit.jupiter.api.AfterEach
|
||||
import org.junit.jupiter.api.BeforeEach
|
||||
import org.junit.jupiter.api.Test
|
||||
import java.math.BigDecimal
|
||||
import java.util.Locale
|
||||
|
||||
internal class SwapProviderStateBuilderTest {
|
||||
|
||||
private var originalLocale: Locale = Locale.getDefault()
|
||||
|
||||
private val onProviderClick: (String) -> Unit = {}
|
||||
|
||||
@BeforeEach
|
||||
fun setUp() {
|
||||
originalLocale = Locale.getDefault()
|
||||
Locale.setDefault(Locale.US)
|
||||
}
|
||||
|
||||
@AfterEach
|
||||
fun tearDown() {
|
||||
Locale.setDefault(originalLocale)
|
||||
}
|
||||
|
||||
// region buildContentClickable
|
||||
|
||||
@Test
|
||||
fun `GIVEN best rate AND no FCA AND no permission WHEN buildContentClickable THEN BestTrade badge`() {
|
||||
val provider = provider(id = "1inch", isRecommended = false)
|
||||
val from = tokenInfo(symbol = "ETH", decimals = 18, amount = BigDecimal.ONE)
|
||||
val to = tokenInfo(symbol = "USDT", decimals = 6, amount = BigDecimal("3000"))
|
||||
|
||||
val result = SwapProviderStateBuilder.buildContentClickable(
|
||||
provider = provider,
|
||||
fromTokenInfo = from,
|
||||
toTokenInfo = to,
|
||||
permissionState = PermissionDataState.Empty,
|
||||
selectionType = ProviderState.SelectionType.CLICK,
|
||||
isBestRate = true,
|
||||
isNeedBestRateBadge = true,
|
||||
needApplyFCARestrictions = false,
|
||||
onProviderClick = onProviderClick,
|
||||
)
|
||||
|
||||
assertThat(result.additionalBadge).isEqualTo(ProviderState.AdditionalBadge.BestTrade)
|
||||
assertThat(result.percentLowerThenBest).isEqualTo(PercentDifference.Empty)
|
||||
assertThat(result.subtitle).isInstanceOf(TextReference.Str::class.java)
|
||||
val subtitle = result.subtitle as TextReference.Str
|
||||
assertThat(subtitle.value).contains("ETH")
|
||||
assertThat(subtitle.value).contains("USDT")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN recommended provider WHEN buildContentClickable THEN Recommended badge`() {
|
||||
val provider = provider(id = "any", isRecommended = true)
|
||||
val info = tokenInfo(symbol = "ETH", decimals = 18, amount = BigDecimal.ONE)
|
||||
|
||||
val result = SwapProviderStateBuilder.buildContentClickable(
|
||||
provider = provider,
|
||||
fromTokenInfo = info,
|
||||
toTokenInfo = info,
|
||||
permissionState = PermissionDataState.Empty,
|
||||
selectionType = ProviderState.SelectionType.CLICK,
|
||||
isBestRate = true,
|
||||
isNeedBestRateBadge = true,
|
||||
needApplyFCARestrictions = false,
|
||||
onProviderClick = onProviderClick,
|
||||
)
|
||||
|
||||
assertThat(result.additionalBadge).isEqualTo(ProviderState.AdditionalBadge.Recommended)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN permission required WHEN buildContentClickable THEN PermissionRequired badge`() {
|
||||
val provider = provider(id = "any", isRecommended = false)
|
||||
val info = tokenInfo(symbol = "ETH", decimals = 18, amount = BigDecimal.ONE)
|
||||
|
||||
val result = SwapProviderStateBuilder.buildContentClickable(
|
||||
provider = provider,
|
||||
fromTokenInfo = info,
|
||||
toTokenInfo = info,
|
||||
permissionState = PermissionDataState.PermissionRequired(
|
||||
isResetApproval = false,
|
||||
spenderAddress = "0xspender",
|
||||
),
|
||||
selectionType = ProviderState.SelectionType.CLICK,
|
||||
isBestRate = true,
|
||||
isNeedBestRateBadge = true,
|
||||
needApplyFCARestrictions = false,
|
||||
onProviderClick = onProviderClick,
|
||||
)
|
||||
|
||||
assertThat(result.additionalBadge).isEqualTo(ProviderState.AdditionalBadge.PermissionRequired)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN FCA restricted provider WHEN buildContentClickable THEN FCAWarningList badge`() {
|
||||
val provider = provider(id = "changelly", isRecommended = true)
|
||||
val info = tokenInfo(symbol = "ETH", decimals = 18, amount = BigDecimal.ONE)
|
||||
|
||||
val result = SwapProviderStateBuilder.buildContentClickable(
|
||||
provider = provider,
|
||||
fromTokenInfo = info,
|
||||
toTokenInfo = info,
|
||||
permissionState = PermissionDataState.PermissionRequired(
|
||||
isResetApproval = false,
|
||||
spenderAddress = "0xspender",
|
||||
),
|
||||
selectionType = ProviderState.SelectionType.CLICK,
|
||||
isBestRate = true,
|
||||
isNeedBestRateBadge = true,
|
||||
needApplyFCARestrictions = true,
|
||||
onProviderClick = onProviderClick,
|
||||
)
|
||||
|
||||
assertThat(result.additionalBadge).isEqualTo(ProviderState.AdditionalBadge.FCAWarningList)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN best rate badge disabled WHEN buildContentClickable THEN Empty badge`() {
|
||||
val provider = provider(id = "any", isRecommended = false)
|
||||
val info = tokenInfo(symbol = "ETH", decimals = 18, amount = BigDecimal.ONE)
|
||||
|
||||
val result = SwapProviderStateBuilder.buildContentClickable(
|
||||
provider = provider,
|
||||
fromTokenInfo = info,
|
||||
toTokenInfo = info,
|
||||
permissionState = PermissionDataState.Empty,
|
||||
selectionType = ProviderState.SelectionType.CLICK,
|
||||
isBestRate = true,
|
||||
isNeedBestRateBadge = false,
|
||||
needApplyFCARestrictions = false,
|
||||
onProviderClick = onProviderClick,
|
||||
)
|
||||
|
||||
assertThat(result.additionalBadge).isEqualTo(ProviderState.AdditionalBadge.Empty)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN provider WHEN buildContentClickable THEN content carries provider identity`() {
|
||||
val provider = provider(id = "1inch", isRecommended = false, name = "1inch", iconUrl = "https://x")
|
||||
val info = tokenInfo(symbol = "ETH", decimals = 18, amount = BigDecimal.ONE)
|
||||
|
||||
val result = SwapProviderStateBuilder.buildContentClickable(
|
||||
provider = provider,
|
||||
fromTokenInfo = info,
|
||||
toTokenInfo = info,
|
||||
permissionState = PermissionDataState.Empty,
|
||||
selectionType = ProviderState.SelectionType.CLICK,
|
||||
isBestRate = false,
|
||||
isNeedBestRateBadge = false,
|
||||
needApplyFCARestrictions = false,
|
||||
onProviderClick = onProviderClick,
|
||||
)
|
||||
|
||||
assertThat(result.id).isEqualTo("1inch")
|
||||
assertThat(result.name).isEqualTo("1inch")
|
||||
assertThat(result.iconUrl).isEqualTo("https://x")
|
||||
assertThat(result.type).isEqualTo("DEX")
|
||||
assertThat(result.selectionType).isEqualTo(ProviderState.SelectionType.CLICK)
|
||||
assertThat(result.namePrefix).isEqualTo(ProviderState.PrefixType.NONE)
|
||||
}
|
||||
|
||||
// endregion
|
||||
|
||||
// region buildContentSelectable
|
||||
|
||||
@Test
|
||||
fun `GIVEN provider in pricesLowerBest WHEN buildContentSelectable THEN percentLowerThenBest is mapped`() {
|
||||
val provider = provider(id = "1inch", isRecommended = false)
|
||||
val info = tokenInfo(symbol = "USDT", decimals = 6, amount = BigDecimal("100"))
|
||||
|
||||
val result = SwapProviderStateBuilder.buildContentSelectable(
|
||||
provider = provider,
|
||||
toTokenInfo = info,
|
||||
permissionState = PermissionDataState.Empty,
|
||||
pricesLowerBest = mapOf("1inch" to 0.5f),
|
||||
selectionType = ProviderState.SelectionType.SELECT,
|
||||
needApplyFCARestrictions = false,
|
||||
onProviderClick = onProviderClick,
|
||||
)
|
||||
|
||||
assertThat(result.percentLowerThenBest).isEqualTo(PercentDifference.Value(0.5f))
|
||||
assertThat(result.subtitle).isInstanceOf(TextReference.Str::class.java)
|
||||
val subtitle = result.subtitle as TextReference.Str
|
||||
assertThat(subtitle.value).contains("USDT")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN provider not in pricesLowerBest WHEN buildContentSelectable THEN percentLowerThenBest is zero`() {
|
||||
val provider = provider(id = "any", isRecommended = false)
|
||||
val info = tokenInfo(symbol = "USDT", decimals = 6, amount = BigDecimal("100"))
|
||||
|
||||
val result = SwapProviderStateBuilder.buildContentSelectable(
|
||||
provider = provider,
|
||||
toTokenInfo = info,
|
||||
permissionState = PermissionDataState.Empty,
|
||||
pricesLowerBest = emptyMap(),
|
||||
selectionType = ProviderState.SelectionType.SELECT,
|
||||
needApplyFCARestrictions = false,
|
||||
onProviderClick = onProviderClick,
|
||||
)
|
||||
|
||||
assertThat(result.percentLowerThenBest).isEqualTo(PercentDifference.Value(0f))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN best rate badge inputs WHEN buildContentSelectable THEN BestTrade badge is never set`() {
|
||||
val provider = provider(id = "any", isRecommended = false)
|
||||
val info = tokenInfo(symbol = "USDT", decimals = 6, amount = BigDecimal("100"))
|
||||
|
||||
val result = SwapProviderStateBuilder.buildContentSelectable(
|
||||
provider = provider,
|
||||
toTokenInfo = info,
|
||||
permissionState = PermissionDataState.Empty,
|
||||
pricesLowerBest = emptyMap(),
|
||||
selectionType = ProviderState.SelectionType.SELECT,
|
||||
needApplyFCARestrictions = false,
|
||||
onProviderClick = onProviderClick,
|
||||
)
|
||||
|
||||
assertThat(result.additionalBadge).isEqualTo(ProviderState.AdditionalBadge.Empty)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN permission required WHEN buildContentSelectable THEN PermissionRequired badge`() {
|
||||
val provider = provider(id = "any", isRecommended = false)
|
||||
val info = tokenInfo(symbol = "USDT", decimals = 6, amount = BigDecimal("100"))
|
||||
|
||||
val result = SwapProviderStateBuilder.buildContentSelectable(
|
||||
provider = provider,
|
||||
toTokenInfo = info,
|
||||
permissionState = PermissionDataState.PermissionRequired(
|
||||
isResetApproval = false,
|
||||
spenderAddress = "0xspender",
|
||||
),
|
||||
pricesLowerBest = emptyMap(),
|
||||
selectionType = ProviderState.SelectionType.SELECT,
|
||||
needApplyFCARestrictions = false,
|
||||
onProviderClick = onProviderClick,
|
||||
)
|
||||
|
||||
assertThat(result.additionalBadge).isEqualTo(ProviderState.AdditionalBadge.PermissionRequired)
|
||||
}
|
||||
|
||||
// endregion
|
||||
|
||||
// region buildAvailableFrom
|
||||
|
||||
@Test
|
||||
fun `GIVEN alert text WHEN buildAvailableFrom THEN subtitle is the alert text`() {
|
||||
val provider = provider(id = "any", isRecommended = false)
|
||||
val alert: TextReference = stringReference("min amount 0.01 ETH")
|
||||
|
||||
val result = SwapProviderStateBuilder.buildAvailableFrom(
|
||||
provider = provider,
|
||||
alertText = alert,
|
||||
selectionType = ProviderState.SelectionType.SELECT,
|
||||
needApplyFCARestrictions = false,
|
||||
onProviderClick = onProviderClick,
|
||||
)
|
||||
|
||||
assertThat(result.subtitle).isEqualTo(alert)
|
||||
assertThat(result.percentLowerThenBest).isEqualTo(PercentDifference.Empty)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN FCA restricted WHEN buildAvailableFrom THEN FCAWarningList badge`() {
|
||||
val provider = provider(id = "okx-on-chain", isRecommended = true)
|
||||
|
||||
val result = SwapProviderStateBuilder.buildAvailableFrom(
|
||||
provider = provider,
|
||||
alertText = TextReference.EMPTY,
|
||||
selectionType = ProviderState.SelectionType.SELECT,
|
||||
needApplyFCARestrictions = true,
|
||||
onProviderClick = onProviderClick,
|
||||
)
|
||||
|
||||
assertThat(result.additionalBadge).isEqualTo(ProviderState.AdditionalBadge.FCAWarningList)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN recommended WHEN buildAvailableFrom THEN Recommended badge`() {
|
||||
val provider = provider(id = "any", isRecommended = true)
|
||||
|
||||
val result = SwapProviderStateBuilder.buildAvailableFrom(
|
||||
provider = provider,
|
||||
alertText = TextReference.EMPTY,
|
||||
selectionType = ProviderState.SelectionType.SELECT,
|
||||
needApplyFCARestrictions = false,
|
||||
onProviderClick = onProviderClick,
|
||||
)
|
||||
|
||||
assertThat(result.additionalBadge).isEqualTo(ProviderState.AdditionalBadge.Recommended)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN no flags WHEN buildAvailableFrom THEN Empty badge`() {
|
||||
val provider = provider(id = "any", isRecommended = false)
|
||||
|
||||
val result = SwapProviderStateBuilder.buildAvailableFrom(
|
||||
provider = provider,
|
||||
alertText = TextReference.EMPTY,
|
||||
selectionType = ProviderState.SelectionType.SELECT,
|
||||
needApplyFCARestrictions = false,
|
||||
onProviderClick = onProviderClick,
|
||||
)
|
||||
|
||||
assertThat(result.additionalBadge).isEqualTo(ProviderState.AdditionalBadge.Empty)
|
||||
}
|
||||
|
||||
// endregion
|
||||
|
||||
// region buildSelectableSubtitle
|
||||
|
||||
@Test
|
||||
fun `GIVEN to token info WHEN buildSelectableSubtitle THEN string contains symbol`() {
|
||||
val info = tokenInfo(symbol = "USDT", decimals = 6, amount = BigDecimal("100"))
|
||||
|
||||
val result = SwapProviderStateBuilder.buildSelectableSubtitle(info)
|
||||
|
||||
assertThat(result).isInstanceOf(TextReference.Str::class.java)
|
||||
val subtitle = result as TextReference.Str
|
||||
assertThat(subtitle.value).contains("USDT")
|
||||
assertThat(subtitle.value).contains("100")
|
||||
}
|
||||
|
||||
// endregion
|
||||
|
||||
private fun provider(
|
||||
id: String,
|
||||
isRecommended: Boolean,
|
||||
name: String = "Provider",
|
||||
iconUrl: String = "https://icon",
|
||||
): SwapProvider = mockk {
|
||||
every { providerId } returns id
|
||||
every { this@mockk.name } returns name
|
||||
every { imageLarge } returns iconUrl
|
||||
every { type } returns ExchangeProviderType.DEX
|
||||
every { this@mockk.isRecommended } returns isRecommended
|
||||
}
|
||||
|
||||
private fun tokenInfo(symbol: String, decimals: Int, amount: BigDecimal): TokenSwapInfo {
|
||||
val currency = mockk<CryptoCurrency.Coin> {
|
||||
every { this@mockk.symbol } returns symbol
|
||||
every { this@mockk.decimals } returns decimals
|
||||
}
|
||||
val swapStatus = mockk<SwapCurrencyStatus> {
|
||||
every { this@mockk.currency } returns currency
|
||||
}
|
||||
return TokenSwapInfo(
|
||||
tokenAmount = SwapAmount(value = amount, decimals = decimals),
|
||||
amountFiat = BigDecimal.ZERO,
|
||||
swapCurrencyStatus = swapStatus,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,247 @@
|
|||
package com.tangem.feature.swap.ui.transfer
|
||||
|
||||
import androidx.compose.ui.text.TextRange
|
||||
import androidx.compose.ui.text.input.TextFieldValue
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import com.tangem.common.ui.account.AccountTitleUM
|
||||
import com.tangem.common.ui.account.CryptoPortfolioIconConverter
|
||||
import com.tangem.common.ui.account.toUM
|
||||
import com.tangem.common.ui.components.currency.icon.converter.CryptoCurrencyToIconStateConverter
|
||||
import com.tangem.common.ui.userwallet.ext.walletInterationIcon
|
||||
import com.tangem.core.ui.extensions.resourceReference
|
||||
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.models.wallet.UserWalletId
|
||||
import com.tangem.domain.swap.models.SwapCurrencyStatus
|
||||
import com.tangem.feature.swap.buildSwapCurrencyStatus
|
||||
import com.tangem.feature.swap.domain.models.SwapAmount
|
||||
import com.tangem.feature.swap.domain.models.ui.PriceImpact
|
||||
import com.tangem.feature.swap.domain.models.ui.SwapState
|
||||
import com.tangem.feature.swap.domain.models.ui.TokenSwapInfo
|
||||
import com.tangem.feature.swap.models.*
|
||||
import com.tangem.feature.swap.models.states.ProviderState
|
||||
import com.tangem.feature.swap.presentation.R
|
||||
import com.tangem.feature.swap.utils.formatToUIRepresentation
|
||||
import io.mockk.every
|
||||
import io.mockk.mockk
|
||||
import org.junit.jupiter.api.Test
|
||||
import org.junit.jupiter.api.TestInstance
|
||||
import java.math.BigDecimal
|
||||
|
||||
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
|
||||
internal class SwapTransferStateBuilderTest {
|
||||
|
||||
private val actions: UiActions = mockk(relaxed = true)
|
||||
private val sut = SwapTransferStateBuilder()
|
||||
|
||||
private val userWalletId = UserWalletId(stringValue = "deadbeef")
|
||||
private val coldWallet: UserWallet.Cold = mockk(relaxed = true) {
|
||||
every { walletId } returns userWalletId
|
||||
}
|
||||
private val fromCurrencyStatus: SwapCurrencyStatus = buildSwapCurrencyStatus(coldWallet)
|
||||
private val toCurrencyStatus: SwapCurrencyStatus = buildSwapCurrencyStatus(coldWallet)
|
||||
private val iconConverter = CryptoCurrencyToIconStateConverter()
|
||||
private val fromIcon = iconConverter.convert(fromCurrencyStatus.status)
|
||||
private val toIcon = iconConverter.convert(toCurrencyStatus.status)
|
||||
|
||||
@Test
|
||||
fun `GIVEN accounts mode enabled WHEN createTransferState THEN cards expose Account titles for from and to`() {
|
||||
val transferState = buildTransferState(
|
||||
fromAmount = BigDecimal("1.5"),
|
||||
toAmount = BigDecimal("1.5"),
|
||||
isAccountsMode = true,
|
||||
)
|
||||
|
||||
val result = sut.createTransferState(actions, transferState, baseStateHolder())
|
||||
|
||||
val portfolioAccount = fromCurrencyStatus.account as Account.CryptoPortfolio
|
||||
val expectedAccountIcon = CryptoPortfolioIconConverter.convert(portfolioAccount.icon)
|
||||
val expectedAccountName = portfolioAccount.accountName.toUM().value
|
||||
val sendType = (result.sendCardData as SwapCardState.SwapCardData).type as TransactionCardType.Inputtable
|
||||
val receiveType = (result.receiveCardData as SwapCardState.SwapCardData).type as TransactionCardType.ReadOnly
|
||||
assertThat(sendType.accountTitleUM).isEqualTo(
|
||||
AccountTitleUM.Account(
|
||||
prefixText = resourceReference(R.string.swapping_from_account_title),
|
||||
name = expectedAccountName,
|
||||
icon = expectedAccountIcon,
|
||||
),
|
||||
)
|
||||
assertThat(receiveType.accountTitleUM).isEqualTo(
|
||||
AccountTitleUM.Account(
|
||||
prefixText = resourceReference(R.string.swapping_to_account_title),
|
||||
name = expectedAccountName,
|
||||
icon = expectedAccountIcon,
|
||||
),
|
||||
)
|
||||
assertSharedCardShape(
|
||||
result = result,
|
||||
transferState = transferState,
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN accounts mode disabled WHEN createTransferState THEN cards fall back to Text titles for from and to`() {
|
||||
val transferState = buildTransferState(
|
||||
fromAmount = BigDecimal("2"),
|
||||
toAmount = BigDecimal("2"),
|
||||
isAccountsMode = false,
|
||||
)
|
||||
|
||||
val result = sut.createTransferState(actions, transferState, baseStateHolder())
|
||||
|
||||
val sendType = (result.sendCardData as SwapCardState.SwapCardData).type as TransactionCardType.Inputtable
|
||||
val receiveType = (result.receiveCardData as SwapCardState.SwapCardData).type as TransactionCardType.ReadOnly
|
||||
assertThat(sendType.accountTitleUM).isEqualTo(
|
||||
AccountTitleUM.Text(resourceReference(R.string.swapping_from_title_v2)),
|
||||
)
|
||||
assertThat(receiveType.accountTitleUM).isEqualTo(
|
||||
AccountTitleUM.Text(resourceReference(R.string.swapping_to_title)),
|
||||
)
|
||||
assertSharedCardShape(
|
||||
result = result,
|
||||
transferState = transferState,
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN insufficient balance and accounts mode disabled WHEN createTransferState THEN from card shows insufficient funds title and error and swap is disabled`() {
|
||||
val transferState = buildTransferState(
|
||||
fromAmount = BigDecimal("10"),
|
||||
toAmount = BigDecimal("10"),
|
||||
isAccountsMode = false,
|
||||
isInsufficientBalance = true,
|
||||
)
|
||||
|
||||
val result = sut.createTransferState(actions, transferState, baseStateHolder())
|
||||
|
||||
val sendType = (result.sendCardData as SwapCardState.SwapCardData).type as TransactionCardType.Inputtable
|
||||
val receiveType = (result.receiveCardData as SwapCardState.SwapCardData).type as TransactionCardType.ReadOnly
|
||||
assertThat(sendType.accountTitleUM).isEqualTo(
|
||||
AccountTitleUM.Text(resourceReference(R.string.swapping_insufficient_funds)),
|
||||
)
|
||||
assertThat(sendType.inputError).isEqualTo(TransactionCardType.InputError.InsufficientFunds)
|
||||
assertThat(receiveType.accountTitleUM).isEqualTo(
|
||||
AccountTitleUM.Text(resourceReference(R.string.swapping_to_title)),
|
||||
)
|
||||
assertThat(result.isInsufficientFunds).isTrue()
|
||||
assertThat(result.swapButton.isEnabled).isFalse()
|
||||
assertThat(result.swapButton.mode).isEqualTo(SwapButton.Mode.TRANSFER)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN insufficient balance and accounts mode enabled WHEN createTransferState THEN from card overrides Account title with insufficient funds text`() {
|
||||
val transferState = buildTransferState(
|
||||
fromAmount = BigDecimal("10"),
|
||||
toAmount = BigDecimal("10"),
|
||||
isAccountsMode = true,
|
||||
isInsufficientBalance = true,
|
||||
)
|
||||
|
||||
val result = sut.createTransferState(actions, transferState, baseStateHolder())
|
||||
|
||||
val portfolioAccount = toCurrencyStatus.account as Account.CryptoPortfolio
|
||||
val expectedAccountIcon = CryptoPortfolioIconConverter.convert(portfolioAccount.icon)
|
||||
val expectedAccountName = portfolioAccount.accountName.toUM().value
|
||||
val sendType = (result.sendCardData as SwapCardState.SwapCardData).type as TransactionCardType.Inputtable
|
||||
val receiveType = (result.receiveCardData as SwapCardState.SwapCardData).type as TransactionCardType.ReadOnly
|
||||
assertThat(sendType.accountTitleUM).isEqualTo(
|
||||
AccountTitleUM.Text(resourceReference(R.string.swapping_insufficient_funds)),
|
||||
)
|
||||
assertThat(sendType.inputError).isEqualTo(TransactionCardType.InputError.InsufficientFunds)
|
||||
assertThat(receiveType.accountTitleUM).isEqualTo(
|
||||
AccountTitleUM.Account(
|
||||
prefixText = resourceReference(R.string.swapping_to_account_title),
|
||||
name = expectedAccountName,
|
||||
icon = expectedAccountIcon,
|
||||
),
|
||||
)
|
||||
assertThat(result.isInsufficientFunds).isTrue()
|
||||
assertThat(result.swapButton.isEnabled).isFalse()
|
||||
}
|
||||
|
||||
private fun assertSharedCardShape(
|
||||
result: SwapStateHolder,
|
||||
transferState: SwapState.Transfer,
|
||||
) {
|
||||
val sendCard = result.sendCardData as SwapCardState.SwapCardData
|
||||
val receiveCard = result.receiveCardData as SwapCardState.SwapCardData
|
||||
val expectedFromText = transferState.fromTokenInfo.tokenAmount.formatToUIRepresentation()
|
||||
val expectedToText = transferState.toTokenInfo.tokenAmount.formatToUIRepresentation()
|
||||
assertThat(sendCard.amountTextFieldValue).isEqualTo(
|
||||
TextFieldValue(text = expectedFromText, selection = TextRange(index = expectedFromText.length)),
|
||||
)
|
||||
assertThat(receiveCard.amountTextFieldValue).isEqualTo(
|
||||
TextFieldValue(text = expectedToText, selection = TextRange(index = expectedToText.length)),
|
||||
)
|
||||
assertThat(sendCard.currencyIconState).isEqualTo(fromIcon)
|
||||
assertThat(receiveCard.currencyIconState).isEqualTo(toIcon)
|
||||
assertThat(sendCard.isBalanceHidden).isEqualTo(transferState.isBalanceHidden)
|
||||
assertThat(receiveCard.isBalanceHidden).isEqualTo(transferState.isBalanceHidden)
|
||||
assertThat((sendCard.type is TransactionCardType.Inputtable)).isTrue()
|
||||
assertThat(receiveCard.type).isInstanceOf(TransactionCardType.ReadOnly::class.java)
|
||||
assertThat(result.swapButton).isEqualTo(
|
||||
SwapButton(
|
||||
walletInteractionIcon = walletInterationIcon(transferState.userWallet),
|
||||
isEnabled = !transferState.isInsufficientBalance,
|
||||
mode = SwapButton.Mode.TRANSFER,
|
||||
onClick = actions.onTransferClick,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
private fun buildTransferState(
|
||||
fromAmount: BigDecimal,
|
||||
toAmount: BigDecimal,
|
||||
isAccountsMode: Boolean,
|
||||
isInsufficientBalance: Boolean = false,
|
||||
): SwapState.Transfer {
|
||||
val fromInfo = TokenSwapInfo(
|
||||
tokenAmount = SwapAmount(value = fromAmount, decimals = fromCurrencyStatus.currency.decimals),
|
||||
amountFiat = fromAmount * QUOTE,
|
||||
swapCurrencyStatus = fromCurrencyStatus,
|
||||
)
|
||||
val toInfo = TokenSwapInfo(
|
||||
tokenAmount = SwapAmount(value = toAmount, decimals = toCurrencyStatus.currency.decimals),
|
||||
amountFiat = toAmount * QUOTE,
|
||||
swapCurrencyStatus = toCurrencyStatus,
|
||||
)
|
||||
return SwapState.Transfer(
|
||||
userWallet = coldWallet,
|
||||
fromTokenInfo = fromInfo,
|
||||
toTokenInfo = toInfo,
|
||||
isInsufficientBalance = isInsufficientBalance,
|
||||
appCurrency = AppCurrency.Default,
|
||||
isBalanceHidden = false,
|
||||
isAccountsMode = isAccountsMode,
|
||||
)
|
||||
}
|
||||
|
||||
private fun baseStateHolder(): SwapStateHolder = SwapStateHolder(
|
||||
sendCardData = SwapCardState.Loading(
|
||||
type = TransactionCardType.ReadOnly(
|
||||
accountTitleUM = AccountTitleUM.Text(resourceReference(R.string.swapping_from_title_v2)),
|
||||
),
|
||||
),
|
||||
receiveCardData = SwapCardState.Loading(
|
||||
type = TransactionCardType.ReadOnly(
|
||||
accountTitleUM = AccountTitleUM.Text(resourceReference(R.string.swapping_to_title)),
|
||||
),
|
||||
),
|
||||
isInsufficientFunds = false,
|
||||
changeCardsButtonState = ChangeCardsButtonState.ENABLED,
|
||||
providerState = ProviderState.Empty(),
|
||||
priceImpact = PriceImpact.Empty,
|
||||
swapButton = SwapButton(walletInteractionIcon = null, isEnabled = false, onClick = {}),
|
||||
shouldShowMaxAmount = false,
|
||||
onRefresh = {},
|
||||
onBackClicked = {},
|
||||
onChangeCardsClicked = {},
|
||||
onSelectTokenClick = {},
|
||||
onSuccess = {},
|
||||
)
|
||||
|
||||
private companion object {
|
||||
val QUOTE: BigDecimal = BigDecimal("2000")
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue