Updated on 2026-08-14
This commit is contained in:
parent
82c19782eb
commit
f3aa08093d
21 changed files with 657 additions and 129 deletions
|
|
@ -201,7 +201,7 @@
|
|||
<string name="express_exchange_notification_failed_text">Visit provider’s website to see why</string>
|
||||
<string name="express_exchange_notification_failed_title">Operation failed by provider</string>
|
||||
<string name="express_exchange_notification_verification_text">Visit provider’s website for verification</string>
|
||||
<string name="express_exchange_notification_verification_title">Provider: Verification needed</string>
|
||||
<string name="express_exchange_notification_verification_title">KYC verification required by provider</string>
|
||||
<string name="express_exchange_status_confirmed">Confirmed</string>
|
||||
<string name="express_exchange_status_confirming">Confirming</string>
|
||||
<string name="express_exchange_status_exchanged">Exchanged</string>
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ import androidx.compose.ui.res.painterResource
|
|||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import com.tangem.core.ui.R
|
||||
import com.tangem.core.ui.components.atoms.text.EllipsisText
|
||||
import com.tangem.core.ui.components.atoms.text.TextEllipsis
|
||||
import com.tangem.core.ui.components.currency.tokenicon.TokenIcon
|
||||
import com.tangem.core.ui.components.currency.tokenicon.TokenIconState
|
||||
import com.tangem.core.ui.components.inputrow.inner.DividerContainer
|
||||
|
|
@ -39,6 +40,8 @@ fun InputRowApprox(
|
|||
rightTitle: TextReference,
|
||||
rightSubtitle: TextReference,
|
||||
modifier: Modifier = Modifier,
|
||||
leftTitleEllipsisOffset: Int = 0,
|
||||
rightTitleEllipsisOffset: Int = 0,
|
||||
showDivider: Boolean = false,
|
||||
) {
|
||||
DividerContainer(
|
||||
|
|
@ -47,7 +50,6 @@ fun InputRowApprox(
|
|||
) {
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
modifier = Modifier
|
||||
.padding(TangemTheme.dimens.spacing12)
|
||||
.fillMaxWidth(),
|
||||
|
|
@ -56,6 +58,7 @@ fun InputRowApprox(
|
|||
iconState = leftIcon,
|
||||
title = leftTitle,
|
||||
subtitle = leftSubtitle,
|
||||
titleEllipsisOffset = leftTitleEllipsisOffset,
|
||||
modifier = Modifier.weight(1f),
|
||||
)
|
||||
Icon(
|
||||
|
|
@ -64,7 +67,7 @@ fun InputRowApprox(
|
|||
tint = TangemTheme.colors.text.tertiary,
|
||||
modifier = Modifier
|
||||
.padding(
|
||||
horizontal = TangemTheme.dimens.spacing8,
|
||||
horizontal = TangemTheme.dimens.spacing4,
|
||||
vertical = TangemTheme.dimens.spacing10,
|
||||
),
|
||||
)
|
||||
|
|
@ -72,6 +75,7 @@ fun InputRowApprox(
|
|||
iconState = rightIcon,
|
||||
title = rightTitle,
|
||||
subtitle = rightSubtitle,
|
||||
titleEllipsisOffset = rightTitleEllipsisOffset,
|
||||
modifier = Modifier.weight(1f),
|
||||
)
|
||||
}
|
||||
|
|
@ -84,6 +88,7 @@ private fun InputRowApproxItem(
|
|||
title: TextReference,
|
||||
subtitle: TextReference,
|
||||
modifier: Modifier = Modifier,
|
||||
titleEllipsisOffset: Int = 0,
|
||||
) {
|
||||
Row(
|
||||
modifier = modifier,
|
||||
|
|
@ -103,6 +108,7 @@ private fun InputRowApproxItem(
|
|||
text = title.resolveReference(),
|
||||
style = TangemTheme.typography.body2,
|
||||
color = TangemTheme.colors.text.primary1,
|
||||
ellipsis = TextEllipsis.OffsetEnd(titleEllipsisOffset),
|
||||
)
|
||||
EllipsisText(
|
||||
text = subtitle.resolveReference(),
|
||||
|
|
@ -122,11 +128,13 @@ private fun InputRowApproxPreview_Light() {
|
|||
TangemTheme {
|
||||
InputRowApprox(
|
||||
leftIcon = TokenIconState.Loading,
|
||||
leftTitle = TextReference.Str("Left title"),
|
||||
leftSubtitle = TextReference.Str("Left subtitle"),
|
||||
leftTitle = TextReference.Str("Left title USD"),
|
||||
leftSubtitle = TextReference.Str("Left subtitle USD"),
|
||||
leftTitleEllipsisOffset = 3,
|
||||
rightIcon = TokenIconState.Loading,
|
||||
rightTitle = TextReference.Str("Right title Right title Right title Right title Right title"),
|
||||
rightSubtitle = TextReference.Str("Right subtitle"),
|
||||
rightTitle = TextReference.Str("Right title Right title Right title Right title Right title USD"),
|
||||
rightSubtitle = TextReference.Str("Right subtitle Right subtitle Right subtitle USD"),
|
||||
rightTitleEllipsisOffset = 3,
|
||||
modifier = Modifier
|
||||
.background(TangemTheme.colors.background.action),
|
||||
)
|
||||
|
|
@ -139,11 +147,13 @@ private fun InputRowApproxPreview_Dark() {
|
|||
TangemTheme(isDark = true) {
|
||||
InputRowApprox(
|
||||
leftIcon = TokenIconState.Loading,
|
||||
leftTitle = TextReference.Str("Left title Left title Left title Left title Left title"),
|
||||
leftSubtitle = TextReference.Str("Left subtitle"),
|
||||
leftTitle = TextReference.Str("Left title Left title Left title Left title Left title USD"),
|
||||
leftSubtitle = TextReference.Str("Left subtitle Left subtitle Left subtitle USD"),
|
||||
leftTitleEllipsisOffset = 3,
|
||||
rightIcon = TokenIconState.Loading,
|
||||
rightTitle = TextReference.Str("Right title"),
|
||||
rightSubtitle = TextReference.Str("Right subtitle"),
|
||||
rightTitle = TextReference.Str("Right title USD"),
|
||||
rightSubtitle = TextReference.Str("Right subtitle USD"),
|
||||
rightTitleEllipsisOffset = 3,
|
||||
modifier = Modifier
|
||||
.background(TangemTheme.colors.background.action),
|
||||
)
|
||||
|
|
|
|||
|
|
@ -104,6 +104,16 @@ fun combinedReference(refs: WrappedList<TextReference>): TextReference {
|
|||
return TextReference.Combined(refs)
|
||||
}
|
||||
|
||||
/**
|
||||
* Combines multiple [TextReference] instances into a single [TextReference].
|
||||
*
|
||||
* @param refs Vararg of [TextReference] instances to be combined.
|
||||
* @return A [TextReference] representing the combined text references.
|
||||
*/
|
||||
fun combinedReference(vararg refs: TextReference): TextReference {
|
||||
return TextReference.Combined(WrappedList(listOf(*refs)))
|
||||
}
|
||||
|
||||
/** Resolve [TextReference] as [String] */
|
||||
@Composable
|
||||
@ReadOnlyComposable
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
package com.tangem.feature.swap.viewmodels
|
||||
package com.tangem.utils.coroutines
|
||||
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.delay
|
||||
|
|
@ -43,6 +43,7 @@ internal class SwapInteractorImpl @Inject constructor(
|
|||
private val transactionManager: TransactionManager,
|
||||
private val userWalletManager: UserWalletManager,
|
||||
private val repository: SwapRepository,
|
||||
private val swapTransactionRepository: SwapTransactionRepository,
|
||||
private val allowPermissionsHandler: AllowPermissionsHandler,
|
||||
private val getSelectedWalletSyncUseCase: GetSelectedWalletSyncUseCase,
|
||||
private val getMultiCryptoCurrencyStatusUseCase: GetCryptoCurrencyStatusesSyncUseCase,
|
||||
|
|
@ -492,6 +493,14 @@ internal class SwapInteractorImpl @Inject constructor(
|
|||
},
|
||||
ifRight = {
|
||||
val timestamp = System.currentTimeMillis()
|
||||
storeSwapTransaction(
|
||||
currencyToSend = currencyToSend,
|
||||
currencyToGet = currencyToGet,
|
||||
amount = amount,
|
||||
swapProvider = swapProvider,
|
||||
swapDataModel = exchangeData.dataModel,
|
||||
timestamp = timestamp,
|
||||
)
|
||||
TxState.TxSent(
|
||||
fromAmount = amountFormatter.formatSwapAmountToUI(
|
||||
amount,
|
||||
|
|
@ -531,6 +540,28 @@ internal class SwapInteractorImpl @Inject constructor(
|
|||
}
|
||||
}
|
||||
|
||||
private suspend fun storeSwapTransaction(
|
||||
currencyToSend: CryptoCurrencyStatus,
|
||||
currencyToGet: CryptoCurrencyStatus,
|
||||
amount: SwapAmount,
|
||||
swapProvider: SwapProvider,
|
||||
swapDataModel: SwapDataModel,
|
||||
timestamp: Long,
|
||||
) {
|
||||
swapTransactionRepository.storeTransaction(
|
||||
userWalletId = UserWalletId(userWalletManager.getWalletId()),
|
||||
fromCryptoCurrencyId = currencyToSend.currency.id,
|
||||
toCryptoCurrencyId = currencyToGet.currency.id,
|
||||
transaction = SavedSwapTransactionModel(
|
||||
txId = swapDataModel.transaction.txId,
|
||||
provider = swapProvider,
|
||||
timestamp = timestamp,
|
||||
fromCryptoAmount = amount.value,
|
||||
toCryptoAmount = swapDataModel.toTokenAmount.value,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
@Deprecated("used in old swap mechanism")
|
||||
override fun getTokenBalance(token: CryptoCurrencyStatus): SwapAmount {
|
||||
return SwapAmount(token.value.amount ?: BigDecimal.ZERO, getTokenDecimals(token.currency))
|
||||
|
|
|
|||
|
|
@ -34,9 +34,10 @@ class SwapDomainModule {
|
|||
userWalletManager: UserWalletManager,
|
||||
transactionManager: TransactionManager,
|
||||
@SwapScope getSelectedWalletSyncUseCase: GetSelectedWalletSyncUseCase,
|
||||
@SwapScope getCryptoCurrencyStatusUseCase: GetCryptoCurrencyStatusesSyncUseCase,
|
||||
getCryptoCurrencyStatusUseCase: GetCryptoCurrencyStatusesSyncUseCase,
|
||||
@SwapScope sendTransactionUseCase: SendTransactionUseCase,
|
||||
quotesRepository: QuotesRepository,
|
||||
swapTransactionRepository: SwapTransactionRepository,
|
||||
walletManagersFacade: WalletManagersFacade,
|
||||
coroutineDispatcherProvider: CoroutineDispatcherProvider,
|
||||
): SwapInteractor {
|
||||
|
|
@ -51,6 +52,7 @@ class SwapDomainModule {
|
|||
quotesRepository = quotesRepository,
|
||||
walletManagersFacade = walletManagersFacade,
|
||||
dispatcher = coroutineDispatcherProvider,
|
||||
swapTransactionRepository = swapTransactionRepository,
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -69,7 +71,6 @@ class SwapDomainModule {
|
|||
return GetSelectedWalletSyncUseCase(walletsStateHolder = walletsStateHolder)
|
||||
}
|
||||
|
||||
@SwapScope
|
||||
@Provides
|
||||
@Singleton
|
||||
fun providesGetCryptoCurrencyStatusUseCase(
|
||||
|
|
|
|||
|
|
@ -1,5 +1,7 @@
|
|||
package com.tangem.feature.swap.domain.models.domain
|
||||
|
||||
import java.math.BigDecimal
|
||||
|
||||
data class SavedSwapTransactionListModel(
|
||||
val userWalletId: String,
|
||||
val fromCryptoCurrencyId: String,
|
||||
|
|
@ -10,10 +12,8 @@ data class SavedSwapTransactionListModel(
|
|||
data class SavedSwapTransactionModel(
|
||||
val txId: String,
|
||||
val timestamp: Long,
|
||||
val fromCryptoAmount: String,
|
||||
val toCryptoAmount: String,
|
||||
val fromCryptoAmount: BigDecimal,
|
||||
val toCryptoAmount: BigDecimal,
|
||||
val provider: SwapProvider,
|
||||
val toFiatAmount: String? = null,
|
||||
val fromFiatAmount: String? = null,
|
||||
val status: ExchangeStatusModel? = null,
|
||||
)
|
||||
|
|
@ -8,6 +8,7 @@ data class SwapSuccessStateHolder(
|
|||
val txUrl: String,
|
||||
val fee: TextReference,
|
||||
val rate: TextReference,
|
||||
val showStatusButton: Boolean,
|
||||
val providerName: TextReference,
|
||||
val providerType: TextReference,
|
||||
val providerIcon: String,
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ import com.tangem.domain.tokens.model.CryptoCurrencyStatus
|
|||
import com.tangem.feature.swap.converters.TokensDataConverter
|
||||
import com.tangem.feature.swap.domain.models.DataError
|
||||
import com.tangem.feature.swap.domain.models.SwapAmount
|
||||
import com.tangem.feature.swap.domain.models.domain.ExchangeProviderType
|
||||
import com.tangem.feature.swap.domain.models.domain.NetworkInfo
|
||||
import com.tangem.feature.swap.domain.models.domain.SwapProvider
|
||||
import com.tangem.feature.swap.domain.models.formatToUIRepresentation
|
||||
|
|
@ -592,6 +593,7 @@ internal class StateBuilder(
|
|||
txUrl = txUrl,
|
||||
providerName = TextReference.Str(providerState.name),
|
||||
providerType = TextReference.Str(providerState.type),
|
||||
showStatusButton = providerState.type == ExchangeProviderType.CEX.name,
|
||||
providerIcon = providerState.iconUrl,
|
||||
fee = TextReference.Str("${fee.amountCrypto} ${fee.symbolCrypto} (${fee.amountFiatFormatted})"),
|
||||
rate = TextReference.Str(providerState.rate),
|
||||
|
|
|
|||
|
|
@ -43,6 +43,7 @@ fun SwapSuccessScreen(state: SwapSuccessStateHolder, onBack: () -> Unit) {
|
|||
SwapSuccessScreenButtons(
|
||||
textRes = R.string.common_close,
|
||||
txUrl = state.txUrl,
|
||||
showStatusButton = state.showStatusButton,
|
||||
onExploreClick = state.onSecondaryButtonClick,
|
||||
onDoneClick = onBack,
|
||||
)
|
||||
|
|
@ -105,6 +106,7 @@ private fun SwapSuccessScreenContent(state: SwapSuccessStateHolder, padding: Pad
|
|||
private fun SwapSuccessScreenButtons(
|
||||
@StringRes textRes: Int,
|
||||
txUrl: String,
|
||||
showStatusButton: Boolean,
|
||||
onExploreClick: () -> Unit,
|
||||
onDoneClick: () -> Unit,
|
||||
) {
|
||||
|
|
@ -119,21 +121,23 @@ private fun SwapSuccessScreenButtons(
|
|||
if (txUrl.isNotBlank()) {
|
||||
Row {
|
||||
SecondaryButtonIconStart(
|
||||
text = stringResource(id = com.tangem.core.ui.R.string.common_explore),
|
||||
iconResId = com.tangem.core.ui.R.drawable.ic_web_24,
|
||||
text = stringResource(id = R.string.common_explore),
|
||||
iconResId = R.drawable.ic_web_24,
|
||||
onClick = onExploreClick,
|
||||
modifier = Modifier.weight(1f),
|
||||
)
|
||||
SpacerW12()
|
||||
SecondaryButtonIconStart(
|
||||
text = stringResource(id = com.tangem.core.ui.R.string.common_share),
|
||||
iconResId = com.tangem.core.ui.R.drawable.ic_share_24,
|
||||
onClick = {
|
||||
hapticFeedback.performHapticFeedback(HapticFeedbackType.LongPress)
|
||||
context.shareText(txUrl)
|
||||
},
|
||||
modifier = Modifier.weight(1f),
|
||||
)
|
||||
if (showStatusButton) {
|
||||
SpacerW12()
|
||||
SecondaryButtonIconStart(
|
||||
text = stringResource(id = R.string.express_cex_status_button_title),
|
||||
iconResId = R.drawable.ic_arrow_top_right_24,
|
||||
onClick = {
|
||||
hapticFeedback.performHapticFeedback(HapticFeedbackType.LongPress)
|
||||
context.shareText(txUrl)
|
||||
},
|
||||
modifier = Modifier.weight(1f),
|
||||
)
|
||||
}
|
||||
}
|
||||
SpacerH12()
|
||||
}
|
||||
|
|
@ -154,6 +158,7 @@ private val state = SwapSuccessStateHolder(
|
|||
fee = TextReference.Str("1 000 DAI ~ 1 000 MATIC"),
|
||||
providerName = TextReference.Str("1inch"),
|
||||
providerType = TextReference.Str(ExchangeProviderType.DEX.name),
|
||||
showStatusButton = false,
|
||||
providerIcon = "",
|
||||
fromTokenAmount = TextReference.Str("1 000 DAI"),
|
||||
toTokenAmount = TextReference.Str("1 000 MATIC"),
|
||||
|
|
|
|||
|
|
@ -37,6 +37,7 @@ dependencies {
|
|||
implementation(deps.tangem.blockchain)
|
||||
implementation(deps.tangem.card.core)
|
||||
implementation(deps.timber)
|
||||
implementation(deps.lifecycle.compose)
|
||||
|
||||
/** DI */
|
||||
implementation(deps.hilt.android)
|
||||
|
|
@ -66,6 +67,9 @@ dependencies {
|
|||
implementation(projects.domain.balanceHiding)
|
||||
implementation(projects.domain.balanceHiding.models)
|
||||
|
||||
/** Temp dependency to swap domain */
|
||||
implementation(projects.features.swap.domain)
|
||||
|
||||
/** Feature Apis */
|
||||
implementation(projects.features.tokendetails.api)
|
||||
implementation(projects.features.send.api)
|
||||
|
|
|
|||
|
|
@ -1,24 +1,28 @@
|
|||
package com.tangem.feature.tokendetails.presentation.tokendetails.state
|
||||
|
||||
import com.tangem.core.ui.components.currency.tokenicon.TokenIconState
|
||||
import com.tangem.core.ui.extensions.TextReference
|
||||
import com.tangem.domain.tokens.model.CryptoCurrency
|
||||
import com.tangem.feature.swap.domain.models.domain.ExchangeStatus
|
||||
import com.tangem.feature.swap.domain.models.domain.SwapProvider
|
||||
import com.tangem.feature.tokendetails.presentation.tokendetails.state.components.ExchangeStatusNotifications
|
||||
import kotlinx.collections.immutable.PersistentList
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
|
||||
internal data class SwapTransactionsState(
|
||||
val txId: String,
|
||||
val provider: SwapProvider,
|
||||
val txUrl: String? = null,
|
||||
val timestamp: Long,
|
||||
val statuses: PersistentList<ExchangeStatusState>,
|
||||
val activeStatus: ExchangeStatus?,
|
||||
val timestamp: TextReference,
|
||||
val fiatSymbol: String,
|
||||
val notification: ExchangeStatusNotifications? = null,
|
||||
val activeStatus: MutableStateFlow<ExchangeStatus?>,
|
||||
val statuses: MutableStateFlow<List<ExchangeStatusState>>,
|
||||
val notification: MutableStateFlow<ExchangeStatusNotifications?> = MutableStateFlow(null),
|
||||
val toCryptoCurrencyId: CryptoCurrency.ID,
|
||||
val toCryptoAmount: String,
|
||||
val toCryptoSymbol: String,
|
||||
val toFiatAmount: String,
|
||||
val toCurrencyIcon: TokenIconState,
|
||||
val fromCryptoCurrencyId: CryptoCurrency.ID,
|
||||
val fromCryptoAmount: String,
|
||||
val fromCryptoSymbol: String,
|
||||
val fromFiatAmount: String,
|
||||
|
|
@ -29,6 +33,7 @@ internal data class SwapTransactionsState(
|
|||
|
||||
internal class ExchangeStatusState(
|
||||
val status: ExchangeStatus,
|
||||
val text: TextReference,
|
||||
val isActive: Boolean,
|
||||
val isDone: Boolean,
|
||||
)
|
||||
|
|
@ -27,6 +27,7 @@ import com.tangem.feature.tokendetails.presentation.tokendetails.state.component
|
|||
import com.tangem.feature.tokendetails.presentation.tokendetails.state.factory.txhistory.TokenDetailsLoadedTxHistoryConverter
|
||||
import com.tangem.feature.tokendetails.presentation.tokendetails.state.factory.txhistory.TokenDetailsLoadingTxHistoryConverter
|
||||
import com.tangem.feature.tokendetails.presentation.tokendetails.state.factory.txhistory.TokenDetailsLoadingTxHistoryConverter.TokenDetailsLoadingTxHistoryModel
|
||||
import com.tangem.feature.tokendetails.presentation.tokendetails.ui.components.exchange.ExchangeStatusBottomSheetConfig
|
||||
import com.tangem.feature.tokendetails.presentation.tokendetails.viewmodels.TokenDetailsClickIntents
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
|
@ -242,6 +243,19 @@ internal class TokenDetailsStateFactory(
|
|||
return state.copy(notifications = notificationConverter.removeRentInfo(state))
|
||||
}
|
||||
|
||||
fun getStateWithExchangeStatusBottomSheet(txId: String): TokenDetailsState {
|
||||
val state = currentStateProvider()
|
||||
return state.copy(
|
||||
bottomSheetConfig = TangemBottomSheetConfig(
|
||||
isShow = true,
|
||||
onDismissRequest = clickIntents::onDismissBottomSheet,
|
||||
content = ExchangeStatusBottomSheetConfig(
|
||||
value = state.swapTxs.first { it.txId == txId },
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
fun getStateAndTriggerEvent(
|
||||
state: TokenDetailsState,
|
||||
errorMessage: TextReference,
|
||||
|
|
|
|||
|
|
@ -0,0 +1,227 @@
|
|||
package com.tangem.feature.tokendetails.presentation.tokendetails.state.factory
|
||||
|
||||
import com.tangem.common.Provider
|
||||
import com.tangem.core.ui.components.currency.tokenicon.converter.CryptoCurrencyToIconStateConverter
|
||||
import com.tangem.core.ui.extensions.TextReference
|
||||
import com.tangem.core.ui.extensions.combinedReference
|
||||
import com.tangem.core.ui.utils.BigDecimalFormatter
|
||||
import com.tangem.core.ui.utils.toDateFormat
|
||||
import com.tangem.core.ui.utils.toTimeFormat
|
||||
import com.tangem.domain.appcurrency.model.AppCurrency
|
||||
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
|
||||
import com.tangem.feature.swap.domain.models.domain.ExchangeStatus
|
||||
import com.tangem.feature.swap.domain.models.domain.SavedSwapTransactionListModel
|
||||
import com.tangem.feature.tokendetails.presentation.tokendetails.state.ExchangeStatusState
|
||||
import com.tangem.feature.tokendetails.presentation.tokendetails.state.SwapTransactionsState
|
||||
import com.tangem.feature.tokendetails.presentation.tokendetails.state.components.ExchangeStatusNotifications
|
||||
import com.tangem.feature.tokendetails.presentation.tokendetails.viewmodels.TokenDetailsClickIntents
|
||||
import com.tangem.features.tokendetails.impl.R
|
||||
import com.tangem.utils.converter.Converter
|
||||
import kotlinx.collections.immutable.PersistentList
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
import kotlinx.collections.immutable.toPersistentList
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.update
|
||||
import java.math.BigDecimal
|
||||
|
||||
internal class TokenDetailsSwapTransactionsStateConverter(
|
||||
private val clickIntents: TokenDetailsClickIntents,
|
||||
appCurrencyProvider: Provider<AppCurrency>,
|
||||
) : Converter<Unit, PersistentList<SwapTransactionsState>> {
|
||||
|
||||
private val iconStateConverter = CryptoCurrencyToIconStateConverter()
|
||||
private val appCurrency = appCurrencyProvider()
|
||||
|
||||
override fun convert(value: Unit): PersistentList<SwapTransactionsState> {
|
||||
return persistentListOf()
|
||||
}
|
||||
|
||||
fun convert(
|
||||
savedTransactions: List<SavedSwapTransactionListModel>,
|
||||
cryptoStatusList: List<CryptoCurrencyStatus>,
|
||||
): PersistentList<SwapTransactionsState> {
|
||||
val result = mutableListOf<SwapTransactionsState>()
|
||||
|
||||
savedTransactions.forEach { swapCurrency ->
|
||||
val firstStatus = cryptoStatusList.first { it.currency.id.value == swapCurrency.fromCryptoCurrencyId }
|
||||
val secondStatus = cryptoStatusList.first { it.currency.id.value == swapCurrency.toCryptoCurrencyId }
|
||||
|
||||
val (fromCurrency, toCurrency) = if (swapCurrency.fromCryptoCurrencyId == firstStatus.currency.id.value) {
|
||||
firstStatus to secondStatus
|
||||
} else {
|
||||
secondStatus to firstStatus
|
||||
}
|
||||
|
||||
swapCurrency.transactions.forEach { transaction ->
|
||||
val toAmount = transaction.toCryptoAmount
|
||||
val fromAmount = transaction.fromCryptoAmount
|
||||
val toFiatAmount = toAmount.multiply(toCurrency.value.fiatRate)
|
||||
val fromFiatAmount = fromAmount.multiply(fromCurrency.value.fiatRate)
|
||||
val timestamp = transaction.timestamp
|
||||
result.add(
|
||||
SwapTransactionsState(
|
||||
txId = transaction.txId,
|
||||
provider = transaction.provider,
|
||||
txUrl = transaction.status?.txUrl,
|
||||
timestamp = TextReference.Str("${timestamp.toDateFormat()}, ${timestamp.toTimeFormat()}"),
|
||||
fiatSymbol = appCurrency.symbol,
|
||||
statuses = MutableStateFlow(getStatuses(transaction.status?.status)),
|
||||
activeStatus = MutableStateFlow(transaction.status?.status),
|
||||
notification = MutableStateFlow(
|
||||
getNotification(
|
||||
transaction.status?.status,
|
||||
transaction.status?.txUrl,
|
||||
),
|
||||
),
|
||||
toCryptoCurrencyId = toCurrency.currency.id,
|
||||
toCryptoAmount = BigDecimalFormatter.formatCryptoAmount(
|
||||
cryptoAmount = toAmount,
|
||||
cryptoCurrency = toCurrency.currency,
|
||||
),
|
||||
toCryptoSymbol = toCurrency.currency.symbol,
|
||||
toFiatAmount = getFiatAmount(toFiatAmount),
|
||||
toCurrencyIcon = iconStateConverter.convert(toCurrency),
|
||||
fromCryptoCurrencyId = fromCurrency.currency.id,
|
||||
fromCryptoAmount = BigDecimalFormatter.formatCryptoAmount(
|
||||
cryptoAmount = fromAmount,
|
||||
cryptoCurrency = fromCurrency.currency,
|
||||
),
|
||||
fromCryptoSymbol = fromCurrency.currency.symbol,
|
||||
fromFiatAmount = getFiatAmount(fromFiatAmount),
|
||||
fromCurrencyIcon = iconStateConverter.convert(fromCurrency),
|
||||
onClick = { clickIntents.onSwapTransactionClick(transaction.txId) },
|
||||
onGoToProviderClick = { clickIntents.onGoToProviderClick(transaction.status?.txUrl.orEmpty()) },
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
return result.toPersistentList()
|
||||
}
|
||||
|
||||
fun updateTxStatus(tx: SwapTransactionsState, status: ExchangeStatus?) {
|
||||
if (tx.activeStatus.value == status) return
|
||||
tx.activeStatus.update { status }
|
||||
tx.notification.update { getNotification(status, tx.txUrl) }
|
||||
tx.statuses.update { getStatuses(status) }
|
||||
}
|
||||
|
||||
private fun getFiatAmount(toFiatAmount: BigDecimal): String {
|
||||
return BigDecimalFormatter.formatFiatAmount(
|
||||
fiatAmount = toFiatAmount,
|
||||
fiatCurrencyCode = appCurrency.code,
|
||||
fiatCurrencySymbol = appCurrency.symbol,
|
||||
)
|
||||
}
|
||||
|
||||
private fun getNotification(status: ExchangeStatus?, txUrl: String?): ExchangeStatusNotifications? {
|
||||
if (txUrl == null) return null
|
||||
return when (status) {
|
||||
ExchangeStatus.Failed -> ExchangeStatusNotifications.Failed {
|
||||
clickIntents.onGoToProviderClick(txUrl)
|
||||
}
|
||||
ExchangeStatus.Verifying -> ExchangeStatusNotifications.NeedVerification {
|
||||
clickIntents.onGoToProviderClick(txUrl)
|
||||
}
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
|
||||
private fun getStatuses(status: ExchangeStatus?): List<ExchangeStatusState> {
|
||||
val isWaiting = status == ExchangeStatus.New || status == ExchangeStatus.Waiting
|
||||
val isConfirming = status == ExchangeStatus.Confirming
|
||||
val isVerifying = status == ExchangeStatus.Verifying
|
||||
val isExchanging = status == ExchangeStatus.Exchanging
|
||||
val isFailed = status == ExchangeStatus.Failed
|
||||
val isSending = status == ExchangeStatus.Sending
|
||||
|
||||
val isWaitingDone = !isWaiting
|
||||
val isConfirmingDone = !isConfirming && isWaitingDone
|
||||
val isExchangingDone = !isExchanging && isConfirmingDone
|
||||
val isSendingDone = !isSending && !isVerifying && !isFailed && isExchangingDone
|
||||
|
||||
return listOf(
|
||||
waitStep(isWaiting, isWaitingDone),
|
||||
confirmStep(isConfirming, isConfirmingDone),
|
||||
exchangeStep(isExchanging, isExchangingDone, isVerifying, isFailed),
|
||||
sendStep(isSending, isSendingDone),
|
||||
)
|
||||
}
|
||||
|
||||
private fun waitStep(isNew: Boolean, isNewDone: Boolean) = ExchangeStatusState(
|
||||
status = ExchangeStatus.New,
|
||||
text = when {
|
||||
isNew -> combinedReference(
|
||||
TextReference.Res(R.string.express_exchange_status_receiving),
|
||||
TextReference.Str(STATUS_ACTIVE_DOTS),
|
||||
)
|
||||
else -> TextReference.Res(R.string.express_exchange_status_receiving)
|
||||
},
|
||||
isActive = isNew,
|
||||
isDone = isNewDone,
|
||||
)
|
||||
|
||||
private fun confirmStep(isConfirming: Boolean, isConfirmingDone: Boolean) = ExchangeStatusState(
|
||||
status = ExchangeStatus.Confirming,
|
||||
text = when {
|
||||
isConfirming -> combinedReference(
|
||||
TextReference.Res(R.string.express_exchange_status_confirming),
|
||||
TextReference.Str(STATUS_ACTIVE_DOTS),
|
||||
)
|
||||
isConfirmingDone -> TextReference.Res(R.string.express_exchange_status_confirmed)
|
||||
else -> TextReference.Res(R.string.express_exchange_status_confirming)
|
||||
},
|
||||
isActive = isConfirming,
|
||||
isDone = isConfirmingDone,
|
||||
)
|
||||
|
||||
private fun exchangeStep(
|
||||
isExchanging: Boolean,
|
||||
isExchangingDone: Boolean,
|
||||
isVerifying: Boolean = false,
|
||||
isFailed: Boolean = false,
|
||||
) = when {
|
||||
isVerifying -> ExchangeStatusState(
|
||||
status = ExchangeStatus.Verifying,
|
||||
text = TextReference.Res(R.string.express_exchange_status_verifying),
|
||||
isActive = true,
|
||||
isDone = false,
|
||||
)
|
||||
isFailed -> ExchangeStatusState(
|
||||
status = ExchangeStatus.Failed,
|
||||
text = TextReference.Res(R.string.express_exchange_status_failed),
|
||||
isActive = true,
|
||||
isDone = false,
|
||||
)
|
||||
else -> ExchangeStatusState(
|
||||
status = ExchangeStatus.Exchanging,
|
||||
text = when {
|
||||
isExchanging -> combinedReference(
|
||||
TextReference.Res(R.string.express_exchange_status_exchanging),
|
||||
TextReference.Str(STATUS_ACTIVE_DOTS),
|
||||
)
|
||||
isExchangingDone -> TextReference.Res(R.string.express_exchange_status_exchanged)
|
||||
else -> TextReference.Res(R.string.express_exchange_status_exchanging)
|
||||
},
|
||||
isActive = isExchanging,
|
||||
isDone = isExchangingDone,
|
||||
)
|
||||
}
|
||||
|
||||
private fun sendStep(isSending: Boolean, isSendingDone: Boolean) = ExchangeStatusState(
|
||||
status = ExchangeStatus.Sending,
|
||||
text = when {
|
||||
isSending -> combinedReference(
|
||||
TextReference.Res(R.string.express_exchange_status_sending),
|
||||
TextReference.Str(STATUS_ACTIVE_DOTS),
|
||||
)
|
||||
isSendingDone -> TextReference.Res(R.string.express_exchange_status_sent)
|
||||
else -> TextReference.Res(R.string.express_exchange_status_sending)
|
||||
},
|
||||
isActive = isSending,
|
||||
isDone = isSendingDone,
|
||||
)
|
||||
|
||||
private companion object {
|
||||
private const val STATUS_ACTIVE_DOTS = "…"
|
||||
}
|
||||
}
|
||||
|
|
@ -22,7 +22,9 @@ internal fun ExchangeEstimate(
|
|||
fromTokenIconState: TokenIconState,
|
||||
toTokenIconState: TokenIconState,
|
||||
fromCryptoAmount: TextReference,
|
||||
fromCryptoSymbol: String,
|
||||
toCryptoAmount: TextReference,
|
||||
toCryptoSymbol: String,
|
||||
fromFiatAmount: TextReference,
|
||||
toFiatAmount: TextReference,
|
||||
modifier: Modifier = Modifier,
|
||||
|
|
@ -59,9 +61,11 @@ internal fun ExchangeEstimate(
|
|||
leftIcon = fromTokenIconState,
|
||||
leftTitle = fromCryptoAmount,
|
||||
leftSubtitle = fromFiatAmount,
|
||||
leftTitleEllipsisOffset = fromCryptoSymbol.length,
|
||||
rightIcon = toTokenIconState,
|
||||
rightTitle = toCryptoAmount,
|
||||
rightSubtitle = toFiatAmount,
|
||||
rightTitleEllipsisOffset = toCryptoSymbol.length,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -17,19 +17,22 @@ import androidx.compose.ui.draw.clip
|
|||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import com.tangem.core.ui.components.SpacerWMax
|
||||
import com.tangem.core.ui.extensions.resolveReference
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.feature.swap.domain.models.domain.ExchangeStatus
|
||||
import com.tangem.feature.tokendetails.presentation.tokendetails.state.ExchangeStatusState
|
||||
import com.tangem.features.tokendetails.impl.R
|
||||
import kotlinx.collections.immutable.PersistentList
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
|
||||
@Composable
|
||||
internal fun ExchangeStatusBlock(
|
||||
statuses: PersistentList<ExchangeStatusState>,
|
||||
statuses: MutableStateFlow<List<ExchangeStatusState>>,
|
||||
onClick: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
val statusValues = statuses.collectAsStateWithLifecycle()
|
||||
Column(
|
||||
modifier = modifier
|
||||
.clip(TangemTheme.shapes.roundedCornersXMedium)
|
||||
|
|
@ -70,10 +73,10 @@ internal fun ExchangeStatusBlock(
|
|||
}
|
||||
}
|
||||
|
||||
statuses.forEachIndexed { index, item ->
|
||||
statusValues.value.forEachIndexed { index, item ->
|
||||
ExchangeStatusStep(
|
||||
stepStatus = item,
|
||||
isLast = index == statuses.lastIndex,
|
||||
isLast = index == statusValues.value.lastIndex,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -128,7 +131,7 @@ private fun ExchangeStatusStepText(stepStatus: ExchangeStatusState) {
|
|||
}
|
||||
|
||||
Text(
|
||||
text = getStatusText(stepStatus)?.let { stringResource(it) }.orEmpty(),
|
||||
text = stepStatus.text.resolveReference(),
|
||||
style = TangemTheme.typography.body2,
|
||||
color = textColor,
|
||||
modifier = Modifier
|
||||
|
|
@ -206,34 +209,4 @@ private fun ExchangeStepSeparator() {
|
|||
shape = CircleShape,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
private fun getStatusText(stepStatus: ExchangeStatusState) = when (stepStatus.status) {
|
||||
ExchangeStatus.Failed -> R.string.express_exchange_status_failed
|
||||
ExchangeStatus.Verifying -> if (stepStatus.isDone) {
|
||||
R.string.express_exchange_status_verified
|
||||
} else {
|
||||
R.string.express_exchange_status_verifying
|
||||
}
|
||||
ExchangeStatus.New, ExchangeStatus.Waiting -> if (stepStatus.isDone) {
|
||||
R.string.express_exchange_status_received
|
||||
} else {
|
||||
R.string.express_exchange_status_receiving
|
||||
}
|
||||
ExchangeStatus.Confirming -> if (stepStatus.isDone) {
|
||||
R.string.express_exchange_status_confirmed
|
||||
} else {
|
||||
R.string.express_exchange_status_confirming
|
||||
}
|
||||
ExchangeStatus.Exchanging -> if (stepStatus.isDone) {
|
||||
R.string.express_exchange_status_exchanged
|
||||
} else {
|
||||
R.string.express_exchange_status_exchanging
|
||||
}
|
||||
ExchangeStatus.Sending -> if (stepStatus.isDone) {
|
||||
R.string.express_exchange_status_sent
|
||||
} else {
|
||||
R.string.express_exchange_status_sending
|
||||
}
|
||||
else -> null
|
||||
}
|
||||
|
|
@ -8,6 +8,7 @@ import androidx.compose.runtime.Composable
|
|||
import androidx.compose.ui.Alignment.Companion.CenterHorizontally
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import com.tangem.core.ui.R
|
||||
import com.tangem.core.ui.components.SpacerH10
|
||||
import com.tangem.core.ui.components.SpacerH12
|
||||
|
|
@ -19,8 +20,7 @@ import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfigContent
|
|||
import com.tangem.core.ui.components.notifications.Notification
|
||||
import com.tangem.core.ui.extensions.TextReference
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.core.ui.utils.toDateFormat
|
||||
import com.tangem.core.ui.utils.toTimeFormat
|
||||
import com.tangem.feature.swap.domain.models.domain.ExchangeStatus
|
||||
import com.tangem.feature.tokendetails.presentation.tokendetails.state.SwapTransactionsState
|
||||
|
||||
@Composable
|
||||
|
|
@ -36,6 +36,8 @@ internal fun ExchangeStatusBottomSheet(config: TangemBottomSheetConfig) {
|
|||
@Composable
|
||||
private fun ExchangeStatusBottomSheetContent(content: ExchangeStatusBottomSheetConfig) {
|
||||
val config = content.value
|
||||
val status = config.activeStatus.collectAsStateWithLifecycle()
|
||||
val notification = config.notification.collectAsStateWithLifecycle()
|
||||
Column(
|
||||
modifier = Modifier.padding(horizontal = TangemTheme.dimens.spacing16),
|
||||
) {
|
||||
|
|
@ -55,13 +57,14 @@ private fun ExchangeStatusBottomSheetContent(content: ExchangeStatusBottomSheetC
|
|||
.align(CenterHorizontally),
|
||||
)
|
||||
SpacerH16()
|
||||
val timestamp = config.timestamp
|
||||
ExchangeEstimate(
|
||||
timestamp = TextReference.Str("${timestamp.toDateFormat()}, ${timestamp.toTimeFormat()}"),
|
||||
timestamp = config.timestamp,
|
||||
fromTokenIconState = config.fromCurrencyIcon,
|
||||
toTokenIconState = config.toCurrencyIcon,
|
||||
fromCryptoAmount = TextReference.Str(config.fromCryptoAmount),
|
||||
fromCryptoSymbol = config.fromCryptoSymbol,
|
||||
toCryptoAmount = TextReference.Str(config.toCryptoAmount),
|
||||
toCryptoSymbol = config.toCryptoSymbol,
|
||||
fromFiatAmount = TextReference.Str(config.fromFiatAmount),
|
||||
toFiatAmount = TextReference.Str(config.toFiatAmount),
|
||||
)
|
||||
|
|
@ -77,12 +80,20 @@ private fun ExchangeStatusBottomSheetContent(content: ExchangeStatusBottomSheetC
|
|||
onClick = config.onGoToProviderClick,
|
||||
)
|
||||
AnimatedContent(
|
||||
targetState = config.notification,
|
||||
targetState = notification.value,
|
||||
label = "Exchange Status Notification Change",
|
||||
) {
|
||||
it?.let {
|
||||
SpacerH12()
|
||||
Notification(config = it.config)
|
||||
val tint = when (status.value) {
|
||||
ExchangeStatus.Verifying -> TangemTheme.colors.icon.attention
|
||||
ExchangeStatus.Failed -> TangemTheme.colors.icon.warning
|
||||
else -> null
|
||||
}
|
||||
Notification(
|
||||
config = it.config,
|
||||
iconTint = tint,
|
||||
modifier = Modifier.padding(top = TangemTheme.dimens.spacing12),
|
||||
)
|
||||
}
|
||||
}
|
||||
SpacerH24()
|
||||
|
|
|
|||
|
|
@ -4,9 +4,7 @@ import androidx.annotation.DrawableRes
|
|||
import androidx.compose.foundation.ExperimentalFoundationApi
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.lazy.LazyListScope
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.Text
|
||||
|
|
@ -16,9 +14,11 @@ import androidx.compose.ui.draw.clip
|
|||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.constraintlayout.compose.ConstraintLayout
|
||||
import androidx.constraintlayout.compose.Dimension
|
||||
import androidx.constraintlayout.compose.Visibility
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
||||
import androidx.constraintlayout.compose.*
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import com.tangem.core.ui.components.atoms.text.EllipsisText
|
||||
import com.tangem.core.ui.components.atoms.text.TextEllipsis
|
||||
import com.tangem.core.ui.components.currency.tokenicon.TokenIcon
|
||||
|
|
@ -41,8 +41,8 @@ internal fun LazyListScope.swapTransactionsItems(
|
|||
contentType = { swapTxs[it]::class.java },
|
||||
) {
|
||||
val item = swapTxs[it]
|
||||
|
||||
val (iconRes, tint) = when (item.activeStatus) {
|
||||
val status = item.activeStatus.collectAsStateWithLifecycle()
|
||||
val (iconRes, tint) = when (status.value) {
|
||||
ExchangeStatus.Verifying -> R.drawable.ic_alert_triangle_20 to TangemTheme.colors.icon.attention
|
||||
ExchangeStatus.Failed -> R.drawable.ic_alert_circle_24 to TangemTheme.colors.icon.warning
|
||||
else -> null to null
|
||||
|
|
@ -54,7 +54,6 @@ internal fun LazyListScope.swapTransactionsItems(
|
|||
toTokenIconState = item.toCurrencyIcon,
|
||||
fromAmount = item.fromCryptoAmount,
|
||||
fromSymbol = item.fromCryptoSymbol,
|
||||
toAmount = item.toCryptoAmount,
|
||||
toSymbol = item.toCryptoSymbol,
|
||||
onClick = item.onClick,
|
||||
infoIconRes = iconRes,
|
||||
|
|
@ -73,7 +72,6 @@ private fun ExchangeStatusItem(
|
|||
toTokenIconState: TokenIconState,
|
||||
fromAmount: String,
|
||||
fromSymbol: String,
|
||||
toAmount: String,
|
||||
toSymbol: String,
|
||||
onClick: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
|
|
@ -121,7 +119,7 @@ private fun ExchangeStatusItem(
|
|||
top.linkTo(titleRef.bottom, padding6)
|
||||
end.linkTo(swapIconRef.start)
|
||||
bottom.linkTo(parent.bottom)
|
||||
width = Dimension.fillToConstraints
|
||||
width = Dimension.fillToConstraints.atMostWrapContent
|
||||
},
|
||||
)
|
||||
Icon(
|
||||
|
|
@ -149,17 +147,16 @@ private fun ExchangeStatusItem(
|
|||
bottom.linkTo(parent.bottom)
|
||||
},
|
||||
)
|
||||
EllipsisText(
|
||||
text = toAmount,
|
||||
Text(
|
||||
text = toSymbol,
|
||||
style = TangemTheme.typography.body2,
|
||||
color = TangemTheme.colors.text.primary1,
|
||||
ellipsis = TextEllipsis.OffsetEnd(toSymbol.length),
|
||||
modifier = Modifier.constrainAs(toRef) {
|
||||
start.linkTo(toIconRef.end, padding6)
|
||||
top.linkTo(titleRef.bottom, padding6)
|
||||
end.linkTo(infoIconRef.start, padding6)
|
||||
end.linkTo(infoIconRef.start, padding6, padding6)
|
||||
bottom.linkTo(parent.bottom)
|
||||
width = Dimension.fillToConstraints
|
||||
width = Dimension.fillToConstraints.atLeastWrapContent
|
||||
},
|
||||
)
|
||||
Icon(
|
||||
|
|
@ -192,4 +189,54 @@ private fun ExchangeStatusItem(
|
|||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//region Preview
|
||||
@Preview
|
||||
@Composable
|
||||
private fun ExchangeStatusItemPreview_Light(
|
||||
@PreviewParameter(ExchangeStatusItemsPreviewParameterProvider::class) amount: String,
|
||||
) {
|
||||
TangemTheme {
|
||||
ExchangeStatusItem(
|
||||
providerName = "ChangeNow",
|
||||
fromTokenIconState = TokenIconState.Loading,
|
||||
toTokenIconState = TokenIconState.Loading,
|
||||
fromAmount = amount,
|
||||
fromSymbol = "USDT",
|
||||
toSymbol = "USDT",
|
||||
onClick = {},
|
||||
infoIconRes = null,
|
||||
infoIconTint = null,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
private fun ExchangeStatusItemPreview_Dark(
|
||||
@PreviewParameter(ExchangeStatusItemsPreviewParameterProvider::class) amount: String,
|
||||
) {
|
||||
TangemTheme(isDark = true) {
|
||||
ExchangeStatusItem(
|
||||
providerName = "ChangeNow",
|
||||
fromTokenIconState = TokenIconState.Loading,
|
||||
toTokenIconState = TokenIconState.Loading,
|
||||
fromAmount = amount,
|
||||
fromSymbol = "USDT",
|
||||
toSymbol = "USDT",
|
||||
onClick = {},
|
||||
infoIconRes = null,
|
||||
infoIconTint = null,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private class ExchangeStatusItemsPreviewParameterProvider : PreviewParameterProvider<String> {
|
||||
override val values: Sequence<String>
|
||||
get() = sequenceOf(
|
||||
"1111111111111111111111111111 USDT",
|
||||
"11111 USDT",
|
||||
)
|
||||
}
|
||||
//endregion
|
||||
|
|
@ -0,0 +1,141 @@
|
|||
package com.tangem.feature.tokendetails.presentation.tokendetails.viewmodels
|
||||
|
||||
import arrow.core.getOrElse
|
||||
import com.tangem.common.Provider
|
||||
import com.tangem.domain.appcurrency.model.AppCurrency
|
||||
import com.tangem.domain.tokens.GetCryptoCurrencyStatusesSyncUseCase
|
||||
import com.tangem.domain.tokens.model.CryptoCurrency
|
||||
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
|
||||
import com.tangem.domain.wallets.models.UserWalletId
|
||||
import com.tangem.domain.wallets.usecase.GetSelectedWalletSyncUseCase
|
||||
import com.tangem.feature.swap.domain.SwapRepository
|
||||
import com.tangem.feature.swap.domain.SwapTransactionRepository
|
||||
import com.tangem.feature.swap.domain.models.domain.ExchangeStatus
|
||||
import com.tangem.feature.swap.domain.models.domain.ExchangeStatusModel
|
||||
import com.tangem.feature.swap.domain.models.domain.SavedSwapTransactionListModel
|
||||
import com.tangem.feature.tokendetails.presentation.tokendetails.state.SwapTransactionsState
|
||||
import com.tangem.feature.tokendetails.presentation.tokendetails.state.factory.TokenDetailsSwapTransactionsStateConverter
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import kotlinx.collections.immutable.PersistentList
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
import kotlinx.collections.immutable.toPersistentList
|
||||
import kotlinx.coroutines.async
|
||||
import kotlinx.coroutines.awaitAll
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.flow.conflate
|
||||
import kotlinx.coroutines.flow.flow
|
||||
import kotlinx.coroutines.withContext
|
||||
|
||||
@Suppress("LongParameterList")
|
||||
internal class ExchangeStatusFactory(
|
||||
private val swapTransactionRepository: SwapTransactionRepository,
|
||||
private val swapRepository: SwapRepository,
|
||||
private val getSelectedWalletSyncUseCase: GetSelectedWalletSyncUseCase,
|
||||
private val getMultiCryptoCurrencyStatusUseCase: GetCryptoCurrencyStatusesSyncUseCase,
|
||||
private val dispatchers: CoroutineDispatcherProvider,
|
||||
private val clickIntents: TokenDetailsClickIntents,
|
||||
private val appCurrencyProvider: Provider<AppCurrency>,
|
||||
private val userWalletId: UserWalletId,
|
||||
private val cryptoCurrencyId: CryptoCurrency.ID,
|
||||
) {
|
||||
|
||||
private val swapTransactionsStateConverter by lazy {
|
||||
TokenDetailsSwapTransactionsStateConverter(
|
||||
clickIntents = clickIntents,
|
||||
appCurrencyProvider = appCurrencyProvider,
|
||||
)
|
||||
}
|
||||
|
||||
operator fun invoke() = combine(
|
||||
flow = swapTransactionRepository.getTransactions(userWalletId, cryptoCurrencyId),
|
||||
flow2 = getWalletCryptoCurrencies().conflate(),
|
||||
) { savedTransactions, cryptoCurrenciesStatusList ->
|
||||
innerLoadSwapState(
|
||||
savedTransactions,
|
||||
cryptoCurrenciesStatusList,
|
||||
)
|
||||
}
|
||||
|
||||
private fun getWalletCryptoCurrencies() = flow {
|
||||
val selectedWallet = getSelectedWalletSyncUseCase().fold(
|
||||
ifLeft = { null },
|
||||
ifRight = { it },
|
||||
)
|
||||
requireNotNull(selectedWallet) { "No selected wallet" }
|
||||
|
||||
val cryptoCurrenciesList = getMultiCryptoCurrencyStatusUseCase(selectedWallet.walletId)
|
||||
.getOrElse { emptyList() }
|
||||
|
||||
emit(cryptoCurrenciesList)
|
||||
}
|
||||
|
||||
suspend fun updateSwapTxStatuses(swapTxList: PersistentList<SwapTransactionsState>) = withContext(dispatchers.io) {
|
||||
swapTxList.map { tx ->
|
||||
async {
|
||||
val status = getExchangeStatus(tx.txId)?.status
|
||||
swapTransactionsStateConverter.updateTxStatus(tx, status)
|
||||
tx.removeIfFinished(status)
|
||||
}
|
||||
}
|
||||
.awaitAll()
|
||||
.filterNotNull()
|
||||
.toPersistentList()
|
||||
}
|
||||
|
||||
private suspend fun innerLoadSwapState(
|
||||
savedTransactions: List<SavedSwapTransactionListModel>?,
|
||||
cryptoCurrenciesStatusList: List<CryptoCurrencyStatus>,
|
||||
): PersistentList<SwapTransactionsState> {
|
||||
val txWithStatuses = savedTransactions?.map { currencySwaps ->
|
||||
currencySwaps.copy(
|
||||
transactions = currencySwaps.transactions.map { tx ->
|
||||
val status = getExchangeStatus(tx.txId)
|
||||
tx.copy(
|
||||
status = status,
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
return getExchangeStatusState(
|
||||
savedTransactions = txWithStatuses,
|
||||
cryptoCurrencyStatusList = cryptoCurrenciesStatusList,
|
||||
)
|
||||
}
|
||||
|
||||
private suspend fun getExchangeStatus(txId: String): ExchangeStatusModel? {
|
||||
return swapRepository.getExchangeStatus(txId)
|
||||
.fold(
|
||||
ifLeft = { null },
|
||||
ifRight = { it },
|
||||
)
|
||||
}
|
||||
|
||||
private fun getExchangeStatusState(
|
||||
savedTransactions: List<SavedSwapTransactionListModel>?,
|
||||
cryptoCurrencyStatusList: List<CryptoCurrencyStatus>,
|
||||
): PersistentList<SwapTransactionsState> {
|
||||
if (savedTransactions == null || cryptoCurrencyStatusList.isEmpty()) {
|
||||
return persistentListOf()
|
||||
}
|
||||
|
||||
return swapTransactionsStateConverter.convert(
|
||||
savedTransactions = savedTransactions,
|
||||
cryptoStatusList = cryptoCurrencyStatusList,
|
||||
)
|
||||
}
|
||||
|
||||
private suspend fun SwapTransactionsState.removeIfFinished(status: ExchangeStatus?): SwapTransactionsState? {
|
||||
return if (status == ExchangeStatus.Refunded || status == ExchangeStatus.Finished) {
|
||||
swapTransactionRepository.removeTransaction(
|
||||
userWalletId = userWalletId,
|
||||
fromCryptoCurrencyId = fromCryptoCurrencyId,
|
||||
toCryptoCurrencyId = toCryptoCurrencyId,
|
||||
txId = txId,
|
||||
)
|
||||
null
|
||||
} else {
|
||||
this
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -38,4 +38,8 @@ interface TokenDetailsClickIntents {
|
|||
fun onDismissBottomSheet()
|
||||
|
||||
fun onCloseRentInfoNotification()
|
||||
|
||||
fun onSwapTransactionClick(txId: String)
|
||||
|
||||
fun onGoToProviderClick(url: String)
|
||||
}
|
||||
|
|
@ -31,16 +31,19 @@ import com.tangem.domain.txhistory.usecase.GetTxHistoryItemsUseCase
|
|||
import com.tangem.domain.walletmanager.WalletManagersFacade
|
||||
import com.tangem.domain.wallets.models.UserWalletId
|
||||
import com.tangem.domain.wallets.usecase.GetExploreUrlUseCase
|
||||
import com.tangem.domain.wallets.usecase.GetSelectedWalletSyncUseCase
|
||||
import com.tangem.domain.wallets.usecase.GetUserWalletUseCase
|
||||
import com.tangem.feature.swap.domain.SwapRepository
|
||||
import com.tangem.feature.swap.domain.SwapTransactionRepository
|
||||
import com.tangem.feature.tokendetails.presentation.router.InnerTokenDetailsRouter
|
||||
import com.tangem.feature.tokendetails.presentation.tokendetails.state.SwapTransactionsState
|
||||
import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenDetailsState
|
||||
import com.tangem.feature.tokendetails.presentation.tokendetails.state.factory.TokenDetailsStateFactory
|
||||
import com.tangem.features.tokendetails.impl.R
|
||||
import com.tangem.features.tokendetails.navigation.TokenDetailsRouter
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import com.tangem.utils.coroutines.JobHolder
|
||||
import com.tangem.utils.coroutines.saveIn
|
||||
import com.tangem.utils.coroutines.*
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import kotlinx.collections.immutable.PersistentList
|
||||
import kotlinx.coroutines.async
|
||||
import kotlinx.coroutines.awaitAll
|
||||
import kotlinx.coroutines.flow.*
|
||||
|
|
@ -50,7 +53,7 @@ import java.math.BigDecimal
|
|||
import javax.inject.Inject
|
||||
import kotlin.properties.Delegates
|
||||
|
||||
@Suppress("LongParameterList", "LargeClass")
|
||||
@Suppress("LongParameterList", "LargeClass", "TooManyFunctions")
|
||||
@HiltViewModel
|
||||
internal class TokenDetailsViewModel @Inject constructor(
|
||||
private val dispatchers: CoroutineDispatcherProvider,
|
||||
|
|
@ -67,6 +70,10 @@ internal class TokenDetailsViewModel @Inject constructor(
|
|||
private val getBalanceHidingSettingsUseCase: GetBalanceHidingSettingsUseCase,
|
||||
private val getCurrencyWarningsUseCase: GetCurrencyWarningsUseCase,
|
||||
private val getExplorerTransactionUrlUseCase: GetExplorerTransactionUrlUseCase,
|
||||
private val getSelectedWalletSyncUseCase: GetSelectedWalletSyncUseCase,
|
||||
private val getMultiCryptoCurrencyStatusUseCase: GetCryptoCurrencyStatusesSyncUseCase,
|
||||
private val swapRepository: SwapRepository,
|
||||
private val swapTransactionRepository: SwapTransactionRepository,
|
||||
private val walletManagersFacade: WalletManagersFacade,
|
||||
private val isDemoCardUseCase: IsDemoCardUseCase,
|
||||
private val reduxStateHolder: ReduxStateHolder,
|
||||
|
|
@ -86,8 +93,11 @@ internal class TokenDetailsViewModel @Inject constructor(
|
|||
private val marketPriceJobHolder = JobHolder()
|
||||
private val refreshStateJobHolder = JobHolder()
|
||||
private val warningsJobHolder = JobHolder()
|
||||
private val swapTxJobHolder = JobHolder()
|
||||
private var cryptoCurrencyStatus: CryptoCurrencyStatus? = null
|
||||
|
||||
private var swapTxStatusTaskScheduler = SingleTaskScheduler<PersistentList<SwapTransactionsState>>()
|
||||
|
||||
private val selectedAppCurrencyFlow: StateFlow<AppCurrency> = createSelectedAppCurrencyFlow()
|
||||
|
||||
private val stateFactory = TokenDetailsStateFactory(
|
||||
|
|
@ -98,6 +108,20 @@ internal class TokenDetailsViewModel @Inject constructor(
|
|||
decimals = cryptoCurrency.decimals,
|
||||
)
|
||||
|
||||
private val exchangeStatusFactory by lazy {
|
||||
ExchangeStatusFactory(
|
||||
swapTransactionRepository = swapTransactionRepository,
|
||||
swapRepository = swapRepository,
|
||||
getSelectedWalletSyncUseCase = getSelectedWalletSyncUseCase,
|
||||
getMultiCryptoCurrencyStatusUseCase = getMultiCryptoCurrencyStatusUseCase,
|
||||
dispatchers = dispatchers,
|
||||
clickIntents = this,
|
||||
appCurrencyProvider = Provider { selectedAppCurrencyFlow.value },
|
||||
userWalletId = userWalletId,
|
||||
cryptoCurrencyId = cryptoCurrency.id,
|
||||
)
|
||||
}
|
||||
|
||||
var uiState: TokenDetailsState by mutableStateOf(stateFactory.getInitialState(cryptoCurrency))
|
||||
private set
|
||||
|
||||
|
|
@ -109,8 +133,14 @@ internal class TokenDetailsViewModel @Inject constructor(
|
|||
handleBalanceHiding(owner)
|
||||
}
|
||||
|
||||
override fun onCleared() {
|
||||
swapTxStatusTaskScheduler.cancelTask()
|
||||
super.onCleared()
|
||||
}
|
||||
|
||||
private fun updateContent() {
|
||||
subscribeOnCurrencyStatusUpdates()
|
||||
subscribeOnExchangeTransactionsUpdates()
|
||||
updateTxHistory(refresh = false, showItemsLoading = true)
|
||||
}
|
||||
|
||||
|
|
@ -177,6 +207,33 @@ internal class TokenDetailsViewModel @Inject constructor(
|
|||
}
|
||||
}
|
||||
|
||||
private fun subscribeOnExchangeTransactionsUpdates() {
|
||||
viewModelScope.launch(dispatchers.io) {
|
||||
swapTxStatusTaskScheduler.cancelTask()
|
||||
exchangeStatusFactory.invoke()
|
||||
.onEach { swapTxs ->
|
||||
swapTxStatusTaskScheduler.scheduleTask(
|
||||
viewModelScope,
|
||||
PeriodicTask(
|
||||
delay = EXCHANGE_STATUS_UPDATE_DELAY,
|
||||
task = {
|
||||
runCatching(dispatchers.io) {
|
||||
exchangeStatusFactory.updateSwapTxStatuses(swapTxs)
|
||||
}
|
||||
},
|
||||
onSuccess = { updatedTxs ->
|
||||
uiState = uiState.copy(swapTxs = updatedTxs)
|
||||
},
|
||||
onError = {},
|
||||
),
|
||||
)
|
||||
}
|
||||
.flowOn(dispatchers.io)
|
||||
.launchIn(viewModelScope)
|
||||
.saveIn(swapTxJobHolder)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param refresh - invalidate cache and get data from remote
|
||||
* @param showItemsLoading - show loading items placeholder.
|
||||
|
|
@ -475,35 +532,16 @@ internal class TokenDetailsViewModel @Inject constructor(
|
|||
override fun onCloseRentInfoNotification() {
|
||||
uiState = stateFactory.getStateWithRemovedRentNotification()
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
// val text = when (stepStatus.status) {
|
||||
// ExchangeStatus.Failed -> stringResource(id = R.string.express_exchange_status_failed)
|
||||
// ExchangeStatus.Verifying -> if (stepStatus.isDone) {
|
||||
// stringResource(id = R.string.express_exchange_status_verified)
|
||||
// } else {
|
||||
// stringResource(id = R.string.express_exchange_status_verifying)
|
||||
// }
|
||||
// ExchangeStatus.New, ExchangeStatus.Waiting -> if (stepStatus.isDone) {
|
||||
// stringResource(id = R.string.express_exchange_status_received)
|
||||
// } else {
|
||||
// stringResource(id = R.string.express_exchange_status_receiving)
|
||||
// }
|
||||
// ExchangeStatus.Confirming -> if (stepStatus.isDone) {
|
||||
// stringResource(id = R.string.express_exchange_status_confirmed)
|
||||
// } else {
|
||||
// stringResource(id = R.string.express_exchange_status_confirming)
|
||||
// }
|
||||
// ExchangeStatus.Exchanging -> if (stepStatus.isDone) {
|
||||
// stringResource(id = R.string.express_exchange_status_exchanged)
|
||||
// } else {
|
||||
// stringResource(id = R.string.express_exchange_status_exchanging)
|
||||
// }
|
||||
// ExchangeStatus.Sending -> if (stepStatus.isDone) {
|
||||
// stringResource(id = R.string.express_exchange_status_sent)
|
||||
// } else {
|
||||
// stringResource(id = R.string.express_exchange_status_sending)
|
||||
// }
|
||||
// else -> ""
|
||||
// }
|
||||
override fun onSwapTransactionClick(txId: String) {
|
||||
uiState = stateFactory.getStateWithExchangeStatusBottomSheet(txId)
|
||||
}
|
||||
|
||||
override fun onGoToProviderClick(url: String) {
|
||||
router.openUrl(url)
|
||||
}
|
||||
|
||||
private companion object {
|
||||
const val EXCHANGE_STATUS_UPDATE_DELAY = 10_000L
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue