Updated on 2026-08-14

This commit is contained in:
Tangem 2025-02-04 19:30:38 +05:00
commit c86f915e7d
28 changed files with 274 additions and 66 deletions

View file

@ -1,6 +1,7 @@
package com.tangem.feature.tokendetails.presentation.tokendetails.state.components
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.extensions.wrappedList
import com.tangem.features.tokendetails.impl.R
@ -133,5 +134,22 @@ internal data class TokenDetailsDialogConfig(
onClick = onConfirmClick,
)
}
data class ConfirmExpressStatusHideDialogConfig(
val onConfirmClick: () -> Unit,
val onCancelClick: () -> Unit,
) : DialogContentConfig() {
override val title: TextReference = resourceReference(R.string.express_status_hide_dialog_title)
override val message: TextReference = resourceReference(R.string.express_status_hide_dialog_text)
override val confirmButtonConfig: ButtonConfig = ButtonConfig(
text = resourceReference(R.string.common_hide),
onClick = onConfirmClick,
)
override val cancelButtonConfig: ButtonConfig = ButtonConfig(
text = resourceReference(R.string.common_cancel),
onClick = onCancelClick,
)
}
}
}

View file

@ -15,7 +15,6 @@ internal data class ExchangeUM(
val statuses: ImmutableList<ExchangeStatusState>,
val notification: ExchangeStatusNotifications? = null,
val showProviderLink: Boolean,
val isRefundTerminalStatus: Boolean,
val fromCryptoCurrency: CryptoCurrency,
val toCryptoCurrency: CryptoCurrency,
) : ExpressTransactionStateUM

View file

@ -94,6 +94,7 @@ internal class TokenDetailsOnrampTransactionStateConverter(
clickIntents.onGoToProviderClick(it)
},
onClick = { clickIntents.onExpressTransactionClick(value.txId) },
onDisposeExpressStatus = clickIntents::onConfirmDisposeExpressStatus,
),
providerName = value.providerName,
providerImageUrl = value.providerImageUrl,

View file

@ -340,6 +340,22 @@ internal class TokenDetailsStateFactory(
return balanceSelectStateConverter.convert(buttonConfig)
}
fun getStateWithConfirmHideExpressStatus(): TokenDetailsState {
return currentStateProvider().copy(
dialogConfig = TokenDetailsDialogConfig(
isShow = true,
onDismissRequest = clickIntents::onDismissDialog,
content = TokenDetailsDialogConfig.DialogContentConfig.ConfirmExpressStatusHideDialogConfig(
onConfirmClick = {
clickIntents.onDisposeExpressStatus()
clickIntents.onDismissDialog()
},
onCancelClick = clickIntents::onDismissDialog,
),
),
)
}
private fun TokenDetailsAppBarMenuConfig.updateMenu(
cardTypesResolver: CardTypesResolver,
hasDerivations: Boolean,

View file

@ -80,17 +80,20 @@ internal class TokenDetailsSwapTransactionsStateConverter(
fromFiatAmount = quote.fiatRate.multiply(fromAmount)
}
}
val notifications =
getNotification(transaction.status?.status, transaction.status?.txExternalUrl, null)
val statusModel = transaction.status
val notifications = getNotification(
status = statusModel?.status,
txUrl = statusModel?.txExternalUrl,
refundToken = statusModel?.refundCurrency,
)
val showProviderLink = getShowProviderLink(notifications, transaction.status)
result.add(
ExchangeUM(
provider = transaction.provider,
statuses = getStatuses(transaction.status?.status),
statuses = getStatuses(statusModel?.status),
notification = notifications,
activeStatus = transaction.status?.status,
activeStatus = statusModel?.status,
showProviderLink = showProviderLink,
isRefundTerminalStatus = true,
fromCryptoCurrency = fromCryptoCurrency,
toCryptoCurrency = toCryptoCurrency,
info = createStateInfo(
@ -107,25 +110,19 @@ internal class TokenDetailsSwapTransactionsStateConverter(
return result.toPersistentList()
}
fun updateTxStatus(
tx: ExchangeUM,
statusModel: ExchangeStatusModel?,
refundToken: CryptoCurrency?,
isRefundTerminalStatus: Boolean,
): ExchangeUM {
fun updateTxStatus(tx: ExchangeUM, statusModel: ExchangeStatusModel?): ExchangeUM {
if (statusModel == null || tx.activeStatus == statusModel.status) {
Timber.e("UpdateTxStatus isn't required. Current status isn't changed")
return tx
}
val hasFailed = statusModel.status == ExchangeStatus.Failed
val notifications = getNotification(statusModel.status, statusModel.txExternalUrl, refundToken)
val notifications = getNotification(statusModel.status, statusModel.txExternalUrl, statusModel.refundCurrency)
val showProviderLink = getShowProviderLink(notifications, statusModel)
return tx.copy(
activeStatus = statusModel.status,
notification = notifications,
statuses = getStatuses(statusModel.status, hasFailed),
showProviderLink = showProviderLink,
isRefundTerminalStatus = isRefundTerminalStatus,
info = tx.info.copy(txExternalUrl = statusModel.txExternalUrl),
)
}
@ -164,7 +161,8 @@ internal class TokenDetailsSwapTransactionsStateConverter(
},
iconState = getIconState(transaction.status?.status),
status = getStatusState(),
notification = null, // fixme [REDACTED_JIRA]
notification = null, // fixme [REDACTED_JIRA],
onDisposeExpressStatus = clickIntents::onConfirmDisposeExpressStatus,
)
}

View file

@ -79,13 +79,13 @@ internal class ExchangeStatusFactory @AssistedInject constructor(
}
}
suspend fun removeTransactionOnBottomSheetClosed(isForceTerminal: Boolean = false) {
suspend fun removeTransactionOnBottomSheetClosed(isForceDispose: Boolean = false) {
val state = currentStateProvider()
val bottomSheetConfig = state.bottomSheetConfig?.content as? ExpressStatusBottomSheetConfig ?: return
val selectedTx = bottomSheetConfig.value as? ExchangeUM ?: return
val shouldTerminate = selectedTx.activeStatus.isTerminal(selectedTx.isRefundTerminalStatus) || isForceTerminal
if (shouldTerminate) {
val shouldDispose = selectedTx.activeStatus?.isAutoDisposable == true || isForceDispose
if (shouldDispose) {
swapTransactionRepository.removeTransaction(
userWalletId = userWalletId,
fromCryptoCurrency = selectedTx.fromCryptoCurrency,
@ -96,20 +96,14 @@ internal class ExchangeStatusFactory @AssistedInject constructor(
}
suspend fun updateSwapTxStatus(swapTx: ExchangeUM): ExchangeUM {
return if (swapTx.activeStatus.isTerminal(swapTx.isRefundTerminalStatus)) {
return if (swapTx.activeStatus?.isTerminal == true) {
swapTx
} else {
val statusModel = getExchangeStatus(swapTx.info.txId, swapTx.provider)
val isRefundTerminalStatus = statusModel?.refundNetwork == null &&
statusModel?.refundContractAddress == null &&
swapTx.provider.type != ExchangeProviderType.DEX_BRIDGE
val addedRefundToken = addRefundCurrencyIfNeeded(statusModel, swapTx.provider.type)
swapTransactionsStateConverter.updateTxStatus(
tx = swapTx,
statusModel = statusModel,
refundToken = addedRefundToken,
isRefundTerminalStatus = isRefundTerminalStatus,
)
}
}
@ -120,8 +114,11 @@ internal class ExchangeStatusFactory @AssistedInject constructor(
ifLeft = { null },
ifRight = { statusModel ->
sendStatusUpdateAnalytics(statusModel, provider)
swapTransactionRepository.storeTransactionState(txId, statusModel)
statusModel
val refundTokenCurrency = addRefundCurrencyIfNeeded(statusModel, provider.type)
swapTransactionRepository.storeTransactionState(txId, statusModel, refundTokenCurrency)
statusModel.copy(refundCurrency = refundTokenCurrency)
},
)
}
@ -174,15 +171,6 @@ internal class ExchangeStatusFactory @AssistedInject constructor(
)
}
private fun ExchangeStatus?.isTerminal(isRefundTerminal: Boolean): Boolean {
val needTerminalRefund = this == ExchangeStatus.Refunded && isRefundTerminal
return needTerminalRefund ||
this == ExchangeStatus.Finished ||
this == ExchangeStatus.Cancelled ||
this == ExchangeStatus.TxFailed ||
this == ExchangeStatus.Unknown
}
private fun toAnalyticStatus(status: ExchangeStatus?): ExpressAnalyticsStatus? {
return when (status) {
ExchangeStatus.New,

View file

@ -157,11 +157,13 @@ internal class ExpressStatusFactory @AssistedInject constructor(
suspend fun removeTransactionOnBottomSheetClosed(
expressState: ExpressTransactionStateUM,
isForceTerminal: Boolean = false,
isForceDispose: Boolean = false,
) {
when (expressState) {
is ExchangeUM -> exchangeStatusFactory.removeTransactionOnBottomSheetClosed(isForceTerminal)
is ExpressTransactionStateUM.OnrampUM -> onrampStatusFactory.removeTransactionOnBottomSheetClosed()
is ExchangeUM -> exchangeStatusFactory.removeTransactionOnBottomSheetClosed(isForceDispose)
is ExpressTransactionStateUM.OnrampUM -> onrampStatusFactory.removeTransactionOnBottomSheetClosed(
isForceDispose,
)
}
}

View file

@ -68,12 +68,12 @@ internal class OnrampStatusFactory @AssistedInject constructor(
}
}
suspend fun removeTransactionOnBottomSheetClosed() {
suspend fun removeTransactionOnBottomSheetClosed(isForceDispose: Boolean) {
val state = currentStateProvider()
val bottomSheetConfig = state.bottomSheetConfig?.content as? ExpressStatusBottomSheetConfig ?: return
val selectedTx = bottomSheetConfig.value as? ExpressTransactionStateUM.OnrampUM ?: return
if (selectedTx.activeStatus.isTerminal) {
if (selectedTx.activeStatus.isAutoDisposable || isForceDispose) {
onrampRemoveTransactionUseCase(externalTxId = selectedTx.info.txExternalId)
}
}

View file

@ -9,6 +9,7 @@ import androidx.compose.ui.Alignment.Companion.CenterHorizontally
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.style.TextAlign
import com.tangem.common.ui.expressStatus.ExpressEstimate
import com.tangem.common.ui.expressStatus.ExpressHideButton
import com.tangem.common.ui.expressStatus.ExpressProvider
import com.tangem.core.ui.R
import com.tangem.core.ui.components.SpacerH10
@ -70,6 +71,11 @@ internal fun ExchangeStatusBottomSheetContent(state: ExchangeUM) {
if (state.notification != null) {
Notification(state = state.notification, activeStatus = state.activeStatus)
}
ExpressHideButton(
isTerminal = state.activeStatus?.isTerminal == true,
isAutoDisposable = state.activeStatus?.isAutoDisposable == true,
onClick = state.info.onDisposeExpressStatus,
)
SpacerH24()
}
}

View file

@ -72,4 +72,8 @@ interface TokenDetailsClickIntents {
fun onGoToRefundedTokenClick(cryptoCurrency: CryptoCurrency)
fun onOpenUrlClick(url: String)
fun onConfirmDisposeExpressStatus()
fun onDisposeExpressStatus()
}

View file

@ -784,16 +784,6 @@ internal class TokenDetailsViewModel @Inject constructor(
}
override fun onGoToRefundedTokenClick(cryptoCurrency: CryptoCurrency) {
val bottomSheetState = internalUiState.value.bottomSheetConfig?.content
if (bottomSheetState is ExpressStatusBottomSheetConfig) {
viewModelScope.launch {
expressStatusFactory.removeTransactionOnBottomSheetClosed(
expressState = bottomSheetState.value,
isForceTerminal = true,
)
}
}
internalUiState.value = stateFactory.getStateWithClosedBottomSheet()
router.openTokenDetails(userWalletId, cryptoCurrency)
}
@ -950,6 +940,23 @@ internal class TokenDetailsViewModel @Inject constructor(
internalUiState.value = stateFactory.getStateWithUpdatedBalanceSegmentedButtonConfig(config)
}
override fun onConfirmDisposeExpressStatus() {
internalUiState.value = stateFactory.getStateWithConfirmHideExpressStatus()
}
override fun onDisposeExpressStatus() {
val bottomSheetState = internalUiState.value.bottomSheetConfig?.content
if (bottomSheetState is ExpressStatusBottomSheetConfig) {
viewModelScope.launch {
expressStatusFactory.removeTransactionOnBottomSheetClosed(
expressState = bottomSheetState.value,
isForceDispose = true,
)
}
}
internalUiState.value = stateFactory.getStateWithClosedBottomSheet()
}
private fun handleUnavailabilityReason(
unavailabilityReason: ScenarioUnavailabilityReason,
tokenSymbol: String,