Updated on 2026-08-14
This commit is contained in:
commit
d0b35bc331
680 changed files with 26556 additions and 2709 deletions
|
|
@ -20,22 +20,16 @@ import com.tangem.core.decompose.context.AppComponentContext
|
|||
import com.tangem.core.decompose.context.childByContext
|
||||
import com.tangem.core.decompose.model.getOrCreateModel
|
||||
import com.tangem.core.decompose.navigation.inner.InnerRouter
|
||||
import com.tangem.core.ui.R
|
||||
import com.tangem.core.ui.decompose.ComposableContentComponent
|
||||
import com.tangem.core.ui.extensions.resourceReference
|
||||
import com.tangem.core.ui.extensions.wrappedList
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.core.ui.utils.parseBigDecimalOrNull
|
||||
import com.tangem.domain.models.currency.CryptoCurrencyStatus
|
||||
import com.tangem.domain.models.wallet.isHotWallet
|
||||
import com.tangem.feature.swap.component.SwapFeeSelectorBlockComponent
|
||||
import com.tangem.feature.swap.domain.models.ui.PermissionDataState
|
||||
import com.tangem.feature.swap.model.SwapModel
|
||||
import com.tangem.feature.swap.models.SwapPermissionUM
|
||||
import com.tangem.feature.swap.router.SwapRoute
|
||||
import com.tangem.feature.swap.ui.SwapScreen
|
||||
import com.tangem.feature.swap.ui.SwapSuccessScreen
|
||||
import com.tangem.features.approval.api.GiveApprovalComponent
|
||||
import com.tangem.features.approval.api.GiveApprovalEntryComponent
|
||||
import com.tangem.features.commonfeatures.api.choosetoken.ChooseTokenComponent
|
||||
import com.tangem.features.send.v2.api.analytics.CommonSendAnalyticEvents
|
||||
import com.tangem.features.swap.SwapComponent
|
||||
|
|
@ -50,7 +44,7 @@ internal class DefaultSwapComponent @AssistedInject constructor(
|
|||
@Assisted appComponentContext: AppComponentContext,
|
||||
@Assisted private val params: SwapComponent.Params,
|
||||
private val swapFeeSelectorBlockComponentFactory: SwapFeeSelectorBlockComponent.Factory,
|
||||
private val giveApprovalComponentFactory: GiveApprovalComponent.Factory,
|
||||
private val giveApprovalEntryComponentFactory: GiveApprovalEntryComponent.Factory,
|
||||
private val chooseTokenComponentFactory: ChooseTokenComponent.Factory,
|
||||
) : SwapComponent, AppComponentContext by appComponentContext {
|
||||
|
||||
|
|
@ -78,12 +72,10 @@ internal class DefaultSwapComponent @AssistedInject constructor(
|
|||
source = model.approvalSlotNavigation,
|
||||
serializer = null,
|
||||
handleBackButton = true,
|
||||
childFactory = { _, factoryContext ->
|
||||
val approvalParams = getApprovalParams()
|
||||
?: error("Approval params are not available")
|
||||
giveApprovalComponentFactory.create(
|
||||
childFactory = { params, factoryContext ->
|
||||
giveApprovalEntryComponentFactory.create(
|
||||
context = childByContext(factoryContext),
|
||||
params = approvalParams,
|
||||
params = GiveApprovalEntryComponent.Params(params),
|
||||
)
|
||||
},
|
||||
)
|
||||
|
|
@ -123,6 +115,7 @@ internal class DefaultSwapComponent @AssistedInject constructor(
|
|||
analyticsCategoryName = CommonSendAnalyticEvents.SWAP_CATEGORY,
|
||||
analyticsSendSource = CommonSendAnalyticEvents.CommonSendSource.Swap,
|
||||
),
|
||||
isTransferMode = config.isTransferMode,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
|
@ -143,6 +136,7 @@ internal class DefaultSwapComponent @AssistedInject constructor(
|
|||
data class FeeSelectorConfig(
|
||||
val sendingCurrencyStatus: CryptoCurrencyStatus,
|
||||
val feeCurrencyStatus: CryptoCurrencyStatus,
|
||||
val isTransferMode: Boolean,
|
||||
)
|
||||
|
||||
@Suppress("LongMethod", "CyclomaticComplexMethod")
|
||||
|
|
@ -151,22 +145,21 @@ internal class DefaultSwapComponent @AssistedInject constructor(
|
|||
val dataState by model.dataStateStateFlow.collectAsStateWithLifecycle()
|
||||
val fromCryptoCurrency by remember { derivedStateOf { dataState.fromSwapCurrencyStatus?.status } }
|
||||
val feePaidCryptoCurrency by remember { derivedStateOf { dataState.feePaidCryptoCurrency } }
|
||||
val isInTransferMode by remember { derivedStateOf { dataState.currentTransferState != null } }
|
||||
val shouldHideBlock by remember {
|
||||
derivedStateOf {
|
||||
val isAmountEmptyOrZero = dataState.amount?.parseBigDecimalOrNull().isNullOrZero()
|
||||
val isInsufficientFunds = model.uiState.isInsufficientFunds
|
||||
val isProviderMissing = dataState.selectedProvider == null
|
||||
val loadedState = dataState.getCurrentLoadedSwapState()
|
||||
val isPermissionNotReady = loadedState?.permissionState !is PermissionDataState.Empty
|
||||
val isInTransferMode = dataState.currentTransferState != null
|
||||
val isSwapNotReady = !isInTransferMode && (isProviderMissing || isPermissionNotReady)
|
||||
val isPermissionNotNeeded = model.isPermissionNotNeeded
|
||||
val isSwapNotReady = !isInTransferMode && (isProviderMissing || !isPermissionNotNeeded)
|
||||
val isTangemPayWithdrawal = model.isTangemPayWithdrawal()
|
||||
|
||||
isAmountEmptyOrZero || isInsufficientFunds || isSwapNotReady || isTangemPayWithdrawal
|
||||
}
|
||||
}
|
||||
|
||||
LaunchedEffect(fromCryptoCurrency, feePaidCryptoCurrency, shouldHideBlock) {
|
||||
LaunchedEffect(fromCryptoCurrency, feePaidCryptoCurrency, shouldHideBlock, isInTransferMode) {
|
||||
if (shouldHideBlock) {
|
||||
TangemLogger.e(
|
||||
messageString = "Dismissing fee selector: " +
|
||||
|
|
@ -194,6 +187,7 @@ internal class DefaultSwapComponent @AssistedInject constructor(
|
|||
FeeSelectorConfig(
|
||||
sendingCurrencyStatus = sendingCryptoCurrencyStatus,
|
||||
feeCurrencyStatus = feeCurrencyStatus,
|
||||
isTransferMode = isInTransferMode,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
|
@ -246,34 +240,6 @@ internal class DefaultSwapComponent @AssistedInject constructor(
|
|||
}
|
||||
}
|
||||
|
||||
private fun getApprovalParams(): GiveApprovalComponent.Params? {
|
||||
val permissionState = model.uiState.permissionUM as? SwapPermissionUM.PermissionRequired ?: return null
|
||||
val fromSwapCurrencyStatus = model.dataState.fromSwapCurrencyStatus ?: return null
|
||||
val feeCryptoCurrency = model.dataState.feePaidCryptoCurrency ?: return null
|
||||
val providerName = model.dataState.selectedProvider?.name.orEmpty()
|
||||
val isHoldToConfirm = fromSwapCurrencyStatus.userWallet.isHotWallet
|
||||
|
||||
return GiveApprovalComponent.Params(
|
||||
userWalletId = params.userWalletId,
|
||||
cryptoCurrencyStatus = fromSwapCurrencyStatus.status,
|
||||
feeCryptoCurrencyStatus = feeCryptoCurrency,
|
||||
amount = model.dataState.amount.orEmpty(),
|
||||
spenderAddress = permissionState.spenderAddress,
|
||||
amountFooter = if (permissionState.isResetApproval) {
|
||||
resourceReference(R.string.update_approval_permission_subtitle)
|
||||
} else {
|
||||
resourceReference(
|
||||
id = R.string.give_permission_swap_subtitle,
|
||||
formatArgs = wrappedList(providerName, fromSwapCurrencyStatus.currency.symbol),
|
||||
)
|
||||
},
|
||||
feeFooter = resourceReference(R.string.swap_give_permission_fee_footer),
|
||||
isResetApproval = permissionState.isResetApproval,
|
||||
isHoldToConfirm = isHoldToConfirm,
|
||||
callback = model.approvalCallback,
|
||||
)
|
||||
}
|
||||
|
||||
private fun onChildBack() {
|
||||
val isEmptyStack = childStack.value.backStack.isEmpty()
|
||||
val isSuccess = model.uiState.successState != null
|
||||
|
|
|
|||
|
|
@ -9,12 +9,16 @@ internal class DefaultSwapFeatureToggles @Inject constructor(
|
|||
featureTogglesManager: FeatureTogglesManager,
|
||||
) : SwapFeatureToggles {
|
||||
|
||||
override val isYieldSwapEnabled: Boolean = featureTogglesManager.isFeatureEnabled(
|
||||
toggle = FeatureToggles.TWI_1326_YIELD_MODE_SWAP_ENABLED,
|
||||
)
|
||||
|
||||
override val isSwapSwitchToTransferEnabled: Boolean = featureTogglesManager.isFeatureEnabled(
|
||||
toggle = FeatureToggles.AND_15207_SWAP_SWITCH_TO_TRANSFER_ENABLED,
|
||||
)
|
||||
|
||||
override val isSwapIntegratedApproveEnabled: Boolean = featureTogglesManager.isFeatureEnabled(
|
||||
toggle = FeatureToggles.SWAP_INTEGRATED_APPROVE,
|
||||
toggle = FeatureToggles.AND_15120_SWAP_INTEGRATED_APPROVE,
|
||||
)
|
||||
|
||||
override val isSwapAbEnabled: Boolean = featureTogglesManager.isFeatureEnabled(
|
||||
|
|
|
|||
|
|
@ -1,11 +1,11 @@
|
|||
package com.tangem.feature.swap.analytics
|
||||
|
||||
import com.tangem.core.analytics.models.AnalyticsEvent
|
||||
import com.tangem.core.analytics.models.AnalyticsParam
|
||||
import com.tangem.core.analytics.models.AnalyticsParam.Key.ACCOUNT_DERIVATION_FROM
|
||||
import com.tangem.core.analytics.models.AnalyticsParam.Key.ACCOUNT_DERIVATION_TO
|
||||
import com.tangem.core.analytics.models.AnalyticsParam.Key.ERROR_CODE
|
||||
import com.tangem.core.analytics.models.AnalyticsParam.Key.ERROR_MESSAGE
|
||||
import com.tangem.core.analytics.models.AnalyticsParam
|
||||
import com.tangem.core.analytics.models.AnalyticsParam.Key.FEE_TOKEN
|
||||
import com.tangem.core.analytics.models.AnalyticsParam.Key.PROVIDER
|
||||
import com.tangem.core.analytics.models.AnalyticsParam.Key.RECEIVE_BLOCKCHAIN
|
||||
|
|
@ -15,6 +15,7 @@ import com.tangem.core.analytics.models.AnalyticsParam.Key.SEND_TOKEN
|
|||
import com.tangem.core.analytics.models.AppsFlyerIncludedEvent
|
||||
import com.tangem.core.analytics.models.getReferralParams
|
||||
import com.tangem.domain.models.currency.CryptoCurrency
|
||||
import com.tangem.domain.models.network.Network
|
||||
import com.tangem.domain.swap.models.PredefinedPercentAmount
|
||||
import com.tangem.feature.swap.domain.models.domain.SwapProvider
|
||||
import com.tangem.feature.swap.domain.models.domain.SwapUIMode
|
||||
|
|
@ -297,6 +298,48 @@ sealed class SwapEvents(
|
|||
event = "Fast amount input",
|
||||
params = mapOf("Percentage" to percent.toAnalyticsValue()),
|
||||
)
|
||||
|
||||
class TransferModeSwitched(
|
||||
fromCurrency: CryptoCurrency?,
|
||||
toCurrency: CryptoCurrency?,
|
||||
) : SwapEvents(
|
||||
event = "Transfer Mode Switched",
|
||||
params = mapOf(
|
||||
SEND_TOKEN to fromCurrency?.symbol.orEmpty(),
|
||||
"Send Blockchain" to fromCurrency?.network?.name.orEmpty(),
|
||||
RECEIVE_TOKEN to toCurrency?.symbol.orEmpty(),
|
||||
"Receive Blockchain" to toCurrency?.network?.name.orEmpty(),
|
||||
),
|
||||
)
|
||||
|
||||
class ButtonTransferClicked(
|
||||
fromCurrency: CryptoCurrency?,
|
||||
toCurrency: CryptoCurrency?,
|
||||
) : SwapEvents(
|
||||
event = "Button - Transfer",
|
||||
params = mapOf(
|
||||
SEND_TOKEN to fromCurrency?.symbol.orEmpty(),
|
||||
"Send Blockchain" to fromCurrency?.network?.name.orEmpty(),
|
||||
RECEIVE_TOKEN to toCurrency?.symbol.orEmpty(),
|
||||
"Receive Blockchain" to toCurrency?.network?.name.orEmpty(),
|
||||
),
|
||||
)
|
||||
|
||||
@Suppress("NullableToStringCall", "LongParameterList")
|
||||
class TransferInProgressScreen(
|
||||
fromCurrency: CryptoCurrency?,
|
||||
toCurrency: CryptoCurrency?,
|
||||
feeNetwork: Network,
|
||||
) : SwapEvents(
|
||||
event = "Transfer in Progress Screen Opened",
|
||||
params = mapOf(
|
||||
SEND_TOKEN to fromCurrency?.symbol.orEmpty(),
|
||||
"Send Blockchain" to fromCurrency?.network?.name.orEmpty(),
|
||||
RECEIVE_TOKEN to toCurrency?.symbol.orEmpty(),
|
||||
"Receive Blockchain" to toCurrency?.network?.name.orEmpty(),
|
||||
"Network fee" to feeNetwork.name,
|
||||
),
|
||||
), AppsFlyerIncludedEvent
|
||||
}
|
||||
|
||||
private fun PredefinedPercentAmount.toAnalyticsValue(): String = when (this) {
|
||||
|
|
|
|||
|
|
@ -19,11 +19,7 @@ import com.tangem.features.send.v2.api.params.FeeSelectorParams
|
|||
import dagger.assisted.Assisted
|
||||
import dagger.assisted.AssistedFactory
|
||||
import dagger.assisted.AssistedInject
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.SharedFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import kotlinx.coroutines.flow.*
|
||||
|
||||
class SwapFeeSelectorBlockComponent @AssistedInject constructor(
|
||||
@Assisted appComponentContext: AppComponentContext,
|
||||
|
|
@ -44,7 +40,11 @@ class SwapFeeSelectorBlockComponent @AssistedInject constructor(
|
|||
null
|
||||
},
|
||||
feeDisplaySource = FeeSelectorParams.FeeDisplaySource.Screen,
|
||||
feeStateConfiguration = FeeSelectorParams.FeeStateConfiguration.ExcludeLow,
|
||||
feeStateConfiguration = if (params.isTransferMode) {
|
||||
FeeSelectorParams.FeeStateConfiguration.None
|
||||
} else {
|
||||
FeeSelectorParams.FeeStateConfiguration.ExcludeLow
|
||||
},
|
||||
feeCryptoCurrencyStatus = params.feeCryptoCurrencyStatus,
|
||||
cryptoCurrencyStatus = params.sendingCryptoCurrencyStatus,
|
||||
analyticsCategoryName = params.analyticsParams.analyticsCategoryName,
|
||||
|
|
@ -100,6 +100,7 @@ class SwapFeeSelectorBlockComponent @AssistedInject constructor(
|
|||
val feeCryptoCurrencyStatus: CryptoCurrencyStatus,
|
||||
val analyticsParams: AnalyticsParams,
|
||||
val repository: ModelRepository,
|
||||
val isTransferMode: Boolean,
|
||||
)
|
||||
|
||||
@AssistedFactory
|
||||
|
|
|
|||
|
|
@ -59,6 +59,7 @@ internal object SwapProviderStateBuilder {
|
|||
),
|
||||
selectionType = selectionType,
|
||||
percentLowerThenBest = PercentDifference.Empty,
|
||||
approvalSettings = ProviderState.ApprovalSettings.Empty,
|
||||
onProviderClick = onProviderClick,
|
||||
)
|
||||
}
|
||||
|
|
@ -78,6 +79,7 @@ internal object SwapProviderStateBuilder {
|
|||
isNeedBestRateBadge: Boolean = false,
|
||||
needApplyFCARestrictions: Boolean,
|
||||
onProviderClick: (String) -> Unit,
|
||||
onApprovalSelectClick: (SwapProvider) -> Unit = {},
|
||||
): ProviderState.Content {
|
||||
return provider.toContent(
|
||||
subtitle = buildSelectableSubtitle(toTokenInfo),
|
||||
|
|
@ -92,6 +94,12 @@ internal object SwapProviderStateBuilder {
|
|||
percentLowerThenBest = pricesLowerBest[provider.providerId]
|
||||
?.let(PercentDifference::Value)
|
||||
?: PercentDifference.Value(0f),
|
||||
approvalSettings = when (permissionState) {
|
||||
is PermissionDataState.PermissionSettings -> ProviderState.ApprovalSettings.Content(
|
||||
onApprovalSelectClick = { onApprovalSelectClick(provider) },
|
||||
)
|
||||
else -> ProviderState.ApprovalSettings.Empty
|
||||
},
|
||||
onProviderClick = onProviderClick,
|
||||
)
|
||||
}
|
||||
|
|
@ -115,6 +123,7 @@ internal object SwapProviderStateBuilder {
|
|||
),
|
||||
selectionType = selectionType,
|
||||
percentLowerThenBest = PercentDifference.Empty,
|
||||
approvalSettings = ProviderState.ApprovalSettings.Empty,
|
||||
onProviderClick = onProviderClick,
|
||||
)
|
||||
}
|
||||
|
|
@ -151,11 +160,13 @@ internal object SwapProviderStateBuilder {
|
|||
}
|
||||
}
|
||||
|
||||
@Suppress("LongParameterList")
|
||||
private fun SwapProvider.toContent(
|
||||
subtitle: TextReference,
|
||||
additionalBadge: ProviderState.AdditionalBadge,
|
||||
selectionType: ProviderState.SelectionType,
|
||||
percentLowerThenBest: PercentDifference,
|
||||
approvalSettings: ProviderState.ApprovalSettings,
|
||||
onProviderClick: (String) -> Unit,
|
||||
): ProviderState.Content {
|
||||
return ProviderState.Content(
|
||||
|
|
@ -169,6 +180,7 @@ internal object SwapProviderStateBuilder {
|
|||
percentLowerThenBest = percentLowerThenBest,
|
||||
namePrefix = ProviderState.PrefixType.NONE,
|
||||
onProviderClick = onProviderClick,
|
||||
approvalSettings = approvalSettings,
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@ import com.tangem.blockchain.common.transaction.TransactionFee
|
|||
import com.tangem.blockchainsdk.utils.fromNetworkId
|
||||
import com.tangem.common.routing.AppRoute
|
||||
import com.tangem.common.routing.AppRouter
|
||||
import com.tangem.common.ui.bottomsheet.permission.state.ApproveType
|
||||
import com.tangem.core.analytics.api.AnalyticsErrorHandler
|
||||
import com.tangem.core.analytics.api.AnalyticsEventHandler
|
||||
import com.tangem.core.analytics.models.AnalyticsParam
|
||||
|
|
@ -57,6 +58,7 @@ import com.tangem.domain.models.currency.CryptoCurrency
|
|||
import com.tangem.domain.models.currency.CryptoCurrencyStatus
|
||||
import com.tangem.domain.models.network.Network
|
||||
import com.tangem.domain.models.wallet.UserWalletId
|
||||
import com.tangem.domain.models.wallet.isHotWallet
|
||||
import com.tangem.domain.pay.WithdrawalResult
|
||||
import com.tangem.domain.pay.usecase.GetPaymentAccountCryptoCurrencyStatusUseCase
|
||||
import com.tangem.domain.settings.usercountry.GetUserCountryUseCase
|
||||
|
|
@ -68,7 +70,7 @@ import com.tangem.domain.swap.models.PredefinedPercentAmount
|
|||
import com.tangem.domain.swap.models.SwapCurrencyStatus
|
||||
import com.tangem.domain.swap.usecase.CalculateAmountUseCase
|
||||
import com.tangem.domain.tangempay.GetTangemPayCustomerIdUseCase
|
||||
import com.tangem.domain.tangempay.TangemPayWithdrawUseCase
|
||||
import com.tangem.domain.tangempay.TangemPayWithdrawWithSwapUseCase
|
||||
import com.tangem.domain.tokens.GetMinimumTransactionAmountSyncUseCase
|
||||
import com.tangem.domain.tokens.UpdateDelayedNetworkStatusUseCase
|
||||
import com.tangem.domain.transaction.error.GetFeeError
|
||||
|
|
@ -89,10 +91,7 @@ import com.tangem.feature.swap.domain.models.SwapAmount
|
|||
import com.tangem.feature.swap.domain.models.domain.*
|
||||
import com.tangem.feature.swap.domain.models.ui.*
|
||||
import com.tangem.feature.swap.domain.transfer.SwapTransferInteractor
|
||||
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.models.*
|
||||
import com.tangem.feature.swap.models.states.SwapNotificationUM
|
||||
import com.tangem.feature.swap.router.SwapRoute
|
||||
import com.tangem.feature.swap.ui.StateBuilder
|
||||
|
|
@ -100,6 +99,8 @@ 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
|
||||
import com.tangem.features.approval.api.GiveApprovalEntryComponent
|
||||
import com.tangem.features.approval.api.SelectApprovalTypeComponent
|
||||
import com.tangem.features.commonfeatures.api.choosetoken.ChooseTokenAnalyticsPayload
|
||||
import com.tangem.features.commonfeatures.api.choosetoken.ChooseTokenBridge
|
||||
import com.tangem.features.commonfeatures.api.choosetoken.ChooseTokenResult
|
||||
|
|
@ -155,7 +156,7 @@ internal class SwapModel @Inject constructor(
|
|||
private val urlOpener: UrlOpener,
|
||||
private val getAccountCurrencyStatusUseCase: GetAccountCurrencyStatusUseCase,
|
||||
private val getPaymentAccountCryptoCurrencyStatusUseCase: GetPaymentAccountCryptoCurrencyStatusUseCase,
|
||||
private val tangemPayWithdrawUseCase: TangemPayWithdrawUseCase,
|
||||
private val tangemPayWithdrawWithSwapUseCase: TangemPayWithdrawWithSwapUseCase,
|
||||
private val isGaslessFeeSupportedForNetwork: IsGaslessFeeSupportedForNetwork,
|
||||
private val feeSelectorReloadTrigger: FeeSelectorReloadTrigger,
|
||||
private val getTangemPayCustomerIdUseCase: GetTangemPayCustomerIdUseCase,
|
||||
|
|
@ -163,7 +164,7 @@ internal class SwapModel @Inject constructor(
|
|||
private val messageSender: UiMessageSender,
|
||||
private val initialCurrenciesResolver: InitialCurrenciesResolver,
|
||||
private val allowPermissionsHandler: AllowPermissionsHandler,
|
||||
swapFeatureToggles: SwapFeatureToggles,
|
||||
private val swapFeatureToggles: SwapFeatureToggles,
|
||||
private val getSwapUiModeUseCase: GetSwapUiModeUseCase,
|
||||
private val setSwapUiModeUseCase: SetSwapUiModeUseCase,
|
||||
private val calculateAmountUseCase: CalculateAmountUseCase,
|
||||
|
|
@ -223,7 +224,7 @@ internal class SwapModel @Inject constructor(
|
|||
}
|
||||
|
||||
var uiState: SwapStateHolder by mutableStateOf(stateBuilder.createInitialLoadingState())
|
||||
private set
|
||||
internal set
|
||||
|
||||
val feeSelectorRepository = FeeSelectorRepository()
|
||||
|
||||
|
|
@ -249,9 +250,17 @@ internal class SwapModel @Inject constructor(
|
|||
private var preselectedFromCurrency: CryptoCurrency? = null
|
||||
private var preselectedToCurrency: CryptoCurrency? = null
|
||||
|
||||
val approvalSlotNavigation = SlotNavigation<Unit>()
|
||||
val isPermissionNotNeeded: Boolean
|
||||
get() {
|
||||
val permissionState = dataState.getCurrentLoadedSwapState()?.permissionState
|
||||
return permissionState == PermissionDataState.Empty ||
|
||||
swapFeatureToggles.isSwapIntegratedApproveEnabled &&
|
||||
permissionState is PermissionDataState.PermissionSettings
|
||||
}
|
||||
|
||||
val approvalCallback = object : GiveApprovalComponent.Callback {
|
||||
val approvalSlotNavigation = SlotNavigation<GiveApprovalEntryComponent.Mode>()
|
||||
|
||||
internal val approvalFullCallback = object : GiveApprovalComponent.Callback {
|
||||
override fun onApproveClick() {}
|
||||
|
||||
override fun onApproveDone() {
|
||||
|
|
@ -276,6 +285,46 @@ internal class SwapModel @Inject constructor(
|
|||
}
|
||||
}
|
||||
|
||||
internal val approvalSelectorCallback = object : SelectApprovalTypeComponent.Callback {
|
||||
override fun onApproveTypeSelected(spenderAddress: String, approveType: ApproveType) {
|
||||
val (swapState, permission) = dataState.lastLoadedSwapStates.firstNotNullOfOrNull { (provider, state) ->
|
||||
if (state !is SwapState.QuotesLoadedState) return@firstNotNullOfOrNull null
|
||||
val permissionState = state.permissionState
|
||||
|
||||
if (permissionState is PermissionDataState.PermissionSettings &&
|
||||
permissionState.spenderAddress == spenderAddress
|
||||
) {
|
||||
state to permissionState
|
||||
} else {
|
||||
null
|
||||
}
|
||||
} ?: return
|
||||
|
||||
if (permission.type == approveType) {
|
||||
approvalSlotNavigation.dismiss()
|
||||
return
|
||||
}
|
||||
dataState = dataState.copy(
|
||||
lastLoadedSwapStates = dataState.lastLoadedSwapStates.toMutableMap().apply {
|
||||
put(
|
||||
swapState.swapProvider,
|
||||
swapState.copy(permissionState = permission.copy(type = approveType)),
|
||||
)
|
||||
},
|
||||
)
|
||||
approvalSlotNavigation.dismiss()
|
||||
modelScope.launch {
|
||||
feeSelectorRepository.state.value = FeeSelectorUM.Loading
|
||||
feeSelectorReloadTrigger.triggerLoadingState()
|
||||
feeSelectorReloadTrigger.triggerUpdate()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCancelClick() {
|
||||
approvalSlotNavigation.dismiss()
|
||||
}
|
||||
}
|
||||
|
||||
init {
|
||||
subscribeToTokenSelection()
|
||||
|
||||
|
|
@ -603,7 +652,15 @@ internal class SwapModel @Inject constructor(
|
|||
toSwapCurrencyStatus = toSwapCurrencyStatus,
|
||||
fromTokenAmount = lastAmount.value,
|
||||
)
|
||||
if (isUpdatedToTransferMode) return
|
||||
if (isUpdatedToTransferMode) {
|
||||
analyticsEventHandler.send(
|
||||
event = SwapEvents.TransferModeSwitched(
|
||||
fromCurrency = fromSwapCurrencyStatus.currency,
|
||||
toCurrency = toSwapCurrencyStatus.currency,
|
||||
),
|
||||
)
|
||||
return
|
||||
}
|
||||
dataState = dataState.copy(currentTransferState = null)
|
||||
modelScope.launch {
|
||||
uiState = stateBuilder.createInitialLoadingState(
|
||||
|
|
@ -740,14 +797,18 @@ internal class SwapModel @Inject constructor(
|
|||
feePaidCryptoCurrencyStatus = feePaidCryptoCurrency,
|
||||
fee = selectedFee,
|
||||
)
|
||||
feeSelectorRepository.state.value = FeeSelectorUM.Loading
|
||||
feeSelectorReloadTrigger.triggerUpdate()
|
||||
if (isTangemPayWithdrawal()) {
|
||||
refreshTransferUIStateIfNeeded()
|
||||
} else {
|
||||
feeSelectorRepository.state.value = FeeSelectorUM.Loading
|
||||
feeSelectorReloadTrigger.triggerUpdate()
|
||||
}
|
||||
}
|
||||
is SwapState.QuotesLoadedState, is SwapState.SwapError -> Unit
|
||||
}
|
||||
}
|
||||
|
||||
private fun refreshTransferUIStateAfterFeeUpdateIfNeeded(
|
||||
private fun refreshTransferUIStateIfNeeded(
|
||||
feePaidCryptoCurrencyStatus: CryptoCurrencyStatus? = null,
|
||||
fee: Fee? = null,
|
||||
) {
|
||||
|
|
@ -768,7 +829,10 @@ internal class SwapModel @Inject constructor(
|
|||
feePaidCurrencyStatus = feePaidCryptoCurrencyStatus,
|
||||
fee = fee,
|
||||
) as? SwapState.Transfer ?: currentTransferState
|
||||
dataState = dataState.copy(currentTransferState = refreshed)
|
||||
dataState = dataState.copy(
|
||||
currentTransferState = refreshed,
|
||||
feePaidCryptoCurrency = feePaidCryptoCurrencyStatus ?: dataState.feePaidCryptoCurrency,
|
||||
)
|
||||
uiState = swapTransferStateBuilder.updateTransferButtonEnableState(
|
||||
dataState = dataState,
|
||||
transferState = refreshed,
|
||||
|
|
@ -776,6 +840,7 @@ internal class SwapModel @Inject constructor(
|
|||
uiStateHolder = uiState,
|
||||
feePaidCryptoCurrencyStatus = feePaidCryptoCurrencyStatus,
|
||||
fee = fee,
|
||||
isTangemPayWithdrawal = isTangemPayWithdrawal(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -960,8 +1025,6 @@ internal class SwapModel @Inject constructor(
|
|||
tokenSwapInfoForProviders = successStates.entries
|
||||
.associate { it.key.providerId to it.value.toTokenInfo },
|
||||
)
|
||||
val isPermissionNotNeeded =
|
||||
dataState.getCurrentLoadedSwapState()?.permissionState == PermissionDataState.Empty
|
||||
if (shouldUpdateFeeBlock && isPermissionNotNeeded) {
|
||||
modelScope.launch { feeSelectorReloadTrigger.triggerUpdate() }
|
||||
} else {
|
||||
|
|
@ -1288,62 +1351,144 @@ internal class SwapModel @Inject constructor(
|
|||
private fun onTransferClick() {
|
||||
val fromSwapCurrencyStatus = dataState.fromSwapCurrencyStatus
|
||||
val toSwapCurrencyStatus = dataState.toSwapCurrencyStatus
|
||||
analyticsEventHandler.send(
|
||||
event = SwapEvents.ButtonTransferClicked(
|
||||
fromCurrency = fromSwapCurrencyStatus?.currency,
|
||||
toCurrency = toSwapCurrencyStatus?.currency,
|
||||
),
|
||||
)
|
||||
val fee = (feeSelectorRepository.state.value as? FeeSelectorUM.Content)?.selectedFeeItem?.fee
|
||||
if (fromSwapCurrencyStatus == null || toSwapCurrencyStatus == null || fee == null) {
|
||||
TangemLogger.e("onTransferClick: missing currency status or fee, aborting")
|
||||
if (fromSwapCurrencyStatus == null || toSwapCurrencyStatus == null) {
|
||||
TangemLogger.e("onTransferClick: missing currency status, aborting")
|
||||
showAlert()
|
||||
return
|
||||
}
|
||||
val transferState = dataState.currentTransferState ?: return
|
||||
uiState = swapTransferStateBuilder.createTransferInProgressState(uiState)
|
||||
modelScope.launch(dispatchers.main) {
|
||||
swapTransferInteractor.sendTransfer(
|
||||
fromSwapCurrencyStatus = fromSwapCurrencyStatus,
|
||||
toSwapCurrencyStatus = toSwapCurrencyStatus,
|
||||
sendingAmount = transferState.sendingAmount,
|
||||
fee = fee,
|
||||
transactionFeeResult = requireNotNull(getSelectedSwapFee()?.transactionFeeResult) {
|
||||
"It should be not null at this stage"
|
||||
},
|
||||
).fold(
|
||||
ifLeft = { error ->
|
||||
TangemLogger.e("onTransferClick: transfer failed: ${error.getAnalyticsDescription()}")
|
||||
when {
|
||||
isTangemPayWithdrawal() -> withdrawTangemPay(
|
||||
transferState = transferState,
|
||||
toSwapCurrencyStatus = toSwapCurrencyStatus,
|
||||
)
|
||||
fee != null -> sendTransfer(
|
||||
fromSwapCurrencyStatus = fromSwapCurrencyStatus,
|
||||
toSwapCurrencyStatus = toSwapCurrencyStatus,
|
||||
transferState = transferState,
|
||||
fee = fee,
|
||||
)
|
||||
else -> {
|
||||
TangemLogger.e("onTransferClick: Illegal state, aborting")
|
||||
showAlert()
|
||||
},
|
||||
ifRight = { txHash ->
|
||||
val txUrl = getExplorerTransactionUrlUseCase(
|
||||
txHash = txHash,
|
||||
currency = fromSwapCurrencyStatus.currency,
|
||||
).getOrElse {
|
||||
TangemLogger.i("onTransferClick: tx hash explore not supported")
|
||||
""
|
||||
}
|
||||
updateWalletBalance()
|
||||
uiState = swapTransferStateBuilder.createSuccessState(
|
||||
uiState = uiState,
|
||||
dataState = dataState,
|
||||
appCurrency = selectedAppCurrencyFlow.value,
|
||||
isAccountsMode = isAccountsMode,
|
||||
txUrl = txUrl,
|
||||
timestamp = System.currentTimeMillis(),
|
||||
fee = null,
|
||||
onExplorerClick = {
|
||||
if (txUrl.isNotEmpty()) {
|
||||
urlOpener.openUrl(txUrl)
|
||||
}
|
||||
},
|
||||
)
|
||||
router.replaceAll(SwapRoute.Success)
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun withdrawTangemPay(
|
||||
transferState: SwapState.Transfer,
|
||||
toSwapCurrencyStatus: SwapCurrencyStatus,
|
||||
) {
|
||||
swapTransferInteractor.withdrawTangemPay(
|
||||
userWallet = transferState.userWallet,
|
||||
cryptoAmount = transferState.sendingAmount,
|
||||
toSwapCurrencyStatus = toSwapCurrencyStatus,
|
||||
)
|
||||
.onLeft { error ->
|
||||
TangemLogger.e(
|
||||
messageString = "onTransferClick: withdrawTangemPay failed: ${error.getAnalyticsDescription()}",
|
||||
)
|
||||
startLoadingQuotesFromLastState()
|
||||
showAlert()
|
||||
}
|
||||
.onRight { result ->
|
||||
when (result) {
|
||||
WithdrawalResult.Cancelled -> startLoadingQuotesFromLastState()
|
||||
WithdrawalResult.Success -> updateTransferModeTangemPayState()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateTransferModeTangemPayState() {
|
||||
sendTransferInProgressEvent()
|
||||
uiState = swapTransferStateBuilder.createTangemPayWithdrawalSuccessState(
|
||||
uiState = uiState,
|
||||
dataState = dataState,
|
||||
fee = getSelectedSwapFee()?.fee,
|
||||
onExploreClick = {
|
||||
val txUrl = uiState.successState?.txUrl.orEmpty()
|
||||
if (txUrl.isNotEmpty()) {
|
||||
urlOpener.openUrl(txUrl)
|
||||
}
|
||||
},
|
||||
)
|
||||
router.replaceAll(SwapRoute.Success)
|
||||
}
|
||||
|
||||
private suspend fun sendTransfer(
|
||||
fromSwapCurrencyStatus: SwapCurrencyStatus,
|
||||
toSwapCurrencyStatus: SwapCurrencyStatus,
|
||||
transferState: SwapState.Transfer,
|
||||
fee: Fee,
|
||||
) {
|
||||
swapTransferInteractor.sendTransfer(
|
||||
fromSwapCurrencyStatus = fromSwapCurrencyStatus,
|
||||
toSwapCurrencyStatus = toSwapCurrencyStatus,
|
||||
sendingAmount = transferState.sendingAmount,
|
||||
fee = fee,
|
||||
transactionFeeResult = requireNotNull(getSelectedSwapFee()?.transactionFeeResult) {
|
||||
"It should be not null at this stage"
|
||||
},
|
||||
).fold(
|
||||
ifLeft = { error ->
|
||||
TangemLogger.e("onTransferClick: transfer failed: ${error.getAnalyticsDescription()}")
|
||||
startLoadingQuotesFromLastState()
|
||||
showAlert()
|
||||
},
|
||||
ifRight = { txHash ->
|
||||
val txUrl = getExplorerTransactionUrlUseCase(
|
||||
txHash = txHash,
|
||||
currency = fromSwapCurrencyStatus.currency,
|
||||
).getOrElse {
|
||||
TangemLogger.i("onTransferClick: tx hash explore not supported")
|
||||
""
|
||||
}
|
||||
updateWalletBalance()
|
||||
sendTransferInProgressEvent()
|
||||
uiState = swapTransferStateBuilder.createSuccessState(
|
||||
uiState = uiState,
|
||||
dataState = dataState,
|
||||
txUrl = txUrl,
|
||||
timestamp = System.currentTimeMillis(),
|
||||
fee = getSelectedSwapFee()?.fee,
|
||||
onExplorerClick = {
|
||||
if (txUrl.isNotEmpty()) {
|
||||
urlOpener.openUrl(txUrl)
|
||||
}
|
||||
},
|
||||
)
|
||||
router.replaceAll(SwapRoute.Success)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
private fun sendTransferInProgressEvent() {
|
||||
val fromSwapCurrencyStatus = dataState.fromSwapCurrencyStatus
|
||||
val toSwapCurrencyStatus = dataState.toSwapCurrencyStatus
|
||||
analyticsEventHandler.send(
|
||||
event = SwapEvents.TransferInProgressScreen(
|
||||
fromCurrency = fromSwapCurrencyStatus?.currency,
|
||||
toCurrency = toSwapCurrencyStatus?.currency,
|
||||
feeNetwork = getFeeToken().network,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
private suspend fun processTangemPayWithdrawal(
|
||||
fromSwapCurrencyStatus: SwapCurrencyStatus,
|
||||
swapTransactionState: SwapTransactionState.TangemPayWithdrawalData,
|
||||
) {
|
||||
tangemPayWithdrawUseCase(
|
||||
tangemPayWithdrawWithSwapUseCase(
|
||||
userWallet = fromSwapCurrencyStatus.userWallet,
|
||||
cryptoAmount = swapTransactionState.cryptoAmount,
|
||||
cryptoCurrencyId = swapTransactionState.cryptoCurrencyId,
|
||||
|
|
@ -1689,13 +1834,27 @@ internal class SwapModel @Inject constructor(
|
|||
onPredefinedPercentSelected = ::onPredefinedPercentSelected,
|
||||
onReduceToAmount = ::onReduceAmountClicked,
|
||||
onReduceByAmount = ::onReduceAmountClicked,
|
||||
openPermissionBottomSheet = {
|
||||
onApproveClick = {
|
||||
singleTaskScheduler.cancelTask()
|
||||
sendGivePermissionClickedEvent()
|
||||
approvalSlotNavigation.activate(Unit)
|
||||
val approval = getApprovalParams()
|
||||
if (approval != null) {
|
||||
approvalSlotNavigation.activate(
|
||||
GiveApprovalEntryComponent.Mode.FullApproval(approval),
|
||||
)
|
||||
}
|
||||
},
|
||||
onApproveTypeSelect = { provider ->
|
||||
val approval = getSelectApprovalTypeParams(provider)
|
||||
if (approval != null) {
|
||||
approvalSlotNavigation.activate(
|
||||
GiveApprovalEntryComponent.Mode.SelectOnly(approval),
|
||||
)
|
||||
}
|
||||
},
|
||||
onAmountSelected = { onAmountSelected(it) },
|
||||
onProviderClick = { providerId ->
|
||||
singleTaskScheduler.cancelTask()
|
||||
analyticsEventHandler.send(SwapEvents.ProviderClicked())
|
||||
val states = dataState.lastLoadedSwapStates.getLastLoadedSuccessStates()
|
||||
val pricesLowerBest = getPricesLowerBest(providerId, states)
|
||||
|
|
@ -2140,6 +2299,7 @@ internal class SwapModel @Inject constructor(
|
|||
dataState.fromSwapCurrencyStatus ?: return Either.Left(GetFeeError.UnknownError)
|
||||
val toSwapCurrencyStatus =
|
||||
dataState.toSwapCurrencyStatus ?: return Either.Left(GetFeeError.UnknownError)
|
||||
val amount = lastAmount.value.parseBigDecimalOrNull() ?: return Either.Left(GetFeeError.UnknownError)
|
||||
val shouldTransferInsteadOfSwap = swapTransferInteractor.shouldTransferInsteadOfSwap(
|
||||
fromSwapCurrencyStatus.currency,
|
||||
toSwapCurrencyStatus.currency,
|
||||
|
|
@ -2148,7 +2308,7 @@ internal class SwapModel @Inject constructor(
|
|||
return swapTransferInteractor.loadFee(
|
||||
fromSwapCurrencyStatus = fromSwapCurrencyStatus,
|
||||
toSwapCurrencyStatus = toSwapCurrencyStatus,
|
||||
fromTokenAmount = lastAmount.value,
|
||||
fromTokenAmount = amount,
|
||||
).onLeft {
|
||||
TangemLogger.e("loadFee[transfer]: Failed to load fee with error $it")
|
||||
}.onRight {
|
||||
|
|
@ -2162,9 +2322,7 @@ internal class SwapModel @Inject constructor(
|
|||
return Either.Left(GetFeeError.UnknownError)
|
||||
}
|
||||
|
||||
val amountDecimal = lastAmount.value.replace(",", ".").toBigDecimalOrNull()
|
||||
?: return Either.Left(GetFeeError.UnknownError)
|
||||
val swapAmount = SwapAmount(amountDecimal, fromSwapCurrencyStatus.currency.decimals)
|
||||
val swapAmount = SwapAmount(amount, fromSwapCurrencyStatus.currency.decimals)
|
||||
val swapDataForCall = when (quoteState.swapProvider.type) {
|
||||
ExchangeProviderType.DEX, ExchangeProviderType.DEX_BRIDGE -> {
|
||||
quoteState.swapDataModel ?: return Either.Left(GetFeeError.UnknownError)
|
||||
|
|
@ -2196,6 +2354,8 @@ internal class SwapModel @Inject constructor(
|
|||
dataState.fromSwapCurrencyStatus ?: return Either.Left(GetFeeError.UnknownError)
|
||||
val toSwapCurrencyStatus =
|
||||
dataState.toSwapCurrencyStatus ?: return Either.Left(GetFeeError.UnknownError)
|
||||
val amount = lastAmount.value.parseBigDecimalOrNull() ?: return Either.Left(GetFeeError.UnknownError)
|
||||
|
||||
val shouldTransferInsteadOfSwap = swapTransferInteractor.shouldTransferInsteadOfSwap(
|
||||
fromSwapCurrencyStatus.currency,
|
||||
toSwapCurrencyStatus.currency,
|
||||
|
|
@ -2204,7 +2364,8 @@ internal class SwapModel @Inject constructor(
|
|||
return swapTransferInteractor.loadFeeExtended(
|
||||
fromSwapCurrencyStatus = fromSwapCurrencyStatus,
|
||||
toSwapCurrencyStatus = toSwapCurrencyStatus,
|
||||
fromTokenAmount = lastAmount.value,
|
||||
fromTokenAmount = amount,
|
||||
selectedToken = selectedToken,
|
||||
)
|
||||
}
|
||||
val quoteState = dataState.getCurrentLoadedSwapState() ?: return Either.Left(GetFeeError.UnknownError)
|
||||
|
|
@ -2213,8 +2374,7 @@ internal class SwapModel @Inject constructor(
|
|||
return Either.Left(GetFeeError.UnknownError)
|
||||
}
|
||||
|
||||
val amountDecimal = lastAmount.value.parseBigDecimalOrNull() ?: return Either.Left(GetFeeError.UnknownError)
|
||||
val swapAmount = SwapAmount(amountDecimal, fromSwapCurrencyStatus.currency.decimals)
|
||||
val swapAmount = SwapAmount(amount, fromSwapCurrencyStatus.currency.decimals)
|
||||
|
||||
// DEX path requires a SwapDataModel.
|
||||
val swapDataForCall = when (quoteState.swapProvider.type) {
|
||||
|
|
@ -2252,7 +2412,7 @@ internal class SwapModel @Inject constructor(
|
|||
|
||||
if (newState is FeeSelectorUM.Error) {
|
||||
TangemLogger.e("loadFee: ${newState.error}, isHidden = true")
|
||||
refreshTransferUIStateAfterFeeUpdateIfNeeded()
|
||||
refreshTransferUIStateIfNeeded()
|
||||
uiState = stateBuilder.createFeeErrorState(
|
||||
uiStateHolder = uiState,
|
||||
quoteModel = dataState.getCurrentLoadedSwapState() ?: return,
|
||||
|
|
@ -2262,10 +2422,6 @@ internal class SwapModel @Inject constructor(
|
|||
modelScope.launch { forceUpdateState.emit(newState.copy(isHidden = true)) }
|
||||
return
|
||||
}
|
||||
refreshTransferUIStateAfterFeeUpdateIfNeeded(
|
||||
feePaidCryptoCurrencyStatus = dataState.feePaidCryptoCurrency,
|
||||
fee = (newState as? FeeSelectorUM.Content)?.selectedFeeItem?.fee,
|
||||
)
|
||||
|
||||
val fromSwapCurrencyStatus = dataState.fromSwapCurrencyStatus
|
||||
val toSwapCurrencyStatus = dataState.toSwapCurrencyStatus
|
||||
|
|
@ -2274,7 +2430,13 @@ internal class SwapModel @Inject constructor(
|
|||
fromSwapCurrencyStatus?.currency,
|
||||
toSwapCurrencyStatus?.currency,
|
||||
)
|
||||
if (shouldTransferInsteadOfSwap) return
|
||||
if (shouldTransferInsteadOfSwap) {
|
||||
refreshTransferUIStateIfNeeded(
|
||||
feePaidCryptoCurrencyStatus = getSelectedSwapFee()?.selectedFeeToken,
|
||||
fee = (newState as? FeeSelectorUM.Content)?.selectedFeeItem?.fee,
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
val quoteState = dataState.getCurrentLoadedSwapState() ?: return
|
||||
val swapFee = getSelectedSwapFee() ?: return
|
||||
|
|
@ -2321,6 +2483,54 @@ internal class SwapModel @Inject constructor(
|
|||
}
|
||||
}
|
||||
|
||||
internal fun getSelectApprovalTypeParams(provider: SwapProvider): SelectApprovalTypeComponent.Params? {
|
||||
val fromSwapCurrencyStatus = dataState.fromSwapCurrencyStatus ?: return null
|
||||
val swapState = dataState.lastLoadedSwapStates[provider] as? SwapState.QuotesLoadedState ?: return null
|
||||
val permissionState = swapState.permissionState as? PermissionDataState.PermissionSettings ?: return null
|
||||
val providerName = swapState.swapProvider.name
|
||||
val approvalType = permissionState.type
|
||||
|
||||
return SelectApprovalTypeComponent.Params(
|
||||
userWalletId = params.userWalletId,
|
||||
cryptoCurrencyStatus = fromSwapCurrencyStatus.status,
|
||||
initialApproveType = approvalType,
|
||||
amountFooter = resourceReference(
|
||||
id = R.string.give_permission_swap_subtitle,
|
||||
formatArgs = wrappedList(providerName, fromSwapCurrencyStatus.currency.symbol),
|
||||
),
|
||||
spenderAddress = permissionState.spenderAddress,
|
||||
callback = approvalSelectorCallback,
|
||||
)
|
||||
}
|
||||
|
||||
internal fun getApprovalParams(): GiveApprovalComponent.Params? {
|
||||
val permissionState = uiState.permissionUM as? SwapPermissionUM.PermissionRequired ?: return null
|
||||
val fromSwapCurrencyStatus = dataState.fromSwapCurrencyStatus ?: return null
|
||||
val feeCryptoCurrency = dataState.feePaidCryptoCurrency ?: return null
|
||||
val providerName = dataState.selectedProvider?.name.orEmpty()
|
||||
val isHoldToConfirm = fromSwapCurrencyStatus.userWallet.isHotWallet
|
||||
|
||||
return GiveApprovalComponent.Params(
|
||||
userWalletId = params.userWalletId,
|
||||
cryptoCurrencyStatus = fromSwapCurrencyStatus.status,
|
||||
feeCryptoCurrencyStatus = feeCryptoCurrency,
|
||||
amount = dataState.amount.orEmpty(),
|
||||
spenderAddress = permissionState.spenderAddress,
|
||||
amountFooter = if (permissionState.isResetApproval) {
|
||||
resourceReference(R.string.update_approval_permission_subtitle)
|
||||
} else {
|
||||
resourceReference(
|
||||
id = R.string.give_permission_swap_subtitle,
|
||||
formatArgs = wrappedList(providerName, fromSwapCurrencyStatus.currency.symbol),
|
||||
)
|
||||
},
|
||||
feeFooter = resourceReference(R.string.swap_give_permission_fee_footer),
|
||||
isResetApproval = permissionState.isResetApproval,
|
||||
isHoldToConfirm = isHoldToConfirm,
|
||||
callback = approvalFullCallback,
|
||||
)
|
||||
}
|
||||
|
||||
private companion object {
|
||||
const val INITIAL_AMOUNT = ""
|
||||
const val UPDATE_DELAY = 10000L
|
||||
|
|
|
|||
|
|
@ -249,7 +249,7 @@ internal class SwapNotificationsFactory(
|
|||
if (quoteModel.permissionState is PermissionDataState.PermissionRequired) {
|
||||
add(
|
||||
SwapNotificationUM.Info.PermissionNeeded(
|
||||
onApproveClick = actions.openPermissionBottomSheet,
|
||||
onApproveClick = actions.onApproveClick,
|
||||
onLearnMoreClick = { actions.onLinkClick(TangemSiteUrlBuilder.HELP_CENTER_SWAP_URL) },
|
||||
),
|
||||
)
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import com.tangem.domain.models.currency.CryptoCurrency
|
|||
import com.tangem.domain.express.models.ProviderFilterType
|
||||
import com.tangem.domain.swap.models.PredefinedPercentAmount
|
||||
import com.tangem.feature.swap.domain.models.SwapAmount
|
||||
import com.tangem.feature.swap.domain.models.domain.SwapProvider
|
||||
import com.tangem.feature.swap.domain.models.domain.SwapUIMode
|
||||
import java.math.BigDecimal
|
||||
|
||||
|
|
@ -18,7 +19,8 @@ internal data class UiActions(
|
|||
val onPredefinedPercentSelected: (PredefinedPercentAmount) -> Unit,
|
||||
val onReduceToAmount: (SwapAmount) -> Unit,
|
||||
val onReduceByAmount: (SwapAmount, reduceBy: BigDecimal) -> Unit,
|
||||
val openPermissionBottomSheet: () -> Unit,
|
||||
val onApproveClick: () -> Unit,
|
||||
val onApproveTypeSelect: (SwapProvider) -> Unit,
|
||||
// region new actions
|
||||
val onRetryClick: () -> Unit,
|
||||
val onProviderClick: (String) -> Unit,
|
||||
|
|
|
|||
|
|
@ -29,6 +29,7 @@ sealed class ProviderState {
|
|||
val additionalBadge: AdditionalBadge,
|
||||
val percentLowerThenBest: PercentDifference = PercentDifference.Empty,
|
||||
val namePrefix: PrefixType,
|
||||
val approvalSettings: ApprovalSettings = ApprovalSettings.Empty,
|
||||
override val onProviderClick: (String) -> Unit,
|
||||
) : ProviderState()
|
||||
|
||||
|
|
@ -61,6 +62,14 @@ sealed class ProviderState {
|
|||
enum class PrefixType {
|
||||
NONE, PROVIDED_BY
|
||||
}
|
||||
|
||||
@Immutable
|
||||
sealed class ApprovalSettings {
|
||||
data object Empty : ApprovalSettings()
|
||||
data class Content(
|
||||
val onApprovalSelectClick: () -> Unit,
|
||||
) : ApprovalSettings()
|
||||
}
|
||||
}
|
||||
|
||||
@Immutable
|
||||
|
|
|
|||
|
|
@ -178,6 +178,7 @@ private fun Preview_ChooseProviderBottomSheet() {
|
|||
percentLowerThenBest = PercentDifference.Value(-1.0f),
|
||||
selectionType = ProviderState.SelectionType.SELECT,
|
||||
namePrefix = ProviderState.PrefixType.NONE,
|
||||
approvalSettings = ProviderState.ApprovalSettings.Empty,
|
||||
onProviderClick = {},
|
||||
),
|
||||
ProviderState.Unavailable(
|
||||
|
|
|
|||
|
|
@ -4,26 +4,32 @@ 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.layout.Arrangement
|
||||
import androidx.compose.material3.CircularProgressIndicator
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.ripple
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.platform.testTag
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.res.vectorResource
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||
import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider
|
||||
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.SpacerWMax
|
||||
import com.tangem.core.ui.extensions.resolveReference
|
||||
import com.tangem.core.ui.extensions.stringReference
|
||||
import com.tangem.core.ui.extensions.stringResourceSafe
|
||||
|
|
@ -194,6 +200,23 @@ private fun ProviderContentState(
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (state.approvalSettings is ProviderState.ApprovalSettings.Content) {
|
||||
SpacerWMax()
|
||||
Icon(
|
||||
imageVector = ImageVector.vectorResource(R.drawable.ic_filter_default_24),
|
||||
contentDescription = null,
|
||||
tint = TangemTheme.colors.icon.informative,
|
||||
modifier = Modifier
|
||||
.padding(end = 14.dp)
|
||||
.size(20.dp)
|
||||
.clickable(
|
||||
indication = ripple(false),
|
||||
interactionSource = remember { MutableInteractionSource() },
|
||||
onClick = state.approvalSettings.onApprovalSelectClick,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
ProviderChevron(selectionType = state.selectionType, isSelected = isSelected)
|
||||
|
|
@ -442,10 +465,9 @@ private fun ProviderItemPreview(
|
|||
@PreviewParameter(ProviderItemParameterProvider::class) state: Pair<ProviderState, Boolean>,
|
||||
) {
|
||||
TangemThemePreview {
|
||||
ProviderItem(
|
||||
ProviderItemBlock(
|
||||
modifier = Modifier.background(TangemTheme.colors.background.action),
|
||||
state = state.first,
|
||||
isSelected = state.second,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -460,22 +482,28 @@ private class ProviderItemParameterProvider : CollectionPreviewParameterProvider
|
|||
subtitle = stringReference(value = "0,64554846 DAI ≈ 1 MATIC"),
|
||||
additionalBadge = ProviderState.AdditionalBadge.Empty,
|
||||
percentLowerThenBest = PercentDifference.Value(value = 12.0f),
|
||||
selectionType = ProviderState.SelectionType.SELECT,
|
||||
selectionType = ProviderState.SelectionType.NONE,
|
||||
namePrefix = ProviderState.PrefixType.PROVIDED_BY,
|
||||
approvalSettings = ProviderState.ApprovalSettings.Empty,
|
||||
onProviderClick = {},
|
||||
)
|
||||
val contentState2 = contentState.copy(
|
||||
val contentStatePermissionRequired = contentState.copy(
|
||||
subtitle = stringReference(value = "1 132,46 MATIC"),
|
||||
additionalBadge = ProviderState.AdditionalBadge.PermissionRequired,
|
||||
percentLowerThenBest = PercentDifference.Value(value = 5f),
|
||||
)
|
||||
val contentStatePermissionIntegrated = contentState.copy(
|
||||
subtitle = stringReference(value = "1 132,46 MATIC"),
|
||||
percentLowerThenBest = PercentDifference.Value(value = 5f),
|
||||
approvalSettings = ProviderState.ApprovalSettings.Content({}),
|
||||
)
|
||||
val unavailableState = ProviderState.Unavailable(
|
||||
id = "1",
|
||||
name = "1inch",
|
||||
type = "DEX",
|
||||
iconUrl = "",
|
||||
alertText = stringReference(value = "Not available"),
|
||||
selectionType = ProviderState.SelectionType.SELECT,
|
||||
selectionType = ProviderState.SelectionType.NONE,
|
||||
onProviderClick = {},
|
||||
)
|
||||
val loadingState = ProviderState.Loading()
|
||||
|
|
@ -483,8 +511,11 @@ private class ProviderItemParameterProvider : CollectionPreviewParameterProvider
|
|||
add(contentState to true)
|
||||
add(contentState to false)
|
||||
|
||||
add(contentState2 to true)
|
||||
add(contentState2 to false)
|
||||
add(contentStatePermissionRequired to true)
|
||||
add(contentStatePermissionRequired to false)
|
||||
|
||||
add(contentStatePermissionIntegrated to true)
|
||||
add(contentStatePermissionIntegrated to false)
|
||||
|
||||
add(unavailableState to true)
|
||||
add(unavailableState to false)
|
||||
|
|
|
|||
|
|
@ -173,6 +173,7 @@ private class SimpleProviderPreview : PreviewParameterProvider<ProviderState> {
|
|||
additionalBadge = ProviderState.AdditionalBadge.Empty,
|
||||
percentLowerThenBest = PercentDifference.Empty,
|
||||
namePrefix = ProviderState.PrefixType.NONE,
|
||||
approvalSettings = ProviderState.ApprovalSettings.Empty,
|
||||
onProviderClick = {},
|
||||
),
|
||||
ProviderState.Content(
|
||||
|
|
|
|||
|
|
@ -100,7 +100,7 @@ internal class StateBuilder(
|
|||
onChangeCardsClicked = actions.onChangeCardsClicked,
|
||||
onMaxAmountSelected = actions.onMaxAmountSelected,
|
||||
changeCardsButtonState = ChangeCardsButtonState.DISABLED,
|
||||
onShowPermissionBottomSheet = actions.openPermissionBottomSheet,
|
||||
onShowPermissionBottomSheet = actions.onApproveClick,
|
||||
onSelectTokenClick = actions.onSelectTokenClick,
|
||||
onSuccess = actions.onSuccess,
|
||||
providerState = ProviderState.Empty(),
|
||||
|
|
@ -1078,6 +1078,7 @@ internal class StateBuilder(
|
|||
pricesLowerBest = pricesLowerBest,
|
||||
onProviderSelect = actions.onProviderSelect,
|
||||
needApplyFCARestrictions = needApplyFCARestrictions,
|
||||
onApprovalSelectClick = actions.onApproveTypeSelect,
|
||||
bestRatedProviderId = bestRatedProviderId,
|
||||
isNeedBestRateBadge = isNeedBestRateBadge,
|
||||
)
|
||||
|
|
@ -1183,6 +1184,7 @@ internal class StateBuilder(
|
|||
private fun Map.Entry<SwapProvider, SwapState>.convertToProviderBottomSheetState(
|
||||
pricesLowerBest: Map<String, Float>,
|
||||
onProviderSelect: (String) -> Unit,
|
||||
onApprovalSelectClick: (SwapProvider) -> Unit,
|
||||
needApplyFCARestrictions: Boolean,
|
||||
bestRatedProviderId: String,
|
||||
isNeedBestRateBadge: Boolean,
|
||||
|
|
@ -1201,6 +1203,7 @@ internal class StateBuilder(
|
|||
isBestRate = bestRatedProviderId == provider.providerId && !state.priceImpact.shouldShowWarning(),
|
||||
isNeedBestRateBadge = isNeedBestRateBadge,
|
||||
onProviderClick = onProviderSelect,
|
||||
onApprovalSelectClick = onApprovalSelectClick,
|
||||
)
|
||||
}
|
||||
is SwapState.SwapError -> getProviderStateForError(
|
||||
|
|
|
|||
|
|
@ -3,12 +3,14 @@ package com.tangem.feature.swap.ui.transfer
|
|||
import com.tangem.blockchain.common.transaction.Fee
|
||||
import com.tangem.common.ui.notifications.NotificationUM
|
||||
import com.tangem.common.ui.notifications.NotificationsFactory.addDustWarningNotification
|
||||
import com.tangem.common.ui.notifications.NotificationsFactory.addExceedsBalanceNotification
|
||||
import com.tangem.common.ui.notifications.NotificationsFactory.addExistentialWarningNotification
|
||||
import com.tangem.common.ui.notifications.NotificationsFactory.addFeeCoverageNotification
|
||||
import com.tangem.common.ui.notifications.NotificationsFactory.addReserveAmountErrorNotification
|
||||
import com.tangem.common.ui.notifications.NotificationsFactory.addTransactionLimitErrorNotification
|
||||
import com.tangem.common.ui.notifications.NotificationsFactory.addValidateTransactionNotifications
|
||||
import com.tangem.core.ui.utils.parseBigDecimal
|
||||
import com.tangem.domain.models.currency.CryptoCurrency
|
||||
import com.tangem.domain.models.currency.CryptoCurrencyStatus
|
||||
import com.tangem.feature.swap.domain.models.SwapAmount
|
||||
import com.tangem.feature.swap.domain.models.ui.SwapState
|
||||
|
|
@ -24,12 +26,14 @@ import javax.inject.Inject
|
|||
|
||||
internal class SwapTransferNotificationsFactory @Inject constructor() {
|
||||
|
||||
@Suppress("LongParameterList")
|
||||
fun getNotifications(
|
||||
transferState: SwapState.Transfer,
|
||||
feeCryptoCurrencyStatus: CryptoCurrencyStatus?,
|
||||
fee: Fee?,
|
||||
onReduceByAmount: (SwapAmount, BigDecimal) -> Unit,
|
||||
onReduceToAmount: (SwapAmount) -> Unit,
|
||||
onBuyClick: (CryptoCurrency) -> Unit,
|
||||
): ImmutableList<NotificationUM> {
|
||||
return buildList {
|
||||
maybeAddRentExemptionError(transferState)
|
||||
|
|
@ -41,6 +45,7 @@ internal class SwapTransferNotificationsFactory @Inject constructor() {
|
|||
onReduceToAmount = onReduceToAmount,
|
||||
)
|
||||
maybeAddNeedReserveToCreateAccountWarning(transferState)
|
||||
maybeAddExceedsBalanceNotification(transferState, onBuyClick)
|
||||
}.toPersistentList()
|
||||
}
|
||||
|
||||
|
|
@ -172,4 +177,21 @@ internal class SwapTransferNotificationsFactory @Inject constructor() {
|
|||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun MutableList<NotificationUM>.maybeAddExceedsBalanceNotification(
|
||||
transferState: SwapState.Transfer,
|
||||
onBuyClick: (CryptoCurrency) -> Unit,
|
||||
) {
|
||||
val cryptoCurrencyStatus = transferState.fromTokenInfo.swapCurrencyStatus.status
|
||||
addExceedsBalanceNotification(
|
||||
cryptoCurrencyWarning = transferState.cryptoCurrencyWarning,
|
||||
cryptoCurrencyStatus = cryptoCurrencyStatus,
|
||||
shouldMergeFeeNetworkName = BlockchainUtils.isArbitrum(
|
||||
networkId = cryptoCurrencyStatus.currency.network.rawId,
|
||||
),
|
||||
onClick = onBuyClick,
|
||||
onAnalyticsEvent = {},
|
||||
onResetAnalyticsEvent = {},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -59,6 +59,7 @@ internal class SwapTransferStateBuilder @Inject constructor(
|
|||
transferState = transferState,
|
||||
feeCryptoCurrencyStatus = feePaidCryptoCurrencyStatus,
|
||||
fee = fee,
|
||||
onBuyClick = actions.openTokenDetailsScreen,
|
||||
onReduceByAmount = actions.onReduceByAmount,
|
||||
onReduceToAmount = actions.onReduceToAmount,
|
||||
)
|
||||
|
|
@ -210,6 +211,9 @@ internal class SwapTransferStateBuilder @Inject constructor(
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* [isTangemPayWithdrawal] - if true - Tangem pay withdrawal done with no fee, skip fee nullability check
|
||||
*/
|
||||
@Suppress("LongParameterList")
|
||||
fun updateTransferButtonEnableState(
|
||||
dataState: SwapProcessDataState,
|
||||
|
|
@ -218,18 +222,20 @@ internal class SwapTransferStateBuilder @Inject constructor(
|
|||
uiStateHolder: SwapStateHolder,
|
||||
feePaidCryptoCurrencyStatus: CryptoCurrencyStatus?,
|
||||
fee: Fee?,
|
||||
isTangemPayWithdrawal: Boolean,
|
||||
): SwapStateHolder {
|
||||
val notifications = notificationsFactory.getNotifications(
|
||||
transferState = transferState,
|
||||
feeCryptoCurrencyStatus = feePaidCryptoCurrencyStatus,
|
||||
fee = fee,
|
||||
onBuyClick = actions.openTokenDetailsScreen,
|
||||
onReduceByAmount = actions.onReduceByAmount,
|
||||
onReduceToAmount = actions.onReduceToAmount,
|
||||
)
|
||||
return uiStateHolder.copy(
|
||||
notifications = notifications,
|
||||
swapButton = uiStateHolder.swapButton.copy(
|
||||
isEnabled = getTransferButtonEnabled(notifications, fee),
|
||||
isEnabled = getTransferButtonEnabled(notifications, fee, isTangemPayWithdrawal),
|
||||
),
|
||||
transferFooter = getSendingFooterText(
|
||||
dataState = dataState,
|
||||
|
|
@ -240,8 +246,12 @@ internal class SwapTransferStateBuilder @Inject constructor(
|
|||
)
|
||||
}
|
||||
|
||||
private fun getTransferButtonEnabled(notifications: ImmutableList<NotificationUM>, fee: Fee?): Boolean {
|
||||
return fee != null && notifications.none { notification ->
|
||||
private fun getTransferButtonEnabled(
|
||||
notifications: ImmutableList<NotificationUM>,
|
||||
fee: Fee?,
|
||||
isTangemPayWithdrawal: Boolean,
|
||||
): Boolean {
|
||||
return (fee != null || isTangemPayWithdrawal) && notifications.none { notification ->
|
||||
notification is SwapNotificationUM.Error || notification is NotificationUM.Error ||
|
||||
notification is SwapNotificationUM.Warning.ExpressErrorWarning ||
|
||||
notification is SwapNotificationUM.Warning.ExpressGeneralError ||
|
||||
|
|
@ -317,13 +327,12 @@ internal class SwapTransferStateBuilder @Inject constructor(
|
|||
fun createSuccessState(
|
||||
uiState: SwapStateHolder,
|
||||
dataState: SwapProcessDataState,
|
||||
appCurrency: AppCurrency,
|
||||
isAccountsMode: Boolean,
|
||||
fee: Fee?,
|
||||
txUrl: String,
|
||||
timestamp: Long,
|
||||
fee: TextReference?,
|
||||
onExplorerClick: () -> Unit,
|
||||
): SwapStateHolder {
|
||||
val transferState = requireNotNull(dataState.currentTransferState)
|
||||
val fromSwapCurrencyStatus = requireNotNull(dataState.fromSwapCurrencyStatus)
|
||||
val toSwapCurrencyStatus = requireNotNull(dataState.toSwapCurrencyStatus)
|
||||
val amount = dataState.amount?.parseBigDecimalOrNull() ?: BigDecimal.ZERO
|
||||
|
|
@ -333,11 +342,11 @@ internal class SwapTransferStateBuilder @Inject constructor(
|
|||
val fromAmountText = amount.format { crypto(fromCurrency.symbol, fromCurrency.decimals) }
|
||||
val toAmountText = amount.format { crypto(toCurrency.symbol, toCurrency.decimals) }
|
||||
val fromFiatAmount = getFormattedFiatAmount(
|
||||
appCurrency = appCurrency,
|
||||
appCurrency = transferState.appCurrency,
|
||||
amount = fromSwapCurrencyStatus.status.value.fiatRate?.multiply(amount),
|
||||
)
|
||||
val toFiatAmount = getFormattedFiatAmount(
|
||||
appCurrency = appCurrency,
|
||||
appCurrency = transferState.appCurrency,
|
||||
amount = toSwapCurrencyStatus.status.value.fiatRate?.multiply(amount),
|
||||
)
|
||||
|
||||
|
|
@ -351,15 +360,15 @@ internal class SwapTransferStateBuilder @Inject constructor(
|
|||
isTransferMode = true,
|
||||
providerIcon = "",
|
||||
rate = TextReference.EMPTY,
|
||||
fee = fee,
|
||||
fee = fee?.let { formatFeeForSuccess(transferState = transferState, fee = it) },
|
||||
fromTitle = getCardAccountTitle(
|
||||
account = fromSwapCurrencyStatus.account,
|
||||
isAccountsMode = isAccountsMode,
|
||||
isAccountsMode = transferState.isAccountsMode,
|
||||
isFromCard = true,
|
||||
),
|
||||
toTitle = getCardAccountTitle(
|
||||
account = toSwapCurrencyStatus.account,
|
||||
isAccountsMode = isAccountsMode,
|
||||
isAccountsMode = transferState.isAccountsMode,
|
||||
isFromCard = false,
|
||||
),
|
||||
fromTokenAmount = stringReference(fromAmountText),
|
||||
|
|
@ -373,4 +382,68 @@ internal class SwapTransferStateBuilder @Inject constructor(
|
|||
),
|
||||
)
|
||||
}
|
||||
|
||||
fun createTangemPayWithdrawalSuccessState(
|
||||
uiState: SwapStateHolder,
|
||||
dataState: SwapProcessDataState,
|
||||
fee: Fee?,
|
||||
onExploreClick: () -> Unit,
|
||||
): SwapStateHolder {
|
||||
val fromSwapCurrencyStatus = requireNotNull(dataState.fromSwapCurrencyStatus)
|
||||
val toSwapCurrencyStatus = requireNotNull(dataState.toSwapCurrencyStatus)
|
||||
val transferState = requireNotNull(dataState.currentTransferState)
|
||||
val amountValue = transferState.sendingAmount
|
||||
|
||||
val fiatAmount = getFormattedFiatAmount(
|
||||
appCurrency = transferState.appCurrency,
|
||||
amount = fromSwapCurrencyStatus.status.value.fiatRate?.multiply(amountValue),
|
||||
)
|
||||
|
||||
return uiState.copy(
|
||||
successState = SwapSuccessStateHolder(
|
||||
timestamp = System.currentTimeMillis(),
|
||||
txUrl = "",
|
||||
providerName = stringReference(""),
|
||||
providerType = stringReference(""),
|
||||
shouldShowStatusButton = false,
|
||||
isTransferMode = true,
|
||||
providerIcon = "",
|
||||
rate = TextReference.EMPTY,
|
||||
fee = fee?.let { formatFeeForSuccess(transferState = transferState, fee = it) },
|
||||
fromTitle = getCardAccountTitle(
|
||||
account = fromSwapCurrencyStatus.account,
|
||||
isAccountsMode = transferState.isAccountsMode,
|
||||
isFromCard = true,
|
||||
),
|
||||
toTitle = getCardAccountTitle(
|
||||
account = toSwapCurrencyStatus.account,
|
||||
isAccountsMode = transferState.isAccountsMode,
|
||||
isFromCard = false,
|
||||
),
|
||||
fromTokenAmount = stringReference(amountValue.toString()),
|
||||
toTokenAmount = stringReference(amountValue.toString()),
|
||||
fromTokenFiatAmount = fiatAmount,
|
||||
toTokenFiatAmount = fiatAmount,
|
||||
fromTokenIconState = iconConverter.convert(fromSwapCurrencyStatus.status),
|
||||
toTokenIconState = iconConverter.convert(toSwapCurrencyStatus.status),
|
||||
onExploreButtonClick = onExploreClick,
|
||||
onStatusButtonClick = {},
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
private fun formatFeeForSuccess(transferState: SwapState.Transfer, fee: Fee): TextReference {
|
||||
val feeAmount = fee.amount
|
||||
val totalFeeValue = feeAmount.value ?: BigDecimal.ZERO
|
||||
val cryptoFormatted = totalFeeValue.format {
|
||||
crypto(symbol = feeAmount.currencySymbol, decimals = feeAmount.decimals)
|
||||
}
|
||||
val appCurrency = transferState.appCurrency
|
||||
val swapCurrencyStatus = transferState.fromTokenInfo.swapCurrencyStatus
|
||||
val fiatRate = swapCurrencyStatus.status.value.fiatRate
|
||||
val fiatFormatted = fiatRate?.multiply(totalFeeValue).format {
|
||||
fiat(fiatCurrencyCode = appCurrency.code, fiatCurrencySymbol = appCurrency.symbol)
|
||||
}
|
||||
return stringReference("$cryptoFormatted ($fiatFormatted)")
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,70 @@
|
|||
package com.tangem.feature.swap.model
|
||||
|
||||
import com.tangem.domain.models.currency.CryptoCurrency
|
||||
import io.mockk.every
|
||||
import io.mockk.mockk
|
||||
import io.mockk.verify
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.jupiter.api.BeforeEach
|
||||
import org.junit.jupiter.api.Test
|
||||
|
||||
/**
|
||||
* Tests for [SwapModel.approvalFullCallback] (the full give-approval flow callback).
|
||||
*
|
||||
* Covers the synchronously-verifiable side effects. The quote-reload path
|
||||
* ([SwapModel] `startLoadingQuotesFromLastState`) short-circuits because `dataState.amount` is null
|
||||
* in these setups, keeping the callbacks side-effect-bounded.
|
||||
*/
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
internal class SwapModelApprovalFullCallbackTest : SwapModelTestBase() {
|
||||
|
||||
@BeforeEach
|
||||
fun setUp() {
|
||||
setUpBase()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN token from-currency WHEN onApproveDone THEN adds contract address to in-progress`() = runTest {
|
||||
val model = createModel()
|
||||
val token = mockk<CryptoCurrency.Token>(relaxed = true) {
|
||||
every { contractAddress } returns "0xContract"
|
||||
}
|
||||
model.dataState = model.dataState.copy(
|
||||
fromSwapCurrencyStatus = swapCurrencyStatus(currency = token),
|
||||
)
|
||||
|
||||
model.approvalFullCallback.onApproveDone()
|
||||
|
||||
verify(exactly = 1) { allowPermissionsHandler.addAddressToInProgress("0xContract") }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN null from-currency WHEN onApproveDone THEN does not add address to in-progress`() = runTest {
|
||||
val model = createModel()
|
||||
model.dataState = model.dataState.copy(fromSwapCurrencyStatus = null)
|
||||
|
||||
model.approvalFullCallback.onApproveDone()
|
||||
|
||||
verify(exactly = 0) { allowPermissionsHandler.addAddressToInProgress(any()) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN failure WHEN onApproveFailed THEN sends alert message`() = runTest {
|
||||
val model = createModel()
|
||||
|
||||
model.approvalFullCallback.onApproveFailed()
|
||||
|
||||
verify(exactly = 1) { messageSender.send(any()) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `WHEN onApproveClick THEN no interactions`() = runTest {
|
||||
val model = createModel()
|
||||
|
||||
// onApproveClick is intentionally a no-op; assert it does not crash.
|
||||
model.approvalFullCallback.onApproveClick()
|
||||
|
||||
verify(exactly = 0) { allowPermissionsHandler.addAddressToInProgress(any()) }
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,111 @@
|
|||
package com.tangem.feature.swap.model
|
||||
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import com.tangem.common.ui.bottomsheet.permission.state.ApproveType
|
||||
import com.tangem.feature.swap.domain.models.ui.PermissionDataState
|
||||
import com.tangem.features.send.v2.api.entity.FeeSelectorUM
|
||||
import io.mockk.coVerify
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.jupiter.api.BeforeEach
|
||||
import org.junit.jupiter.api.Test
|
||||
|
||||
/**
|
||||
* Tests for [SwapModel.approvalSelectorCallback] (the [PermissionDataState.PermissionSettings]
|
||||
* approval-type selector).
|
||||
*/
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
internal class SwapModelApprovalSelectorCallbackTest : SwapModelTestBase() {
|
||||
|
||||
@BeforeEach
|
||||
fun setUp() {
|
||||
setUpBase()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN no current loaded state WHEN onApproveTypeSelected THEN no state change and no reload`() = runTest {
|
||||
val model = createModel()
|
||||
val before = model.dataState
|
||||
|
||||
model.approvalSelectorCallback.onApproveTypeSelected("0xSpender", ApproveType.UNLIMITED)
|
||||
|
||||
assertThat(model.dataState).isEqualTo(before)
|
||||
coVerify(exactly = 0) { feeSelectorReloadTrigger.triggerLoadingState() }
|
||||
coVerify(exactly = 0) { feeSelectorReloadTrigger.triggerUpdate() }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN permission is not PermissionSettings WHEN onApproveTypeSelected THEN no reload`() = runTest {
|
||||
val provider = swapProvider()
|
||||
val model = createModel()
|
||||
model.dataState = model.dataState.copy(
|
||||
selectedProvider = provider,
|
||||
lastLoadedSwapStates = mapOf(provider to quotesLoadedState(provider, PermissionDataState.Empty)),
|
||||
)
|
||||
|
||||
model.approvalSelectorCallback.onApproveTypeSelected("0xSpender", ApproveType.UNLIMITED)
|
||||
|
||||
coVerify(exactly = 0) { feeSelectorReloadTrigger.triggerLoadingState() }
|
||||
coVerify(exactly = 0) { feeSelectorReloadTrigger.triggerUpdate() }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN same approve type WHEN onApproveTypeSelected THEN no state change and no reload`() = runTest {
|
||||
val provider = swapProvider()
|
||||
val model = createModel()
|
||||
model.dataState = model.dataState.copy(
|
||||
selectedProvider = provider,
|
||||
lastLoadedSwapStates = mapOf(
|
||||
provider to quotesLoadedState(provider, permissionSettings(type = ApproveType.LIMITED)),
|
||||
),
|
||||
)
|
||||
val before = model.dataState
|
||||
|
||||
model.approvalSelectorCallback.onApproveTypeSelected("0xSpender", ApproveType.LIMITED)
|
||||
|
||||
assertThat(model.dataState).isEqualTo(before)
|
||||
coVerify(exactly = 0) { feeSelectorReloadTrigger.triggerLoadingState() }
|
||||
coVerify(exactly = 0) { feeSelectorReloadTrigger.triggerUpdate() }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN different approve type WHEN onApproveTypeSelected THEN updates state and triggers reload`() = runTest {
|
||||
val provider = swapProvider()
|
||||
val model = createModel()
|
||||
model.dataState = model.dataState.copy(
|
||||
selectedProvider = provider,
|
||||
lastLoadedSwapStates = mapOf(
|
||||
provider to quotesLoadedState(provider, permissionSettings(type = ApproveType.LIMITED)),
|
||||
),
|
||||
)
|
||||
|
||||
model.approvalSelectorCallback.onApproveTypeSelected("0xSpender", ApproveType.UNLIMITED)
|
||||
|
||||
val updated = model.dataState.getCurrentLoadedSwapState()
|
||||
val settings = updated?.permissionState as? PermissionDataState.PermissionSettings
|
||||
assertThat(settings).isNotNull()
|
||||
assertThat(settings!!.type).isEqualTo(ApproveType.UNLIMITED)
|
||||
assertThat(model.feeSelectorRepository.state.value).isEqualTo(FeeSelectorUM.Loading)
|
||||
coVerify(exactly = 1) { feeSelectorReloadTrigger.triggerLoadingState() }
|
||||
coVerify(exactly = 1) { feeSelectorReloadTrigger.triggerUpdate() }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN any state WHEN onCancelClick THEN no state change and no reload`() = runTest {
|
||||
val provider = swapProvider()
|
||||
val model = createModel()
|
||||
model.dataState = model.dataState.copy(
|
||||
selectedProvider = provider,
|
||||
lastLoadedSwapStates = mapOf(
|
||||
provider to quotesLoadedState(provider, permissionSettings()),
|
||||
),
|
||||
)
|
||||
val before = model.dataState
|
||||
|
||||
model.approvalSelectorCallback.onCancelClick()
|
||||
|
||||
assertThat(model.dataState).isEqualTo(before)
|
||||
coVerify(exactly = 0) { feeSelectorReloadTrigger.triggerLoadingState() }
|
||||
coVerify(exactly = 0) { feeSelectorReloadTrigger.triggerUpdate() }
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,142 @@
|
|||
package com.tangem.feature.swap.model
|
||||
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import com.tangem.domain.models.currency.CryptoCurrency
|
||||
import com.tangem.domain.models.currency.CryptoCurrencyStatus
|
||||
import com.tangem.domain.models.wallet.UserWallet
|
||||
import com.tangem.feature.swap.models.SwapPermissionUM
|
||||
import io.mockk.every
|
||||
import io.mockk.mockk
|
||||
import org.junit.jupiter.api.BeforeEach
|
||||
import org.junit.jupiter.api.Test
|
||||
|
||||
/**
|
||||
* Tests for [SwapModel.getApprovalParams].
|
||||
*
|
||||
* Builds [com.tangem.features.approval.api.GiveApprovalComponent.Params] only when:
|
||||
* - `uiState.permissionUM` is [SwapPermissionUM.PermissionRequired]
|
||||
* - `fromSwapCurrencyStatus` is non-null
|
||||
* - `feePaidCryptoCurrency` is non-null
|
||||
*/
|
||||
internal class SwapModelGetApprovalParamsTest : SwapModelTestBase() {
|
||||
|
||||
@BeforeEach
|
||||
fun setUp() {
|
||||
setUpBase()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN permissionUM is not PermissionRequired THEN returns null`() {
|
||||
val model = createModel()
|
||||
// default uiState has permissionUM == Empty
|
||||
model.dataState = model.dataState.copy(
|
||||
fromSwapCurrencyStatus = swapCurrencyStatus(),
|
||||
feePaidCryptoCurrency = mockk(relaxed = true),
|
||||
)
|
||||
|
||||
assertThat(model.getApprovalParams()).isNull()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN null fromSwapCurrencyStatus THEN returns null`() {
|
||||
val model = createModel()
|
||||
model.uiState = model.uiState.copy(
|
||||
permissionUM = SwapPermissionUM.PermissionRequired(isResetApproval = false, spenderAddress = "0xSpender"),
|
||||
)
|
||||
model.dataState = model.dataState.copy(
|
||||
fromSwapCurrencyStatus = null,
|
||||
feePaidCryptoCurrency = mockk(relaxed = true),
|
||||
)
|
||||
|
||||
assertThat(model.getApprovalParams()).isNull()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN null feePaidCryptoCurrency THEN returns null`() {
|
||||
val model = createModel()
|
||||
model.uiState = model.uiState.copy(
|
||||
permissionUM = SwapPermissionUM.PermissionRequired(isResetApproval = false, spenderAddress = "0xSpender"),
|
||||
)
|
||||
model.dataState = model.dataState.copy(
|
||||
fromSwapCurrencyStatus = swapCurrencyStatus(),
|
||||
feePaidCryptoCurrency = null,
|
||||
)
|
||||
|
||||
assertThat(model.getApprovalParams()).isNull()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN give-approval prerequisites met THEN builds params with give footer`() {
|
||||
val model = createModel()
|
||||
val coldWallet = mockk<UserWallet.Cold>(relaxed = true)
|
||||
val currency = mockk<CryptoCurrency>(relaxed = true) { every { symbol } returns "DAI" }
|
||||
val status = mockk<CryptoCurrencyStatus>(relaxed = true)
|
||||
val from = swapCurrencyStatus(wallet = coldWallet, status = status, currency = currency)
|
||||
val feeStatus: CryptoCurrencyStatus = mockk(relaxed = true)
|
||||
val provider = swapProvider(name = "1inch")
|
||||
|
||||
model.uiState = model.uiState.copy(
|
||||
permissionUM = SwapPermissionUM.PermissionRequired(isResetApproval = false, spenderAddress = "0xSpender"),
|
||||
)
|
||||
model.dataState = model.dataState.copy(
|
||||
fromSwapCurrencyStatus = from,
|
||||
feePaidCryptoCurrency = feeStatus,
|
||||
selectedProvider = provider,
|
||||
amount = "12.5",
|
||||
)
|
||||
|
||||
val params = model.getApprovalParams()
|
||||
|
||||
assertThat(params).isNotNull()
|
||||
assertThat(params!!.userWalletId).isEqualTo(userWalletId)
|
||||
assertThat(params.cryptoCurrencyStatus).isEqualTo(status)
|
||||
assertThat(params.feeCryptoCurrencyStatus).isEqualTo(feeStatus)
|
||||
assertThat(params.amount).isEqualTo("12.5")
|
||||
assertThat(params.spenderAddress).isEqualTo("0xSpender")
|
||||
assertThat(params.isResetApproval).isFalse()
|
||||
// Cold wallet -> not hold-to-confirm
|
||||
assertThat(params.isHoldToConfirm).isFalse()
|
||||
assertThat(params.callback).isSameInstanceAs(model.approvalFullCallback)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN hot wallet THEN isHoldToConfirm is true`() {
|
||||
val model = createModel()
|
||||
val hotWallet = mockk<UserWallet.Hot>(relaxed = true)
|
||||
val from = swapCurrencyStatus(wallet = hotWallet)
|
||||
|
||||
model.uiState = model.uiState.copy(
|
||||
permissionUM = SwapPermissionUM.PermissionRequired(isResetApproval = false, spenderAddress = "0xSpender"),
|
||||
)
|
||||
model.dataState = model.dataState.copy(
|
||||
fromSwapCurrencyStatus = from,
|
||||
feePaidCryptoCurrency = mockk(relaxed = true),
|
||||
amount = "1",
|
||||
)
|
||||
|
||||
val params = model.getApprovalParams()
|
||||
|
||||
assertThat(params).isNotNull()
|
||||
assertThat(params!!.isHoldToConfirm).isTrue()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN reset approval THEN isResetApproval is true`() {
|
||||
val model = createModel()
|
||||
val from = swapCurrencyStatus()
|
||||
|
||||
model.uiState = model.uiState.copy(
|
||||
permissionUM = SwapPermissionUM.PermissionRequired(isResetApproval = true, spenderAddress = "0xSpender"),
|
||||
)
|
||||
model.dataState = model.dataState.copy(
|
||||
fromSwapCurrencyStatus = from,
|
||||
feePaidCryptoCurrency = mockk(relaxed = true),
|
||||
amount = "1",
|
||||
)
|
||||
|
||||
val params = model.getApprovalParams()
|
||||
|
||||
assertThat(params).isNotNull()
|
||||
assertThat(params!!.isResetApproval).isTrue()
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,76 @@
|
|||
package com.tangem.feature.swap.model
|
||||
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import com.tangem.common.ui.bottomsheet.permission.state.ApproveType
|
||||
import com.tangem.domain.models.currency.CryptoCurrency
|
||||
import com.tangem.domain.models.currency.CryptoCurrencyStatus
|
||||
import com.tangem.feature.swap.domain.models.ui.PermissionDataState
|
||||
import com.tangem.feature.swap.domain.models.ui.SwapState
|
||||
import io.mockk.every
|
||||
import io.mockk.mockk
|
||||
import org.junit.jupiter.api.BeforeEach
|
||||
import org.junit.jupiter.api.Test
|
||||
|
||||
/**
|
||||
* Tests for [SwapModel.getSelectApprovalTypeParams].
|
||||
*
|
||||
* Builds [com.tangem.features.approval.api.SelectApprovalTypeComponent.Params] from the current
|
||||
* `fromSwapCurrencyStatus` and the provider's loaded swap state. Returns `null` when prerequisites
|
||||
* are missing. `initialApproveType` is taken from [PermissionDataState.PermissionSettings.type] or
|
||||
* falls back to [ApproveType.LIMITED].
|
||||
*/
|
||||
internal class SwapModelGetSelectApprovalTypeParamsTest : SwapModelTestBase() {
|
||||
|
||||
@BeforeEach
|
||||
fun setUp() {
|
||||
setUpBase()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN null fromSwapCurrencyStatus THEN returns null`() {
|
||||
val provider = swapProvider()
|
||||
val model = createModel()
|
||||
model.dataState = model.dataState.copy(
|
||||
fromSwapCurrencyStatus = null,
|
||||
lastLoadedSwapStates = mapOf(provider to quotesLoadedState(provider, permissionSettings())),
|
||||
)
|
||||
|
||||
assertThat(model.getSelectApprovalTypeParams(provider)).isNull()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN provider state is not QuotesLoadedState THEN returns null`() {
|
||||
val provider = swapProvider()
|
||||
val notLoaded: SwapState.EmptyAmountState = mockk(relaxed = true)
|
||||
val model = createModel()
|
||||
model.dataState = model.dataState.copy(
|
||||
fromSwapCurrencyStatus = swapCurrencyStatus(),
|
||||
lastLoadedSwapStates = mapOf(provider to notLoaded),
|
||||
)
|
||||
|
||||
assertThat(model.getSelectApprovalTypeParams(provider)).isNull()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN PermissionSettings present THEN uses its type as initialApproveType`() {
|
||||
val provider = swapProvider(name = "ParaSwap")
|
||||
val currency = mockk<CryptoCurrency>(relaxed = true) { every { symbol } returns "USDT" }
|
||||
val status = mockk<CryptoCurrencyStatus>(relaxed = true)
|
||||
val from = swapCurrencyStatus(status = status, currency = currency)
|
||||
val model = createModel()
|
||||
model.dataState = model.dataState.copy(
|
||||
fromSwapCurrencyStatus = from,
|
||||
lastLoadedSwapStates = mapOf(
|
||||
provider to quotesLoadedState(provider, permissionSettings(type = ApproveType.UNLIMITED)),
|
||||
),
|
||||
)
|
||||
|
||||
val params = model.getSelectApprovalTypeParams(provider)
|
||||
|
||||
assertThat(params).isNotNull()
|
||||
assertThat(params!!.userWalletId).isEqualTo(userWalletId)
|
||||
assertThat(params.cryptoCurrencyStatus).isEqualTo(status)
|
||||
assertThat(params.initialApproveType).isEqualTo(ApproveType.UNLIMITED)
|
||||
assertThat(params.callback).isSameInstanceAs(model.approvalSelectorCallback)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,92 @@
|
|||
package com.tangem.feature.swap.model
|
||||
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import com.tangem.feature.swap.domain.models.ui.PermissionDataState
|
||||
import io.mockk.every
|
||||
import org.junit.jupiter.api.BeforeEach
|
||||
import org.junit.jupiter.api.Test
|
||||
|
||||
/**
|
||||
* Tests for [SwapModel.isPermissionNotNeeded].
|
||||
*
|
||||
* The getter is `true` when the current loaded swap state needs no approval:
|
||||
* - always when [PermissionDataState.Empty]
|
||||
* - additionally for [PermissionDataState.PermissionSettings] when the integrated-approve toggle is ON
|
||||
*/
|
||||
internal class SwapModelIsPermissionNotNeededTest : SwapModelTestBase() {
|
||||
|
||||
@BeforeEach
|
||||
fun setUp() {
|
||||
setUpBase()
|
||||
}
|
||||
|
||||
private fun modelWithPermissionState(
|
||||
permissionState: PermissionDataState,
|
||||
isIntegratedApproveEnabled: Boolean,
|
||||
): SwapModel {
|
||||
every { swapFeatureToggles.isSwapIntegratedApproveEnabled } returns isIntegratedApproveEnabled
|
||||
val provider = swapProvider()
|
||||
val model = createModel()
|
||||
model.dataState = model.dataState.copy(
|
||||
selectedProvider = provider,
|
||||
lastLoadedSwapStates = mapOf(provider to quotesLoadedState(provider, permissionState)),
|
||||
)
|
||||
return model
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN toggle OFF and Empty permission THEN permission is not needed`() {
|
||||
val model = modelWithPermissionState(PermissionDataState.Empty, isIntegratedApproveEnabled = false)
|
||||
|
||||
assertThat(model.isPermissionNotNeeded).isTrue()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN toggle OFF and PermissionSettings THEN permission is needed`() {
|
||||
val model = modelWithPermissionState(permissionSettings(), isIntegratedApproveEnabled = false)
|
||||
|
||||
assertThat(model.isPermissionNotNeeded).isFalse()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN toggle OFF and PermissionRequired THEN permission is needed`() {
|
||||
val model = modelWithPermissionState(
|
||||
PermissionDataState.PermissionRequired(isResetApproval = false, spenderAddress = "0x"),
|
||||
isIntegratedApproveEnabled = false,
|
||||
)
|
||||
|
||||
assertThat(model.isPermissionNotNeeded).isFalse()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN toggle ON and Empty permission THEN permission is not needed`() {
|
||||
val model = modelWithPermissionState(PermissionDataState.Empty, isIntegratedApproveEnabled = true)
|
||||
|
||||
assertThat(model.isPermissionNotNeeded).isTrue()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN toggle ON and PermissionSettings THEN permission is not needed`() {
|
||||
val model = modelWithPermissionState(permissionSettings(), isIntegratedApproveEnabled = true)
|
||||
|
||||
assertThat(model.isPermissionNotNeeded).isTrue()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN toggle ON and PermissionRequired THEN permission is needed`() {
|
||||
val model = modelWithPermissionState(
|
||||
PermissionDataState.PermissionRequired(isResetApproval = false, spenderAddress = "0x"),
|
||||
isIntegratedApproveEnabled = true,
|
||||
)
|
||||
|
||||
assertThat(model.isPermissionNotNeeded).isFalse()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN no current loaded state THEN permission is needed`() {
|
||||
every { swapFeatureToggles.isSwapIntegratedApproveEnabled } returns true
|
||||
val model = createModel()
|
||||
|
||||
assertThat(model.isPermissionNotNeeded).isFalse()
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,236 @@
|
|||
package com.tangem.feature.swap.model
|
||||
|
||||
import arrow.core.right
|
||||
import com.tangem.common.ui.bottomsheet.permission.state.ApproveType
|
||||
import com.tangem.core.analytics.api.AnalyticsErrorHandler
|
||||
import com.tangem.core.analytics.api.AnalyticsEventHandler
|
||||
import com.tangem.core.decompose.model.MutableParamsContainer
|
||||
import com.tangem.core.decompose.navigation.Router
|
||||
import com.tangem.core.decompose.ui.UiMessageSender
|
||||
import com.tangem.core.navigation.url.UrlOpener
|
||||
import com.tangem.common.routing.AppRouter
|
||||
import com.tangem.datasource.local.appsflyer.AppsFlyerStore
|
||||
import com.tangem.domain.account.status.usecase.GetAccountCurrencyStatusUseCase
|
||||
import com.tangem.domain.account.status.usecase.GetFeePaidCryptoCurrencyStatusSyncUseCase
|
||||
import com.tangem.domain.account.status.usecase.IsAccountsModeEnabledUseCase
|
||||
import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase
|
||||
import com.tangem.domain.balancehiding.GetBalanceHidingSettingsUseCase
|
||||
import com.tangem.domain.feedback.GetWalletMetaInfoUseCase
|
||||
import com.tangem.domain.feedback.SaveBlockchainErrorUseCase
|
||||
import com.tangem.domain.feedback.SendFeedbackEmailUseCase
|
||||
import com.tangem.domain.models.currency.CryptoCurrency
|
||||
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.pay.usecase.GetPaymentAccountCryptoCurrencyStatusUseCase
|
||||
import com.tangem.domain.settings.usercountry.GetUserCountryUseCase
|
||||
import com.tangem.domain.settings.usercountry.models.UserCountry
|
||||
import com.tangem.domain.stories.ShouldShowStoriesUseCase
|
||||
import com.tangem.domain.swap.models.SwapCurrencyStatus
|
||||
import com.tangem.domain.swap.usecase.CalculateAmountUseCase
|
||||
import com.tangem.domain.tangempay.GetTangemPayCustomerIdUseCase
|
||||
import com.tangem.domain.tangempay.TangemPayWithdrawUseCase
|
||||
import com.tangem.domain.tangempay.TangemPayWithdrawWithSwapUseCase
|
||||
import com.tangem.domain.tokens.GetMinimumTransactionAmountSyncUseCase
|
||||
import com.tangem.domain.tokens.UpdateDelayedNetworkStatusUseCase
|
||||
import com.tangem.domain.transaction.usecase.gasless.IsGaslessFeeSupportedForNetwork
|
||||
import com.tangem.domain.txhistory.usecase.GetExplorerTransactionUrlUseCase
|
||||
import com.tangem.feature.swap.domain.GetSwapUiModeUseCase
|
||||
import com.tangem.feature.swap.domain.SetSwapUiModeUseCase
|
||||
import com.tangem.feature.swap.domain.AllowPermissionsHandler
|
||||
import com.tangem.feature.swap.domain.SwapInteractor
|
||||
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.SwapState
|
||||
import com.tangem.feature.swap.domain.transfer.SwapTransferInteractor
|
||||
import com.tangem.feature.swap.ui.transfer.SwapTransferStateBuilder
|
||||
import com.tangem.features.commonfeatures.api.choosetoken.ChooseTokenBridge
|
||||
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.coroutines.TestingCoroutineDispatcherProvider
|
||||
import io.mockk.coEvery
|
||||
import io.mockk.every
|
||||
import io.mockk.mockk
|
||||
import kotlinx.coroutines.channels.Channel
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.emptyFlow
|
||||
|
||||
/**
|
||||
* Shared test infrastructure for [SwapModel] unit tests.
|
||||
*
|
||||
* Wires every constructor dependency as a relaxed MockK and stubs the init-block calls so that
|
||||
* constructing the model has no observable side effects. See `swap-model-test-plan.md` for details.
|
||||
*/
|
||||
internal abstract class SwapModelTestBase {
|
||||
|
||||
protected val router: Router = mockk(relaxed = true)
|
||||
protected val appRouter: AppRouter = mockk(relaxed = true)
|
||||
protected val analyticsEventHandler: AnalyticsEventHandler = mockk(relaxed = true)
|
||||
protected val analyticsErrorEventHandler: AnalyticsErrorHandler = mockk(relaxed = true)
|
||||
protected val getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase = mockk(relaxed = true)
|
||||
protected val updateDelayedCurrencyStatusUseCase: UpdateDelayedNetworkStatusUseCase = mockk(relaxed = true)
|
||||
protected val getFeePaidCryptoCurrencyStatusSyncUseCase: GetFeePaidCryptoCurrencyStatusSyncUseCase =
|
||||
mockk(relaxed = true)
|
||||
protected val getWalletMetaInfoUseCase: GetWalletMetaInfoUseCase = mockk(relaxed = true)
|
||||
protected val saveBlockchainErrorUseCase: SaveBlockchainErrorUseCase = mockk(relaxed = true)
|
||||
protected val sendFeedbackEmailUseCase: SendFeedbackEmailUseCase = mockk(relaxed = true)
|
||||
protected val getMinimumTransactionAmountSyncUseCase: GetMinimumTransactionAmountSyncUseCase =
|
||||
mockk(relaxed = true)
|
||||
protected val getExplorerTransactionUrlUseCase: GetExplorerTransactionUrlUseCase = mockk(relaxed = true)
|
||||
protected val shouldShowStoriesUseCase: ShouldShowStoriesUseCase = mockk(relaxed = true)
|
||||
protected val isAccountsModeEnabledUseCase: IsAccountsModeEnabledUseCase = mockk(relaxed = true)
|
||||
protected val swapInteractor: SwapInteractor = mockk(relaxed = true)
|
||||
protected val swapTransferInteractor: SwapTransferInteractor = mockk(relaxed = true)
|
||||
protected val swapTransferStateBuilder: SwapTransferStateBuilder = mockk(relaxed = true)
|
||||
protected val urlOpener: UrlOpener = mockk(relaxed = true)
|
||||
protected val getAccountCurrencyStatusUseCase: GetAccountCurrencyStatusUseCase = mockk(relaxed = true)
|
||||
protected val getPaymentAccountCryptoCurrencyStatusUseCase: GetPaymentAccountCryptoCurrencyStatusUseCase =
|
||||
mockk(relaxed = true)
|
||||
protected val tangemPayWithdrawWithSwapUseCase: TangemPayWithdrawWithSwapUseCase = mockk(relaxed = true)
|
||||
protected val isGaslessFeeSupportedForNetwork: IsGaslessFeeSupportedForNetwork = mockk(relaxed = true)
|
||||
protected val feeSelectorReloadTrigger: FeeSelectorReloadTrigger = mockk(relaxed = true)
|
||||
protected val getTangemPayCustomerIdUseCase: GetTangemPayCustomerIdUseCase = mockk(relaxed = true)
|
||||
protected val appsFlyerStore: AppsFlyerStore = mockk(relaxed = true)
|
||||
protected val messageSender: UiMessageSender = mockk(relaxed = true)
|
||||
protected val initialCurrenciesResolver: InitialCurrenciesResolver = mockk(relaxed = true)
|
||||
protected val allowPermissionsHandler: AllowPermissionsHandler = mockk(relaxed = true)
|
||||
protected val swapFeatureToggles: SwapFeatureToggles = mockk(relaxed = true)
|
||||
protected val getSwapUiModeUseCase: GetSwapUiModeUseCase = mockk(relaxed = true)
|
||||
protected val setSwapUiModeUseCase: SetSwapUiModeUseCase = mockk(relaxed = true)
|
||||
protected val calculateAmountUseCase: CalculateAmountUseCase = mockk(relaxed = true)
|
||||
|
||||
private val chooseTokenBridgeFactory: ChooseTokenBridge.Factory = mockk(relaxed = true)
|
||||
private val getUserCountryUseCase: GetUserCountryUseCase = mockk(relaxed = true)
|
||||
private val getBalanceHidingSettingsUseCase: GetBalanceHidingSettingsUseCase = mockk(relaxed = true)
|
||||
|
||||
protected val userWalletId = UserWalletId(stringValue = "0123456789ABCDEF")
|
||||
|
||||
/** Stubs init-block calls so [createModel] has no side effects. Call from `@BeforeEach`. */
|
||||
protected fun setUpBase() {
|
||||
val bridge = mockk<ChooseTokenBridge>(relaxed = true) {
|
||||
every { onCurrencyChosen } returns Channel()
|
||||
every { onClose } returns Channel()
|
||||
}
|
||||
every { chooseTokenBridgeFactory.create(any(), any(), any()) } returns bridge
|
||||
|
||||
every { getUserCountryUseCase.invokeSync() } returns UserCountry.Other("US").right()
|
||||
every { getBalanceHidingSettingsUseCase.invoke() } returns emptyFlow()
|
||||
coEvery { isAccountsModeEnabledUseCase.invokeSync() } returns false
|
||||
coEvery { shouldShowStoriesUseCase.invokeSync(any()) } returns false
|
||||
coEvery { initialCurrenciesResolver.invoke(any(), any(), any(), any()) } returns (null to null)
|
||||
every { getSelectedAppCurrencyUseCase.invoke() } returns emptyFlow()
|
||||
every { swapFeatureToggles.isSwapIntegratedApproveEnabled } returns true
|
||||
}
|
||||
|
||||
protected fun createParams(): SwapComponent.Params = SwapComponent.Params(
|
||||
userWalletId = userWalletId,
|
||||
cryptoCurrency = null,
|
||||
screenSource = "Test",
|
||||
)
|
||||
|
||||
@Suppress("LongMethod")
|
||||
protected fun createModel(): SwapModel = SwapModel(
|
||||
paramsContainer = MutableParamsContainer(createParams()),
|
||||
getUserCountryUseCase = getUserCountryUseCase,
|
||||
getBalanceHidingSettingsUseCase = getBalanceHidingSettingsUseCase,
|
||||
chooseTokenBridgeFactory = chooseTokenBridgeFactory,
|
||||
router = router,
|
||||
appRouter = appRouter,
|
||||
dispatchers = TestingCoroutineDispatcherProvider(),
|
||||
analyticsEventHandler = analyticsEventHandler,
|
||||
analyticsErrorEventHandler = analyticsErrorEventHandler,
|
||||
getSelectedAppCurrencyUseCase = getSelectedAppCurrencyUseCase,
|
||||
updateDelayedCurrencyStatusUseCase = updateDelayedCurrencyStatusUseCase,
|
||||
getFeePaidCryptoCurrencyStatusSyncUseCase = getFeePaidCryptoCurrencyStatusSyncUseCase,
|
||||
getWalletMetaInfoUseCase = getWalletMetaInfoUseCase,
|
||||
saveBlockchainErrorUseCase = saveBlockchainErrorUseCase,
|
||||
sendFeedbackEmailUseCase = sendFeedbackEmailUseCase,
|
||||
getMinimumTransactionAmountSyncUseCase = getMinimumTransactionAmountSyncUseCase,
|
||||
getExplorerTransactionUrlUseCase = getExplorerTransactionUrlUseCase,
|
||||
shouldShowStoriesUseCase = shouldShowStoriesUseCase,
|
||||
isAccountsModeEnabledUseCase = isAccountsModeEnabledUseCase,
|
||||
swapInteractor = swapInteractor,
|
||||
swapTransferInteractor = swapTransferInteractor,
|
||||
swapTransferStateBuilder = swapTransferStateBuilder,
|
||||
urlOpener = urlOpener,
|
||||
getAccountCurrencyStatusUseCase = getAccountCurrencyStatusUseCase,
|
||||
getPaymentAccountCryptoCurrencyStatusUseCase = getPaymentAccountCryptoCurrencyStatusUseCase,
|
||||
tangemPayWithdrawWithSwapUseCase = tangemPayWithdrawWithSwapUseCase,
|
||||
isGaslessFeeSupportedForNetwork = isGaslessFeeSupportedForNetwork,
|
||||
feeSelectorReloadTrigger = feeSelectorReloadTrigger,
|
||||
getTangemPayCustomerIdUseCase = getTangemPayCustomerIdUseCase,
|
||||
appsFlyerStore = appsFlyerStore,
|
||||
messageSender = messageSender,
|
||||
initialCurrenciesResolver = initialCurrenciesResolver,
|
||||
allowPermissionsHandler = allowPermissionsHandler,
|
||||
swapFeatureToggles = swapFeatureToggles,
|
||||
getSwapUiModeUseCase = getSwapUiModeUseCase,
|
||||
setSwapUiModeUseCase = setSwapUiModeUseCase,
|
||||
calculateAmountUseCase = calculateAmountUseCase,
|
||||
)
|
||||
|
||||
// region builders
|
||||
|
||||
protected fun swapProvider(
|
||||
id: String = "provider-1",
|
||||
name: String = "1inch",
|
||||
type: ExchangeProviderType = ExchangeProviderType.DEX,
|
||||
): SwapProvider = SwapProvider(
|
||||
providerId = id,
|
||||
name = name,
|
||||
type = type,
|
||||
imageLarge = "",
|
||||
termsOfUse = null,
|
||||
privacyPolicy = null,
|
||||
slippage = null,
|
||||
)
|
||||
|
||||
protected fun permissionSettings(
|
||||
type: ApproveType = ApproveType.LIMITED,
|
||||
spender: String = "0xSpender",
|
||||
): PermissionDataState.PermissionSettings = PermissionDataState.PermissionSettings(
|
||||
type = type,
|
||||
spenderAddress = spender,
|
||||
)
|
||||
|
||||
protected fun quotesLoadedState(
|
||||
provider: SwapProvider,
|
||||
permissionState: PermissionDataState = PermissionDataState.Empty,
|
||||
): SwapState.QuotesLoadedState = mockk(relaxed = true) {
|
||||
every { swapProvider } returns provider
|
||||
every { this@mockk.permissionState } returns permissionState
|
||||
every {
|
||||
copy(
|
||||
fromTokenInfo = any(),
|
||||
toTokenInfo = any(),
|
||||
priceImpact = any(),
|
||||
preparedSwapConfigState = any(),
|
||||
permissionState = any(),
|
||||
swapDataModel = any(),
|
||||
currencyCheck = any(),
|
||||
validationResult = any(),
|
||||
minAdaValue = any(),
|
||||
swapProvider = any(),
|
||||
)
|
||||
} answers {
|
||||
quotesLoadedState(
|
||||
provider = provider,
|
||||
permissionState = arg(4),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
protected fun swapCurrencyStatus(
|
||||
wallet: UserWallet = mockk(relaxed = true),
|
||||
status: CryptoCurrencyStatus = mockk(relaxed = true),
|
||||
currency: CryptoCurrency = mockk(relaxed = true),
|
||||
): SwapCurrencyStatus = mockk(relaxed = true) {
|
||||
every { userWallet } returns wallet
|
||||
every { this@mockk.status } returns status
|
||||
every { this@mockk.currency } returns currency
|
||||
}
|
||||
|
||||
// endregion
|
||||
}
|
||||
|
|
@ -43,6 +43,7 @@ internal class SwapTransferNotificationsFactoryTest {
|
|||
fee = null,
|
||||
onReduceByAmount = { _, _ -> },
|
||||
onReduceToAmount = {},
|
||||
onBuyClick = {},
|
||||
)
|
||||
|
||||
assertThat(result).isEmpty()
|
||||
|
|
@ -65,6 +66,7 @@ internal class SwapTransferNotificationsFactoryTest {
|
|||
fee = null,
|
||||
onReduceByAmount = { _, _ -> },
|
||||
onReduceToAmount = {},
|
||||
onBuyClick = {},
|
||||
)
|
||||
|
||||
assertThat(result.filterIsInstance<NotificationUM.Solana.RentInfo>()).hasSize(1)
|
||||
|
|
@ -91,6 +93,7 @@ internal class SwapTransferNotificationsFactoryTest {
|
|||
fee = fee,
|
||||
onReduceByAmount = { _, _ -> },
|
||||
onReduceToAmount = {},
|
||||
onBuyClick = {},
|
||||
)
|
||||
|
||||
assertThat(result.filterIsInstance<NotificationUM.Error.ExistentialDeposit>()).hasSize(1)
|
||||
|
|
@ -113,6 +116,7 @@ internal class SwapTransferNotificationsFactoryTest {
|
|||
fee = null,
|
||||
onReduceByAmount = { _, _ -> },
|
||||
onReduceToAmount = {},
|
||||
onBuyClick = {},
|
||||
)
|
||||
|
||||
assertThat(result.filterIsInstance<NotificationUM.Error.MinimumAmountError>()).hasSize(1)
|
||||
|
|
@ -131,6 +135,7 @@ internal class SwapTransferNotificationsFactoryTest {
|
|||
fee = null,
|
||||
onReduceByAmount = { _, _ -> },
|
||||
onReduceToAmount = {},
|
||||
onBuyClick = {},
|
||||
)
|
||||
|
||||
assertThat(result.filterIsInstance<NotificationUM.Cardano.MinAdaValueCharged>()).hasSize(1)
|
||||
|
|
@ -154,6 +159,7 @@ internal class SwapTransferNotificationsFactoryTest {
|
|||
fee = null,
|
||||
onReduceByAmount = { _, _ -> },
|
||||
onReduceToAmount = {},
|
||||
onBuyClick = {},
|
||||
)
|
||||
|
||||
assertThat(result.filterIsInstance<NotificationUM.Warning.FeeCoverageNotification>()).hasSize(1)
|
||||
|
|
@ -176,6 +182,7 @@ internal class SwapTransferNotificationsFactoryTest {
|
|||
fee = null,
|
||||
onReduceByAmount = { _, _ -> },
|
||||
onReduceToAmount = {},
|
||||
onBuyClick = {},
|
||||
)
|
||||
|
||||
val reserve = result.filterIsInstance<SwapNotificationUM.Warning.NeedReserveToCreateAccount>()
|
||||
|
|
@ -199,15 +206,39 @@ internal class SwapTransferNotificationsFactoryTest {
|
|||
fee = null,
|
||||
onReduceByAmount = { _, _ -> },
|
||||
onReduceToAmount = {},
|
||||
onBuyClick = {},
|
||||
)
|
||||
|
||||
assertThat(result.filterIsInstance<SwapNotificationUM.Warning.ReduceAmount>()).hasSize(1)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN BalanceNotEnoughForFee warning WHEN getNotifications THEN TokenExceedsBalance is added`() = runTest {
|
||||
val warning = CryptoCurrencyWarning.BalanceNotEnoughForFee(
|
||||
tokenCurrency = buildCoin(),
|
||||
coinCurrency = buildCoin(),
|
||||
)
|
||||
val transferState = buildTransferState(
|
||||
cryptoCurrencyWarning = warning,
|
||||
)
|
||||
|
||||
val result = sut.getNotifications(
|
||||
transferState = transferState,
|
||||
feeCryptoCurrencyStatus = null,
|
||||
fee = null,
|
||||
onReduceByAmount = { _, _ -> },
|
||||
onReduceToAmount = {},
|
||||
onBuyClick = {},
|
||||
)
|
||||
|
||||
assertThat(result.filterIsInstance<NotificationUM.Error.TokenExceedsBalance>()).hasSize(1)
|
||||
}
|
||||
|
||||
@Suppress("LongParameterList")
|
||||
private fun buildTransferState(
|
||||
fromTokenInfo: TokenSwapInfo = buildTokenInfo(buildCoinStatus()),
|
||||
toTokenInfo: TokenSwapInfo = buildTokenInfo(buildCoinStatus()),
|
||||
cryptoCurrencyWarning: CryptoCurrencyWarning? = null,
|
||||
currencyCheck: CryptoCurrencyCheck? = null,
|
||||
validationResult: Throwable? = null,
|
||||
minAdaValue: BigDecimal? = null,
|
||||
|
|
@ -217,6 +248,7 @@ internal class SwapTransferNotificationsFactoryTest {
|
|||
userWallet = coldWallet,
|
||||
fromTokenInfo = fromTokenInfo,
|
||||
toTokenInfo = toTokenInfo,
|
||||
cryptoCurrencyWarning = cryptoCurrencyWarning,
|
||||
isInsufficientBalance = false,
|
||||
appCurrency = AppCurrency.Default,
|
||||
isBalanceHidden = false,
|
||||
|
|
|
|||
|
|
@ -56,6 +56,7 @@ internal class SwapTransferStateBuilderTest {
|
|||
fee = any(),
|
||||
onReduceByAmount = any(),
|
||||
onReduceToAmount = any(),
|
||||
onBuyClick = any(),
|
||||
)
|
||||
} returns persistentListOf()
|
||||
}
|
||||
|
|
@ -129,6 +130,7 @@ internal class SwapTransferStateBuilderTest {
|
|||
fee = null,
|
||||
onReduceByAmount = any(),
|
||||
onReduceToAmount = any(),
|
||||
onBuyClick = any(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -170,6 +172,7 @@ internal class SwapTransferStateBuilderTest {
|
|||
fee = null,
|
||||
onReduceByAmount = any(),
|
||||
onReduceToAmount = any(),
|
||||
onBuyClick = any(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -212,6 +215,7 @@ internal class SwapTransferStateBuilderTest {
|
|||
fee = null,
|
||||
onReduceByAmount = any(),
|
||||
onReduceToAmount = any(),
|
||||
onBuyClick = any(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -260,6 +264,7 @@ internal class SwapTransferStateBuilderTest {
|
|||
fee = null,
|
||||
onReduceByAmount = any(),
|
||||
onReduceToAmount = any(),
|
||||
onBuyClick = any(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -307,6 +312,7 @@ internal class SwapTransferStateBuilderTest {
|
|||
fee = fee,
|
||||
onReduceByAmount = any(),
|
||||
onReduceToAmount = any(),
|
||||
onBuyClick = any(),
|
||||
)
|
||||
} returns persistentListOf()
|
||||
|
||||
|
|
@ -317,6 +323,7 @@ internal class SwapTransferStateBuilderTest {
|
|||
uiStateHolder = uiState,
|
||||
feePaidCryptoCurrencyStatus = null,
|
||||
fee = fee,
|
||||
isTangemPayWithdrawal = false,
|
||||
)
|
||||
|
||||
assertThat(result.swapButton.isEnabled).isTrue()
|
||||
|
|
@ -329,6 +336,7 @@ internal class SwapTransferStateBuilderTest {
|
|||
fee = fee,
|
||||
onReduceByAmount = any(),
|
||||
onReduceToAmount = any(),
|
||||
onBuyClick = any(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -358,6 +366,7 @@ internal class SwapTransferStateBuilderTest {
|
|||
uiStateHolder = uiState,
|
||||
feePaidCryptoCurrencyStatus = null,
|
||||
fee = fee,
|
||||
isTangemPayWithdrawal = false,
|
||||
)
|
||||
|
||||
assertThat(result.transferFooter).isInstanceOf(TextReference.Combined::class.java)
|
||||
|
|
@ -408,6 +417,7 @@ internal class SwapTransferStateBuilderTest {
|
|||
uiStateHolder = uiState,
|
||||
feePaidCryptoCurrencyStatus = null,
|
||||
fee = fee,
|
||||
isTangemPayWithdrawal = false,
|
||||
)
|
||||
|
||||
assertThat(result.transferFooter).isEqualTo(
|
||||
|
|
@ -452,6 +462,7 @@ internal class SwapTransferStateBuilderTest {
|
|||
uiStateHolder = uiState,
|
||||
feePaidCryptoCurrencyStatus = null,
|
||||
fee = fee,
|
||||
isTangemPayWithdrawal = false,
|
||||
)
|
||||
|
||||
assertThat(result.transferFooter).isEqualTo(
|
||||
|
|
@ -464,25 +475,40 @@ internal class SwapTransferStateBuilderTest {
|
|||
|
||||
@Test
|
||||
fun `GIVEN dataState with from-to currencies WHEN createSuccessState THEN success holder is built in transfer mode with given fee and txUrl`() {
|
||||
val appCurrency = AppCurrency(code = "USD", name = "US Dollar", symbol = "$")
|
||||
val amount = BigDecimal("1.5")
|
||||
val transferState = buildTransferState(
|
||||
fromAmount = amount,
|
||||
toAmount = amount,
|
||||
isAccountsMode = true,
|
||||
)
|
||||
val dataState = SwapProcessDataState(
|
||||
fromSwapCurrencyStatus = fromCurrencyStatus,
|
||||
toSwapCurrencyStatus = toCurrencyStatus,
|
||||
amount = amount.toPlainString(),
|
||||
currentTransferState = transferState,
|
||||
)
|
||||
val feeValue = BigDecimal("0.001")
|
||||
val fee = Fee.Common(
|
||||
amount = Amount(currencySymbol = "ETH", value = feeValue, decimals = 18),
|
||||
)
|
||||
val appCurrency = transferState.appCurrency
|
||||
val expectedFee = stringReference(
|
||||
"${feeValue.format { crypto(symbol = "ETH", decimals = 18) }} " +
|
||||
"(${
|
||||
fromCurrencyStatus.status.value.fiatRate!!.multiply(feeValue).format {
|
||||
fiat(fiatCurrencyCode = appCurrency.code, fiatCurrencySymbol = appCurrency.symbol)
|
||||
}
|
||||
})",
|
||||
)
|
||||
val fee: TextReference = stringReference("0.001 ETH")
|
||||
val txUrl = "https://explorer.example/tx/0xabc"
|
||||
val timestamp = 1_700_000_000_000L
|
||||
|
||||
val result = sut.createSuccessState(
|
||||
uiState = baseStateHolder(),
|
||||
dataState = dataState,
|
||||
appCurrency = appCurrency,
|
||||
isAccountsMode = true,
|
||||
fee = fee,
|
||||
txUrl = txUrl,
|
||||
timestamp = timestamp,
|
||||
fee = fee,
|
||||
onExplorerClick = {},
|
||||
)
|
||||
|
||||
|
|
@ -491,7 +517,7 @@ internal class SwapTransferStateBuilderTest {
|
|||
assertThat(success.shouldShowStatusButton).isFalse()
|
||||
assertThat(success.timestamp).isEqualTo(timestamp)
|
||||
assertThat(success.txUrl).isEqualTo(txUrl)
|
||||
assertThat(success.fee).isEqualTo(fee)
|
||||
assertThat(success.fee).isEqualTo(expectedFee)
|
||||
assertThat(success.providerName).isEqualTo(TextReference.EMPTY)
|
||||
assertThat(success.providerType).isEqualTo(TextReference.EMPTY)
|
||||
assertThat(success.providerIcon).isEmpty()
|
||||
|
|
@ -518,6 +544,140 @@ internal class SwapTransferStateBuilderTest {
|
|||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN null fee but tangem pay withdrawal WHEN updateTransferButtonEnableState THEN swap button is enabled with no footer`() =
|
||||
runTest {
|
||||
val transferState = buildTransferState(
|
||||
fromAmount = BigDecimal("1"),
|
||||
toAmount = BigDecimal("1"),
|
||||
isAccountsMode = false,
|
||||
)
|
||||
val dataState = SwapProcessDataState()
|
||||
val uiState = baseStateHolder().copy(
|
||||
swapButton = SwapButton(
|
||||
walletInteractionIcon = null,
|
||||
isEnabled = false,
|
||||
mode = SwapButton.Mode.TRANSFER,
|
||||
onClick = {},
|
||||
),
|
||||
)
|
||||
|
||||
val result = sut.updateTransferButtonEnableState(
|
||||
dataState = dataState,
|
||||
transferState = transferState,
|
||||
actions = actions,
|
||||
uiStateHolder = uiState,
|
||||
feePaidCryptoCurrencyStatus = null,
|
||||
fee = null,
|
||||
isTangemPayWithdrawal = true,
|
||||
)
|
||||
|
||||
assertThat(result.swapButton.isEnabled).isTrue()
|
||||
assertThat(result.swapButton.mode).isEqualTo(SwapButton.Mode.TRANSFER)
|
||||
// fee is null → footer is omitted, but the button stays enabled because it is a Tangem Pay withdrawal
|
||||
assertThat(result.transferFooter).isNull()
|
||||
assertThat(result.notifications).isEmpty()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN null fee and not tangem pay withdrawal WHEN updateTransferButtonEnableState THEN swap button stays disabled`() =
|
||||
runTest {
|
||||
val transferState = buildTransferState(
|
||||
fromAmount = BigDecimal("1"),
|
||||
toAmount = BigDecimal("1"),
|
||||
isAccountsMode = false,
|
||||
)
|
||||
val dataState = SwapProcessDataState()
|
||||
val uiState = baseStateHolder().copy(
|
||||
swapButton = SwapButton(
|
||||
walletInteractionIcon = null,
|
||||
isEnabled = false,
|
||||
mode = SwapButton.Mode.TRANSFER,
|
||||
onClick = {},
|
||||
),
|
||||
)
|
||||
|
||||
val result = sut.updateTransferButtonEnableState(
|
||||
dataState = dataState,
|
||||
transferState = transferState,
|
||||
actions = actions,
|
||||
uiStateHolder = uiState,
|
||||
feePaidCryptoCurrencyStatus = null,
|
||||
fee = null,
|
||||
isTangemPayWithdrawal = false,
|
||||
)
|
||||
|
||||
assertThat(result.swapButton.isEnabled).isFalse()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN transfer dataState WHEN createTangemPayWithdrawalSuccessState THEN feeless transfer success holder is built`() {
|
||||
val sendingAmount = BigDecimal("1.5")
|
||||
val transferState = buildTransferState(
|
||||
fromAmount = sendingAmount,
|
||||
toAmount = sendingAmount,
|
||||
isAccountsMode = true,
|
||||
)
|
||||
val dataState = SwapProcessDataState(
|
||||
fromSwapCurrencyStatus = fromCurrencyStatus,
|
||||
toSwapCurrencyStatus = toCurrencyStatus,
|
||||
currentTransferState = transferState,
|
||||
)
|
||||
val onExploreClick = {}
|
||||
val appCurrency = transferState.appCurrency
|
||||
val expectedFiat = stringReference(
|
||||
fromCurrencyStatus.status.value.fiatRate!!.multiply(sendingAmount).format {
|
||||
fiat(fiatCurrencyCode = appCurrency.code, fiatCurrencySymbol = appCurrency.symbol)
|
||||
},
|
||||
)
|
||||
|
||||
val before = System.currentTimeMillis()
|
||||
val result = sut.createTangemPayWithdrawalSuccessState(
|
||||
uiState = baseStateHolder(),
|
||||
dataState = dataState,
|
||||
fee = null,
|
||||
onExploreClick = onExploreClick,
|
||||
)
|
||||
val after = System.currentTimeMillis()
|
||||
|
||||
val success = requireNotNull(result.successState)
|
||||
assertThat(success.isTransferMode).isTrue()
|
||||
assertThat(success.shouldShowStatusButton).isFalse()
|
||||
assertThat(success.fee).isNull()
|
||||
assertThat(success.txUrl).isEmpty()
|
||||
assertThat(success.providerName).isEqualTo(stringReference(""))
|
||||
assertThat(success.providerType).isEqualTo(stringReference(""))
|
||||
assertThat(success.providerIcon).isEmpty()
|
||||
assertThat(success.rate).isEqualTo(TextReference.EMPTY)
|
||||
assertThat(success.timestamp).isAtLeast(before)
|
||||
assertThat(success.timestamp).isAtMost(after)
|
||||
assertThat(success.fromTokenAmount).isEqualTo(stringReference(sendingAmount.toString()))
|
||||
assertThat(success.toTokenAmount).isEqualTo(stringReference(sendingAmount.toString()))
|
||||
assertThat(success.fromTokenFiatAmount).isEqualTo(expectedFiat)
|
||||
assertThat(success.toTokenFiatAmount).isEqualTo(expectedFiat)
|
||||
assertThat(success.fromTokenIconState).isEqualTo(fromIcon)
|
||||
assertThat(success.toTokenIconState).isEqualTo(toIcon)
|
||||
assertThat(success.onExploreButtonClick).isEqualTo(onExploreClick)
|
||||
|
||||
val portfolioAccount = fromCurrencyStatus.account as Account.CryptoPortfolio
|
||||
val expectedIcon = CryptoPortfolioIconConverter.convert(portfolioAccount.icon)
|
||||
val expectedName = portfolioAccount.accountName.toUM().value
|
||||
assertThat(success.fromTitle).isEqualTo(
|
||||
AccountTitleUM.Account(
|
||||
prefixText = resourceReference(R.string.swapping_from_account_title),
|
||||
name = expectedName,
|
||||
icon = expectedIcon,
|
||||
),
|
||||
)
|
||||
assertThat(success.toTitle).isEqualTo(
|
||||
AccountTitleUM.Account(
|
||||
prefixText = resourceReference(R.string.swapping_to_account_title),
|
||||
name = expectedName,
|
||||
icon = expectedIcon,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
private fun assertSharedCardShape(
|
||||
result: SwapStateHolder,
|
||||
transferState: SwapState.Transfer,
|
||||
|
|
@ -570,6 +730,7 @@ internal class SwapTransferStateBuilderTest {
|
|||
userWallet = coldWallet,
|
||||
fromTokenInfo = fromInfo,
|
||||
toTokenInfo = toInfo,
|
||||
cryptoCurrencyWarning = null,
|
||||
isInsufficientBalance = isInsufficientBalance,
|
||||
appCurrency = AppCurrency.Default,
|
||||
isBalanceHidden = false,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue