Updated on 2026-08-14

This commit is contained in:
Tangem 2026-07-06 13:39:30 +03:00
commit 92849d43c2
1407 changed files with 64092 additions and 11990 deletions

View file

@ -6,6 +6,7 @@ import androidx.compose.ui.Modifier
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.arkivanov.decompose.ComponentContext
import com.arkivanov.decompose.extensions.compose.subscribeAsState
import com.arkivanov.decompose.router.slot.activate
import com.arkivanov.decompose.router.slot.childSlot
import com.arkivanov.decompose.router.slot.dismiss
import com.tangem.core.decompose.context.AppComponentContext
@ -32,6 +33,8 @@ import com.tangem.features.tokendetails.ExpressTransactionsComponent
import com.tangem.features.tokendetails.TokenDetailsComponent
import com.tangem.features.tokenreceive.TokenReceiveComponent
import com.tangem.features.txhistory.component.TxHistoryComponent
import com.tangem.features.txhistory.component.TxHistoryDetailsComponent
import com.tangem.features.txhistory.component.TxHistoryDetailsSlotConfig
import com.tangem.features.yield.supply.api.YieldSupplyComponent
import com.tangem.features.yield.supply.api.YieldSupplyDepositedWarningComponent
import dagger.assisted.Assisted
@ -44,6 +47,7 @@ internal class DefaultTokenDetailsComponent @AssistedInject constructor(
@Assisted params: TokenDetailsComponent.Params,
tokenMarketBlockComponentFactory: TokenMarketBlockComponent.Factory,
txHistoryComponentFactory: TxHistoryComponent.Factory,
private val txHistoryDetailsComponentFactory: TxHistoryDetailsComponent.Factory,
expressTransactionsComponentFactory: ExpressTransactionsComponent.Factory,
private val tokenReceiveComponentFactory: TokenReceiveComponent.Factory,
private val yieldSupplyWarningComponentFactory: YieldSupplyDepositedWarningComponent.Factory,
@ -59,6 +63,9 @@ internal class DefaultTokenDetailsComponent @AssistedInject constructor(
userWalletId = params.userWalletId,
currency = params.currency,
openExplorer = model::onExploreClick,
onTxDetailsRequested = { txHistoryInfo ->
model.txDetailsNavigation.activate(TxHistoryDetailsSlotConfig(txHistoryInfo))
},
),
)
@ -88,6 +95,24 @@ internal class DefaultTokenDetailsComponent @AssistedInject constructor(
},
)
private val txHistoryDetailsSlot = childSlot(
key = TX_HISTORY_DETAILS_SLOT_KEY,
source = model.txDetailsNavigation,
serializer = null,
handleBackButton = true,
childFactory = { config, ctx ->
txHistoryDetailsComponentFactory.create(
context = childByContext(ctx),
params = TxHistoryDetailsComponent.Params(
txHistoryInfo = config.txHistoryInfo,
userWalletId = params.userWalletId,
currency = params.currency,
onDismiss = model.txDetailsNavigation::dismiss,
),
)
},
)
private val tokenMarketBlockComponent = params.currency.toTokenMarketParam()?.let { tokenMarketParams ->
tokenMarketBlockComponentFactory.create(
appComponentContext = child("tokenMarketBlockComponent"),
@ -109,6 +134,7 @@ internal class DefaultTokenDetailsComponent @AssistedInject constructor(
override fun Content(modifier: Modifier) {
val bottomSheet by bottomSheetSlot.subscribeAsState()
val ratingSlotState by ratingSlot.subscribeAsState()
val txHistoryDetails by txHistoryDetailsSlot.subscribeAsState()
NavigationBar3ButtonsScrim()
if (LocalRedesignEnabled.current) {
@ -136,6 +162,7 @@ internal class DefaultTokenDetailsComponent @AssistedInject constructor(
}
bottomSheet.child?.instance?.BottomSheet()
txHistoryDetails.child?.instance?.BottomSheet()
}
private fun CryptoCurrency.toTokenMarketParam(): TokenMarketBlockComponent.Params? {
@ -204,5 +231,6 @@ internal class DefaultTokenDetailsComponent @AssistedInject constructor(
companion object {
private const val RATING_SLOT_KEY = "ratingSlot"
private const val TX_HISTORY_DETAILS_SLOT_KEY = "txHistoryDetailsSlot"
}
}

View file

@ -14,6 +14,9 @@ import com.tangem.common.routing.AppRoute
import com.tangem.common.routing.AppRouter
import com.tangem.common.ui.bottomsheet.receive.AddressModel
import com.tangem.common.ui.bottomsheet.receive.mapToAddressModels
import com.tangem.features.rating.RatingComponent
import com.tangem.feature.swap.domain.SwapFeedbackUseCase
import com.tangem.feature.swap.domain.models.domain.SwapFeedbackParams
import com.tangem.common.ui.tokens.getUnavailabilityReasonText
import com.tangem.common.ui.userwallet.converter.WalletIconUMConverter
import com.tangem.common.ui.userwallet.ext.walletInterationIcon
@ -88,8 +91,6 @@ import com.tangem.domain.txhistory.usecase.GetFixedTxHistoryItemsUseCase
import com.tangem.domain.wallets.usecase.*
import com.tangem.domain.yield.supply.models.YieldSupplyRewardBalance
import com.tangem.domain.yield.supply.usecase.YieldSupplyGetRewardsBalanceUseCase
import com.tangem.feature.swap.domain.SwapFeedbackUseCase
import com.tangem.feature.swap.domain.models.domain.SwapFeedbackParams
import com.tangem.feature.tokendetails.deeplink.TokenDetailsDeepLinkActionListener
import com.tangem.feature.tokendetails.domain.GetCurrencyWarningsUseCase
import com.tangem.feature.tokendetails.presentation.router.InnerTokenDetailsRouter
@ -100,12 +101,11 @@ import com.tangem.feature.tokendetails.presentation.tokendetails.state.*
import com.tangem.feature.tokendetails.presentation.tokendetails.state.factory.QuickTopUpBlockFactory
import com.tangem.feature.tokendetails.presentation.tokendetails.state.factory.TokenDetailsStateFactory
import com.tangem.feature.tokendetails.presentation.tokendetails.state.transformer.*
import com.tangem.features.rating.RatingComponent
import com.tangem.features.swap.SwapFeatureToggles
import com.tangem.features.tokendetails.ExpressTransactionsEvent
import com.tangem.features.tokendetails.ExpressTransactionsEventListener
import com.tangem.features.tokendetails.TokenDetailsComponent
import com.tangem.features.tokendetails.impl.R
import com.tangem.features.txhistory.component.TxHistoryDetailsSlotConfig
import com.tangem.features.txhistory.entity.TxHistoryContentUpdateEmitter
import com.tangem.features.yield.supply.api.YieldSupplyDepositedWarningComponent
import com.tangem.features.yield.supply.api.analytics.YieldSupplyAnalytics
@ -180,7 +180,6 @@ internal class TokenDetailsModel @Inject constructor(
private val designFeatureToggles: DesignFeatureToggles,
private val redesignStateController: TokenDetailsStateController,
private val swapFeedbackUseCase: SwapFeedbackUseCase,
private val swapFeatureToggles: SwapFeatureToggles,
private val quickTopUpBlockFactory: QuickTopUpBlockFactory,
private val getFixedTxHistoryItemsUseCase: GetFixedTxHistoryItemsUseCase,
private val checkOnrampAvailabilityUseCase: CheckOnrampAvailabilityUseCase,
@ -211,6 +210,7 @@ internal class TokenDetailsModel @Inject constructor(
val bottomSheetNavigation: SlotNavigation<TokenDetailsBottomSheetConfig> = SlotNavigation()
val ratingSlotNavigation = SlotNavigation<RatingComponent.Params>()
val txDetailsNavigation = SlotNavigation<TxHistoryDetailsSlotConfig>()
private val stateFactory = TokenDetailsStateFactory(
currentStateProvider = Provider { uiState.value },
@ -1160,7 +1160,6 @@ internal class TokenDetailsModel @Inject constructor(
txExternalUrl: String,
userWalletIdStringValue: String,
) {
if (!swapFeatureToggles.isSwapRateExperienceEnabled) return
ratingSlotNavigation.activate(
RatingComponent.Params(
onLoadRating = {

View file

@ -14,6 +14,7 @@ import com.tangem.domain.models.wallet.UserWallet
import com.tangem.domain.tokens.model.analytics.TokenExchangeAnalyticsEvent
import com.tangem.domain.tokens.model.analytics.TokenOnrampAnalyticsEvent
import com.tangem.domain.tokens.model.analytics.TokenScreenAnalyticsEvent
import com.tangem.domain.txhistory.TxHistoryFeatureToggles
import com.tangem.feature.swap.domain.models.domain.ExchangeStatus
import com.tangem.feature.tokendetails.presentation.tokendetails.model.ExpressTransactionsClickIntents
import com.tangem.feature.tokendetails.presentation.tokendetails.state.components.ExchangeStatusNotification
@ -47,6 +48,7 @@ internal class ExpressStatusFactory @AssistedInject constructor(
onrampStatusFactory: OnrampStatusFactory.Factory,
exchangeStatusFactory: ExchangeStatusFactory.Factory,
private val swapFeatureToggles: SwapFeatureToggles,
private val txHistoryFeatureToggles: TxHistoryFeatureToggles,
) {
private val exchangeStatusFactory by lazy(mode = LazyThreadSafetyMode.NONE) {
@ -106,12 +108,17 @@ internal class ExpressStatusFactory @AssistedInject constructor(
if (currentTx is ExchangeUM && currentTx.activeStatus == ExchangeStatus.Finished) {
updateBalance(currentTx.toCryptoCurrency)
}
val expressTxsToDisplay = expressTxs.filterNot {
when (it) {
is ExpressTransactionStateUM.OnrampUM -> it.activeStatus.isHidden
else -> false
}
}.toPersistentList()
val expressTxsToDisplay = if (txHistoryFeatureToggles.isNewTxHistoryEnabled) {
persistentListOf()
} else {
expressTxs.filterNot {
when (it) {
is ExpressTransactionStateUM.OnrampUM -> it.activeStatus.isHidden
else -> false
}
}.toPersistentList()
}
return state.copy(
transactions = expressTxs,
transactionsToDisplay = expressTxsToDisplay,

View file

@ -1,267 +0,0 @@
package com.tangem.feature.tokendetails.presentation.tokendetails.ui.components.express.exchange
import androidx.annotation.DrawableRes
import androidx.compose.animation.AnimatedContent
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.foundation.background
import androidx.compose.foundation.border
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.Icon
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.tooling.preview.Preview
import com.tangem.core.ui.components.SpacerWMax
import com.tangem.core.ui.extensions.resolveReference
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.extensions.stringResourceSafe
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreview
import com.tangem.feature.swap.domain.models.domain.ExchangeStatus
import com.tangem.feature.swap.domain.models.domain.ExchangeStatus.Companion.isFailed
import com.tangem.feature.tokendetails.presentation.tokendetails.state.express.ExchangeStatusState
import com.tangem.features.tokendetails.impl.R
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.toImmutableList
@Deprecated("Use ExpressStatusBlock from common")
@Composable
internal fun ExchangeStatusBlock(
statuses: ImmutableList<ExchangeStatusState>,
showLink: Boolean,
onClick: () -> Unit,
modifier: Modifier = Modifier,
) {
Column(
modifier = modifier
.clip(TangemTheme.shapes.roundedCornersXMedium)
.background(TangemTheme.colors.background.action)
.padding(
vertical = TangemTheme.dimens.spacing14,
horizontal = TangemTheme.dimens.spacing12,
),
) {
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier
.padding(bottom = TangemTheme.dimens.spacing16),
) {
Text(
text = stringResourceSafe(id = R.string.express_exchange_status_title),
style = TangemTheme.typography.subtitle2,
color = TangemTheme.colors.text.tertiary,
)
SpacerWMax()
AnimatedVisibility(visible = showLink) {
Row(
modifier = Modifier.clickable { onClick() },
verticalAlignment = Alignment.CenterVertically,
) {
Icon(
painter = painterResource(id = R.drawable.ic_arrow_top_right_24),
contentDescription = null,
tint = TangemTheme.colors.icon.informative,
modifier = Modifier
.size(TangemTheme.dimens.spacing16)
.padding(end = TangemTheme.dimens.spacing2),
)
Text(
text = stringResourceSafe(id = R.string.common_go_to_provider),
style = TangemTheme.typography.body2,
color = TangemTheme.colors.text.tertiary,
)
}
}
}
AnimatedContent(targetState = statuses.lastIndex, label = "Exchange Status List Change") {
Column {
statuses.forEachIndexed { index, item ->
ExchangeStatusStep(
stepStatus = item,
isLast = index == it,
)
}
}
}
}
}
@Composable
private fun ExchangeStatusStep(
stepStatus: ExchangeStatusState,
modifier: Modifier = Modifier,
isLast: Boolean = false,
) {
Row(modifier = modifier) {
Column(
horizontalAlignment = Alignment.CenterHorizontally,
) {
AnimatedContent(
targetState = stepStatus,
label = "Exchange Step Change Success",
modifier = Modifier
.size(TangemTheme.dimens.size20),
) { state ->
when {
state.status == ExchangeStatus.Cancelled -> {
ExchangeStep(
iconRes = R.drawable.ic_close_24,
color = TangemTheme.colors.icon.warning,
isDone = false,
)
}
state.status.isFailed() ||
state.status == ExchangeStatus.Refunded ||
state.status == ExchangeStatus.Paused
-> {
ExchangeStep(
iconRes = R.drawable.ic_close_24,
color = TangemTheme.colors.icon.warning,
isDone = state.isDone,
)
}
state.status == ExchangeStatus.Verifying -> ExchangeStep(
iconRes = R.drawable.ic_exclamation_24,
color = TangemTheme.colors.icon.attention,
isDone = state.isDone,
)
state.isDone -> ExchangeStep(
iconRes = R.drawable.ic_check_24,
color = TangemTheme.colors.icon.primary1,
isDone = true,
)
state.isActive -> ExchangeStepInProgress()
else -> ExchangeStepDefault()
}
}
if (!isLast) {
ExchangeStepSeparator()
}
}
ExchangeStatusStepText(stepStatus)
}
}
@Composable
private fun ExchangeStatusStepText(stepStatus: ExchangeStatusState) {
val status = stepStatus.status
val textColor = when {
status == ExchangeStatus.Cancelled || status == ExchangeStatus.Refunded || status == ExchangeStatus.Paused -> {
TangemTheme.colors.icon.warning
}
status.isFailed() && !stepStatus.isDone -> TangemTheme.colors.icon.warning
status == ExchangeStatus.Verifying && !stepStatus.isDone -> TangemTheme.colors.icon.attention
stepStatus.isDone -> TangemTheme.colors.text.primary1
!stepStatus.isActive -> TangemTheme.colors.text.disabled
else -> TangemTheme.colors.text.primary1
}
Text(
text = stepStatus.text.resolveReference(),
style = TangemTheme.typography.body2,
color = textColor,
modifier = Modifier
.padding(start = TangemTheme.dimens.spacing12),
)
}
@Composable
private fun ExchangeStepDefault() {
Box(
modifier = Modifier
.border(
width = TangemTheme.dimens.size1_5,
color = TangemTheme.colors.field.focused,
shape = CircleShape,
)
.padding(TangemTheme.dimens.spacing2),
)
}
@Composable
private fun ExchangeStep(color: Color, @DrawableRes iconRes: Int, isDone: Boolean) {
val (iconColor, borderColor) = if (isDone) {
TangemTheme.colors.icon.primary1 to TangemTheme.colors.field.focused
} else {
color to color
}
Icon(
painter = painterResource(id = iconRes),
contentDescription = null,
tint = iconColor,
modifier = Modifier
.border(
width = TangemTheme.dimens.size1_5,
color = borderColor,
shape = CircleShape,
)
.padding(TangemTheme.dimens.spacing2),
)
}
@Composable
private fun ExchangeStepInProgress() {
CircularProgressIndicator(
color = TangemTheme.colors.icon.primary1,
strokeWidth = TangemTheme.dimens.size2,
modifier = Modifier
.padding(TangemTheme.dimens.spacing2)
.size(TangemTheme.dimens.size14),
)
}
@Composable
private fun ExchangeStepSeparator() {
Box(
modifier = Modifier
.padding(vertical = TangemTheme.dimens.spacing2)
.size(
width = TangemTheme.dimens.size1_5,
height = TangemTheme.dimens.size10,
)
.background(
color = TangemTheme.colors.field.focused,
shape = CircleShape,
),
)
}
@Preview
@Composable
private fun Preview_ExchangeStatusBlock() {
val base = ExchangeStatusState(
status = ExchangeStatus.Failed,
text = resourceReference(id = R.string.express_exchange_status_failed),
isActive = true,
isDone = false,
)
TangemThemePreview {
ExchangeStatusBlock(
statuses = listOf(
base,
base.copy(isActive = false, isDone = false),
base.copy(isActive = true, isDone = false),
base.copy(isActive = true, isDone = true),
ExchangeStatusState(
status = ExchangeStatus.Paused,
text = resourceReference(id = R.string.express_exchange_status_paused),
isActive = true,
isDone = false,
),
)
.toImmutableList(),
showLink = false,
onClick = {},
)
}
}

View file

@ -14,7 +14,13 @@ import androidx.compose.ui.unit.dp
import com.tangem.common.ui.expressStatus.ExpressEstimate
import com.tangem.common.ui.expressStatus.ExpressHideButton
import com.tangem.common.ui.expressStatus.ExpressProvider
import com.tangem.common.ui.expressStatus.ExpressStatusBlock
import com.tangem.common.ui.expressStatus.state.ExpressLinkUM
import com.tangem.common.ui.expressStatus.state.ExpressStatusItemState
import com.tangem.common.ui.expressStatus.state.ExpressStatusItemUM
import com.tangem.common.ui.expressStatus.state.ExpressStatusUM
import com.tangem.core.ui.R
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.components.SpacerH
import com.tangem.core.ui.components.SpacerH10
import com.tangem.core.ui.components.SpacerH12
@ -27,7 +33,9 @@ import com.tangem.core.ui.res.TangemTheme
import com.tangem.feature.swap.domain.models.domain.ExchangeStatus
import com.tangem.feature.swap.domain.models.domain.ExchangeStatus.Companion.isFailed
import com.tangem.feature.tokendetails.presentation.tokendetails.state.components.ExchangeStatusNotification
import com.tangem.feature.tokendetails.presentation.tokendetails.state.express.ExchangeStatusState
import com.tangem.feature.tokendetails.presentation.tokendetails.state.express.ExchangeUM
import kotlinx.collections.immutable.toImmutableList
@Composable
internal fun ExchangeStatusBottomSheetContent(
@ -80,11 +88,7 @@ internal fun ExchangeStatusBottomSheetContent(
extraContent()
SpacerH12()
}
ExchangeStatusBlock(
statuses = state.statuses,
showLink = state.showProviderLink,
onClick = { state.info.onGoToProviderClick(state.info.txExternalUrl.orEmpty()) },
)
ExpressStatusBlock(state = state.toExpressStatusUM())
if (state.notification != null) {
Notification(state = state.notification, activeStatus = state.activeStatus)
}
@ -101,6 +105,34 @@ internal fun ExchangeStatusBottomSheetContent(
}
}
private fun ExchangeUM.toExpressStatusUM(): ExpressStatusUM = ExpressStatusUM(
title = resourceReference(R.string.express_exchange_status_title),
link = if (showProviderLink) {
ExpressLinkUM.Content(
icon = R.drawable.ic_arrow_top_right_24,
text = resourceReference(R.string.common_go_to_provider),
onClick = { info.onGoToProviderClick(info.txExternalUrl.orEmpty()) },
)
} else {
ExpressLinkUM.Empty
},
statuses = statuses.map { it.toExpressStatusItemUM() }.toImmutableList(),
)
private fun ExchangeStatusState.toExpressStatusItemUM(): ExpressStatusItemUM = ExpressStatusItemUM(
text = text,
state = when {
status == ExchangeStatus.Cancelled -> ExpressStatusItemState.Error
status.isFailed() || status == ExchangeStatus.Refunded || status == ExchangeStatus.Paused -> {
if (isDone) ExpressStatusItemState.Done else ExpressStatusItemState.Error
}
status == ExchangeStatus.Verifying -> ExpressStatusItemState.Warning
isDone -> ExpressStatusItemState.Done
isActive -> ExpressStatusItemState.Active
else -> ExpressStatusItemState.Default
},
)
@Composable
private fun Notification(state: ExchangeStatusNotification, activeStatus: ExchangeStatus?) {
AnimatedContent(

View file

@ -22,7 +22,7 @@ import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.domain.models.wallet.isLocked
import com.tangem.domain.models.wallet.isMultiCurrency
import com.tangem.domain.tokens.wallet.WalletBalanceFetcher
import com.tangem.domain.wallets.models.GetUserWalletError
import com.tangem.domain.wallets.models.errors.GetUserWalletError
import com.tangem.domain.wallets.usecase.GetSelectedWalletSyncUseCase
import com.tangem.domain.wallets.usecase.GetUserWalletUseCase
import com.tangem.domain.wallets.usecase.SelectWalletUseCase