Updated on 2026-08-14
This commit is contained in:
parent
23ca8014da
commit
7da444caf1
4 changed files with 115 additions and 80 deletions
|
|
@ -19,7 +19,7 @@ data class SwapStateHolder(
|
|||
val blockchainId: String, // not the same as networkId, its local id in app
|
||||
val warnings: List<SwapWarning> = emptyList(),
|
||||
val alert: SwapWarning.GenericWarning? = null,
|
||||
val updateInProgress: Boolean = false,
|
||||
val changeCardsButtonState: ChangeCardsButtonState = ChangeCardsButtonState.ENABLED,
|
||||
val providerState: ProviderState,
|
||||
|
||||
val fee: FeeItemState = FeeItemState.Empty,
|
||||
|
|
@ -131,4 +131,8 @@ sealed interface SwapWarning {
|
|||
|
||||
enum class GenericWarningType {
|
||||
NETWORK, OTHER
|
||||
}
|
||||
|
||||
enum class ChangeCardsButtonState {
|
||||
ENABLED, DISABLED, UPDATE_IN_PROGRESS
|
||||
}
|
||||
|
|
@ -84,7 +84,7 @@ internal class StateBuilder(
|
|||
onBackClicked = actions.onBackClicked,
|
||||
onChangeCardsClicked = actions.onChangeCardsClicked,
|
||||
onMaxAmountSelected = actions.onMaxAmountSelected,
|
||||
updateInProgress = true,
|
||||
changeCardsButtonState = ChangeCardsButtonState.UPDATE_IN_PROGRESS,
|
||||
onShowPermissionBottomSheet = actions.openPermissionBottomSheet,
|
||||
providerState = ProviderState.Empty(),
|
||||
)
|
||||
|
|
@ -138,7 +138,7 @@ internal class StateBuilder(
|
|||
loading = false,
|
||||
onClick = { },
|
||||
),
|
||||
updateInProgress = false,
|
||||
changeCardsButtonState = ChangeCardsButtonState.DISABLED,
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -186,7 +186,7 @@ internal class StateBuilder(
|
|||
swapButton = SwapButton(enabled = false, loading = true, onClick = {}),
|
||||
providerState = ProviderState.Loading(),
|
||||
permissionState = uiStateHolder.permissionState,
|
||||
updateInProgress = true,
|
||||
changeCardsButtonState = ChangeCardsButtonState.UPDATE_IN_PROGRESS,
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -207,6 +207,7 @@ internal class StateBuilder(
|
|||
bestRatedProviderId: String,
|
||||
isManyProviders: Boolean,
|
||||
selectedFeeType: FeeType,
|
||||
isReverseSwapPossible: Boolean,
|
||||
): SwapStateHolder {
|
||||
if (uiStateHolder.sendCardData !is SwapCardState.SwapCardData) return uiStateHolder
|
||||
if (uiStateHolder.receiveCardData !is SwapCardState.SwapCardData) return uiStateHolder
|
||||
|
|
@ -257,7 +258,11 @@ internal class StateBuilder(
|
|||
loading = false,
|
||||
onClick = actions.onSwapClick,
|
||||
),
|
||||
updateInProgress = false,
|
||||
changeCardsButtonState = if (isReverseSwapPossible) {
|
||||
ChangeCardsButtonState.ENABLED
|
||||
} else {
|
||||
ChangeCardsButtonState.DISABLED
|
||||
},
|
||||
providerState = swapProvider.convertToContentClickableProviderState(
|
||||
isBestRate = bestRatedProviderId == swapProvider.providerId,
|
||||
fromTokenInfo = quoteModel.fromTokenInfo,
|
||||
|
|
@ -360,6 +365,7 @@ internal class StateBuilder(
|
|||
fromToken: TokenSwapInfo,
|
||||
toToken: CryptoCurrencyStatus?,
|
||||
dataError: DataError,
|
||||
isReverseSwapPossible: Boolean
|
||||
): SwapStateHolder {
|
||||
if (uiStateHolder.sendCardData !is SwapCardState.SwapCardData) return uiStateHolder
|
||||
if (uiStateHolder.receiveCardData !is SwapCardState.SwapCardData) return uiStateHolder
|
||||
|
|
@ -409,7 +415,11 @@ internal class StateBuilder(
|
|||
loading = false,
|
||||
onClick = actions.onSwapClick,
|
||||
),
|
||||
updateInProgress = false,
|
||||
changeCardsButtonState = if (isReverseSwapPossible) {
|
||||
ChangeCardsButtonState.ENABLED
|
||||
} else {
|
||||
ChangeCardsButtonState.DISABLED
|
||||
},
|
||||
providerState = providerState,
|
||||
)
|
||||
}
|
||||
|
|
@ -475,6 +485,7 @@ internal class StateBuilder(
|
|||
fun createQuotesEmptyAmountState(
|
||||
uiStateHolder: SwapStateHolder,
|
||||
emptyAmountState: SwapState.EmptyAmountState,
|
||||
isReverseSwapPossible: Boolean,
|
||||
): SwapStateHolder {
|
||||
if (uiStateHolder.sendCardData !is SwapCardState.SwapCardData) return uiStateHolder
|
||||
if (uiStateHolder.receiveCardData !is SwapCardState.SwapCardData) return uiStateHolder
|
||||
|
|
@ -514,7 +525,11 @@ internal class StateBuilder(
|
|||
loading = false,
|
||||
onClick = { },
|
||||
),
|
||||
updateInProgress = false,
|
||||
changeCardsButtonState = if (isReverseSwapPossible) {
|
||||
ChangeCardsButtonState.ENABLED
|
||||
} else {
|
||||
ChangeCardsButtonState.DISABLED
|
||||
},
|
||||
providerState = ProviderState.Empty(),
|
||||
)
|
||||
}
|
||||
|
|
@ -538,7 +553,7 @@ internal class StateBuilder(
|
|||
|
||||
fun createSilentLoadState(uiState: SwapStateHolder): SwapStateHolder {
|
||||
return uiState.copy(
|
||||
updateInProgress = true,
|
||||
changeCardsButtonState = ChangeCardsButtonState.UPDATE_IN_PROGRESS,
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -713,7 +728,7 @@ internal class StateBuilder(
|
|||
onClick = onAlertClick,
|
||||
type = if (txState is TxState.NetworkError) GenericWarningType.NETWORK else GenericWarningType.OTHER,
|
||||
),
|
||||
updateInProgress = false,
|
||||
changeCardsButtonState = ChangeCardsButtonState.ENABLED,
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -195,23 +195,36 @@ private fun SwapButton(state: SwapStateHolder, modifier: Modifier = Modifier) {
|
|||
contentColor = TangemTheme.colors.text.primary1,
|
||||
modifier = modifier.size(TangemTheme.dimens.size48),
|
||||
onClick = state.onChangeCardsClicked,
|
||||
enabled = !state.updateInProgress,
|
||||
enabled = state.changeCardsButtonState == ChangeCardsButtonState.ENABLED,
|
||||
) {
|
||||
if (state.updateInProgress) {
|
||||
CircularProgressIndicator(
|
||||
modifier = Modifier
|
||||
.size(TangemTheme.dimens.size16)
|
||||
.padding(TangemTheme.dimens.spacing14),
|
||||
color = TangemTheme.colors.icon.primary1,
|
||||
strokeWidth = TangemTheme.dimens.size2,
|
||||
)
|
||||
} else {
|
||||
Icon(
|
||||
painter = painterResource(id = R.drawable.ic_exchange_vertical_24),
|
||||
contentDescription = null,
|
||||
tint = TangemTheme.colors.text.primary1,
|
||||
modifier = Modifier.padding(TangemTheme.dimens.spacing12),
|
||||
)
|
||||
when (state.changeCardsButtonState) {
|
||||
ChangeCardsButtonState.UPDATE_IN_PROGRESS -> {
|
||||
CircularProgressIndicator(
|
||||
modifier = Modifier
|
||||
.size(TangemTheme.dimens.size16)
|
||||
.padding(TangemTheme.dimens.spacing14),
|
||||
color = TangemTheme.colors.icon.primary1,
|
||||
strokeWidth = TangemTheme.dimens.size2,
|
||||
)
|
||||
}
|
||||
ChangeCardsButtonState.ENABLED -> {
|
||||
Icon(
|
||||
painter = painterResource(id = R.drawable.ic_exchange_vertical_24),
|
||||
contentDescription = null,
|
||||
tint = TangemTheme.colors.text.primary1,
|
||||
modifier = Modifier.padding(TangemTheme.dimens.spacing12),
|
||||
)
|
||||
}
|
||||
ChangeCardsButtonState.DISABLED -> {
|
||||
Icon(
|
||||
painter = painterResource(id = R.drawable.ic_exchange_vertical_24),
|
||||
contentDescription = null,
|
||||
tint = TangemTheme.colors.text.disabled,
|
||||
modifier = Modifier.padding(TangemTheme.dimens.spacing12),
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -60,8 +60,8 @@ internal class SwapViewModel @Inject constructor(
|
|||
savedStateHandle: SavedStateHandle,
|
||||
) : ViewModel(), DefaultLifecycleObserver {
|
||||
|
||||
private val initialCryptoCurrency: CryptoCurrency = savedStateHandle[SwapFragment.CURRENCY_BUNDLE_KEY]
|
||||
?: error("no expected parameter CryptoCurrency found`")
|
||||
private val initialCryptoCurrency: CryptoCurrency =
|
||||
savedStateHandle[SwapFragment.CURRENCY_BUNDLE_KEY] ?: error("no expected parameter CryptoCurrency found`")
|
||||
private lateinit var initialCryptoCurrencyStatus: CryptoCurrencyStatus
|
||||
|
||||
private var isBalanceHidden = true
|
||||
|
|
@ -114,15 +114,12 @@ internal class SwapViewModel @Inject constructor(
|
|||
}
|
||||
|
||||
override fun onCreate(owner: LifecycleOwner) {
|
||||
getBalanceHidingSettingsUseCase()
|
||||
.flowWithLifecycle(owner.lifecycle)
|
||||
.onEach {
|
||||
getBalanceHidingSettingsUseCase().flowWithLifecycle(owner.lifecycle).onEach {
|
||||
isBalanceHidden = it.isBalanceHidden
|
||||
withContext(dispatchers.main) {
|
||||
uiState = stateBuilder.updateBalanceHiddenState(uiState, isBalanceHidden)
|
||||
}
|
||||
}
|
||||
.launchIn(viewModelScope)
|
||||
}.launchIn(viewModelScope)
|
||||
}
|
||||
|
||||
override fun onCleared() {
|
||||
|
|
@ -175,19 +172,18 @@ internal class SwapViewModel @Inject constructor(
|
|||
selectedCurrency = null,
|
||||
)
|
||||
|
||||
uiState =
|
||||
stateBuilder.createInitialErrorState(
|
||||
uiState,
|
||||
(it as? ExpressException)?.dataError?.code ?: DataError.UnknownError.code,
|
||||
) {
|
||||
uiState = stateBuilder.createInitialLoadingState(
|
||||
initialCurrency = initialCryptoCurrency,
|
||||
networkInfo = blockchainInteractor.getBlockchainInfo(
|
||||
initialCryptoCurrency.network.backendId,
|
||||
),
|
||||
)
|
||||
initTokens()
|
||||
}
|
||||
uiState = stateBuilder.createInitialErrorState(
|
||||
uiState,
|
||||
(it as? ExpressException)?.dataError?.code ?: DataError.UnknownError.code,
|
||||
) {
|
||||
uiState = stateBuilder.createInitialLoadingState(
|
||||
initialCurrency = initialCryptoCurrency,
|
||||
networkInfo = blockchainInteractor.getBlockchainInfo(
|
||||
initialCryptoCurrency.network.backendId,
|
||||
),
|
||||
)
|
||||
initTokens()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -216,7 +212,7 @@ internal class SwapViewModel @Inject constructor(
|
|||
}
|
||||
|
||||
private fun updateTokensState(dataState: TokensDataStateExpress) {
|
||||
val tokensDataState = if (!isOrderReversed) dataState.toGroup else dataState.fromGroup
|
||||
val tokensDataState = if (isOrderReversed) dataState.fromGroup else dataState.toGroup
|
||||
uiState = stateBuilder.addTokensToState(
|
||||
uiState = uiState,
|
||||
tokensDataState = tokensDataState,
|
||||
|
|
@ -295,14 +291,12 @@ internal class SwapViewModel @Inject constructor(
|
|||
if (providersState.isNotEmpty()) {
|
||||
val (provider, state) = updateLoadedQuotes(providersState)
|
||||
setupLoadedState(provider, state, fromToken)
|
||||
val successStates = providersState
|
||||
.getLastLoadedSuccessStates()
|
||||
val successStates = providersState.getLastLoadedSuccessStates()
|
||||
val pricesLowerBest = getPricesLowerBest(successStates)
|
||||
uiState = stateBuilder.updateProvidersBottomSheetContent(
|
||||
uiState = uiState,
|
||||
pricesLowerBest = pricesLowerBest,
|
||||
tokenSwapInfoForProviders = successStates.entries
|
||||
.associate { it.key.providerId to it.value.toTokenInfo },
|
||||
tokenSwapInfoForProviders = successStates.entries.associate { it.key.providerId to it.value.toTokenInfo },
|
||||
)
|
||||
} else {
|
||||
Timber.e("Accidentally empty quotes list")
|
||||
|
|
@ -329,6 +323,7 @@ internal class SwapViewModel @Inject constructor(
|
|||
bestRatedProviderId = bestRatedProviderId,
|
||||
isManyProviders = dataState.lastLoadedSwapStates.size > 1,
|
||||
selectedFeeType = dataState.selectedFee?.feeType ?: FeeType.NORMAL,
|
||||
isReverseSwapPossible = isReverseSwapPossible()
|
||||
)
|
||||
if (uiState.warnings.any { it is SwapWarning.UnableToCoverFeeWarning }) {
|
||||
analyticsEventHandler.send(
|
||||
|
|
@ -343,6 +338,7 @@ internal class SwapViewModel @Inject constructor(
|
|||
uiState = stateBuilder.createQuotesEmptyAmountState(
|
||||
uiStateHolder = uiState,
|
||||
emptyAmountState = state,
|
||||
isReverseSwapPossible = isReverseSwapPossible(),
|
||||
)
|
||||
}
|
||||
is SwapState.SwapError -> {
|
||||
|
|
@ -353,6 +349,7 @@ internal class SwapViewModel @Inject constructor(
|
|||
fromToken = state.fromTokenInfo,
|
||||
toToken = dataState.toCryptoCurrency,
|
||||
dataError = state.error,
|
||||
isReverseSwapPossible = isReverseSwapPossible(),
|
||||
)
|
||||
sendErrorAnalyticsEvent(state.error, provider)
|
||||
}
|
||||
|
|
@ -419,16 +416,15 @@ internal class SwapViewModel @Inject constructor(
|
|||
}
|
||||
|
||||
private fun selectDefaultFee(state: SwapState.QuotesLoadedState): TxFee? {
|
||||
return dataState.selectedFee
|
||||
?: when (val txFee = state.txFee) {
|
||||
TxFeeState.Empty -> null
|
||||
is TxFeeState.MultipleFeeState -> {
|
||||
txFee.normalFee
|
||||
}
|
||||
is TxFeeState.SingleFeeState -> {
|
||||
txFee.fee
|
||||
}
|
||||
return dataState.selectedFee ?: when (val txFee = state.txFee) {
|
||||
TxFeeState.Empty -> null
|
||||
is TxFeeState.MultipleFeeState -> {
|
||||
txFee.normalFee
|
||||
}
|
||||
is TxFeeState.SingleFeeState -> {
|
||||
txFee.fee
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("LongMethod")
|
||||
|
|
@ -453,8 +449,7 @@ internal class SwapViewModel @Inject constructor(
|
|||
includeFeeInAmount = lastLoadedQuotesState.preparedSwapConfigState.includeFeeInAmount,
|
||||
fee = requireNotNull(dataState.selectedFee),
|
||||
)
|
||||
}
|
||||
.onSuccess {
|
||||
}.onSuccess {
|
||||
when (it) {
|
||||
is TxState.TxSent -> {
|
||||
val url = blockchainInteractor.getExplorerTransactionLink(
|
||||
|
|
@ -499,8 +494,7 @@ internal class SwapViewModel @Inject constructor(
|
|||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.onFailure {
|
||||
}.onFailure {
|
||||
Timber.e(it)
|
||||
startLoadingQuotesFromLastState()
|
||||
makeDefaultAlert()
|
||||
|
|
@ -539,8 +533,7 @@ internal class SwapViewModel @Inject constructor(
|
|||
approveData = requireNotNull(dataState.approveDataModel) {
|
||||
"dataState.approveDataModel might not be null"
|
||||
},
|
||||
forTokenContractAddress = (dataState.fromCryptoCurrency?.currency as? CryptoCurrency.Token)
|
||||
?.contractAddress
|
||||
forTokenContractAddress = (dataState.fromCryptoCurrency?.currency as? CryptoCurrency.Token)?.contractAddress
|
||||
?: "",
|
||||
fromToken = requireNotNull(dataState.fromCryptoCurrency?.currency) {
|
||||
"dataState.fromCurrency might not be null"
|
||||
|
|
@ -554,8 +547,7 @@ internal class SwapViewModel @Inject constructor(
|
|||
},
|
||||
),
|
||||
)
|
||||
}
|
||||
.onSuccess {
|
||||
}.onSuccess {
|
||||
when (it) {
|
||||
is TxState.TxSent -> {
|
||||
uiState = stateBuilder.loadingPermissionState(uiState)
|
||||
|
|
@ -569,8 +561,7 @@ internal class SwapViewModel @Inject constructor(
|
|||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.onFailure {
|
||||
}.onFailure {
|
||||
makeDefaultAlert()
|
||||
}
|
||||
}
|
||||
|
|
@ -789,8 +780,8 @@ internal class SwapViewModel @Inject constructor(
|
|||
},
|
||||
onClickFee = {
|
||||
val selectedFee = dataState.selectedFee?.feeType ?: FeeType.NORMAL
|
||||
val txFeeState = dataState.getCurrentLoadedSwapState()?.txFee as? TxFeeState.MultipleFeeState
|
||||
?: return@UiActions
|
||||
val txFeeState =
|
||||
dataState.getCurrentLoadedSwapState()?.txFee as? TxFeeState.MultipleFeeState ?: return@UiActions
|
||||
uiState = stateBuilder.showSelectFeeBottomSheet(
|
||||
uiState = uiState,
|
||||
selectedFee = selectedFee,
|
||||
|
|
@ -868,9 +859,7 @@ internal class SwapViewModel @Inject constructor(
|
|||
private fun findBestQuoteProvider(state: SuccessLoadedSwapData): SwapProvider? {
|
||||
// finding best quotes
|
||||
return state.minByOrNull {
|
||||
if (!it.value.fromTokenInfo.amountFiat.isNullOrZero() &&
|
||||
!it.value.toTokenInfo.amountFiat.isNullOrZero()
|
||||
) {
|
||||
if (!it.value.fromTokenInfo.amountFiat.isNullOrZero() && !it.value.toTokenInfo.amountFiat.isNullOrZero()) {
|
||||
it.value.fromTokenInfo.amountFiat.divide(
|
||||
it.value.toTokenInfo.amountFiat,
|
||||
it.value.toTokenInfo.cryptoCurrencyStatus.currency.decimals,
|
||||
|
|
@ -900,11 +889,9 @@ internal class SwapViewModel @Inject constructor(
|
|||
}
|
||||
|
||||
private fun createSelectedAppCurrencyFlow(): StateFlow<AppCurrency> {
|
||||
return getSelectedAppCurrencyUseCase()
|
||||
.map { maybeAppCurrency ->
|
||||
return getSelectedAppCurrencyUseCase().map { maybeAppCurrency ->
|
||||
maybeAppCurrency.getOrElse { AppCurrency.Default }
|
||||
}
|
||||
.stateIn(
|
||||
}.stateIn(
|
||||
scope = viewModelScope,
|
||||
started = SharingStarted.Eagerly,
|
||||
initialValue = AppCurrency.Default,
|
||||
|
|
@ -937,11 +924,27 @@ internal class SwapViewModel @Inject constructor(
|
|||
}
|
||||
|
||||
private fun Map<SwapProvider, SwapState>.getLastLoadedSuccessStates(): SuccessLoadedSwapData {
|
||||
return this
|
||||
.filter { it.value is SwapState.QuotesLoadedState }
|
||||
return this.filter { it.value is SwapState.QuotesLoadedState }
|
||||
.mapValues { it.value as SwapState.QuotesLoadedState }
|
||||
}
|
||||
|
||||
private fun isReverseSwapPossible(): Boolean {
|
||||
val from = dataState.fromCryptoCurrency ?: return false
|
||||
val to = dataState.toCryptoCurrency ?: return false
|
||||
|
||||
val currenciesGroup = if (isOrderReversed) {
|
||||
dataState.tokensDataState?.toGroup
|
||||
} else {
|
||||
dataState.tokensDataState?.fromGroup
|
||||
} ?: return false
|
||||
|
||||
val chosen = if (isOrderReversed) from else to
|
||||
|
||||
return currenciesGroup.available
|
||||
.map { it.currencyStatus.currency }
|
||||
.contains(chosen.currency)
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val loggingTag = "SwapViewModel"
|
||||
private const val INITIAL_AMOUNT = ""
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue