Updated on 2026-08-14

This commit is contained in:
Tangem 2026-06-01 16:32:51 +02:00
parent f6f3aabbfc
commit cff379462f
12 changed files with 848 additions and 71 deletions

View file

@ -7,5 +7,6 @@ internal class DefaultTangemPayFeatureToggles(
private val featureTogglesManager: FeatureTogglesManager,
) : TangemPayFeatureToggles {
override val isRedesignEnabled: Boolean
get() = featureTogglesManager.isFeatureEnabled(FeatureToggles.AND_15368_VISA_PAY_REDESIGN)
get() = featureTogglesManager.isFeatureEnabled(FeatureToggles.AND_15368_VISA_PAY_REDESIGN) &&
featureTogglesManager.isFeatureEnabled(FeatureToggles.APP_REDESIGN_ENABLED)
}

View file

@ -5,9 +5,12 @@ import androidx.compose.runtime.getValue
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.tangem.core.decompose.context.AppComponentContext
import com.tangem.core.decompose.model.getOrCreateModel
import com.tangem.core.ui.res.LocalVisaRedesignEnabled
import com.tangem.features.tangempay.components.TangemPayTransactionBottomSheetComponent
import com.tangem.features.tangempay.entity.TangemPayTxHistoryDetailsUiState
import com.tangem.features.tangempay.model.TangemPayTxHistoryDetailsModel
import com.tangem.features.tangempay.ui.TangemPayTxHistoryDetailsContent
import com.tangem.features.tangempay.ui.TangemPayTxHistoryDetailsContentV2
import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
@ -25,8 +28,15 @@ internal class TangemPayTxHistoryDetailsComponent @AssistedInject constructor(
@Composable
override fun BottomSheet() {
val state by model.uiState.collectAsStateWithLifecycle()
TangemPayTxHistoryDetailsContent(state = state)
val states by model.uiState.collectAsStateWithLifecycle()
when (val uiState = states.toUiState(isRedesignEnabled = LocalVisaRedesignEnabled.current)) {
is TangemPayTxHistoryDetailsUiState.Legacy -> {
TangemPayTxHistoryDetailsContent(state = uiState.state)
}
is TangemPayTxHistoryDetailsUiState.Redesign -> {
TangemPayTxHistoryDetailsContentV2(state = uiState.state)
}
}
}
@AssistedFactory

View file

@ -1,7 +1,9 @@
package com.tangem.features.tangempay.entity
import androidx.compose.runtime.Immutable
import com.tangem.core.ui.components.label.entity.LabelUM
import com.tangem.core.ui.components.notifications.NotificationConfig
import com.tangem.core.ui.ds.image.TangemIconUM
import com.tangem.core.ui.extensions.ColorReference
import com.tangem.core.ui.extensions.ImageReference
import com.tangem.core.ui.extensions.TextReference
@ -28,6 +30,58 @@ internal data class TangemPayTxHistoryDetailsUM(
val iconTint: ColorReference,
val containerColor: ColorReference?,
)
}
data class ButtonState(val text: TextReference, val onClick: () -> Unit, val startIcon: ImageReference.Res? = null)
internal data class ButtonState(
val text: TextReference,
val onClick: () -> Unit,
val startIcon: ImageReference.Res? = null,
)
internal data class TangemPayTxHistoryDetailsUMV2(
val isBalanceHidden: Boolean,
val title: TextReference,
val subtitle: TextReference,
val iconState: TangemIconUM,
val transactionTitle: TextReference,
val transactionCategory: TextReference,
val mcc: TextReference?,
val transactionAmount: String,
val localTransactionText: String?,
val label: TransactionLabelUM?,
val buttonState: ButtonState,
val dismiss: () -> Unit,
)
internal data class TransactionLabelUM(
val transactionStateType: TransactionStateType,
val icon: TangemIconUM,
val title: TextReference,
val subtitle: TextReference? = null,
)
internal enum class TransactionStateType {
Completed,
InProgress,
Rejected,
Reversed,
}
@Immutable
internal sealed interface TangemPayTxHistoryDetailsUiState {
data class Legacy(val state: TangemPayTxHistoryDetailsUM) : TangemPayTxHistoryDetailsUiState
data class Redesign(val state: TangemPayTxHistoryDetailsUMV2) : TangemPayTxHistoryDetailsUiState
}
internal data class TangemPayTxHistoryDetailsUiStates(
val legacy: TangemPayTxHistoryDetailsUM,
val redesign: TangemPayTxHistoryDetailsUMV2,
) {
fun toUiState(isRedesignEnabled: Boolean): TangemPayTxHistoryDetailsUiState {
return if (isRedesignEnabled) {
TangemPayTxHistoryDetailsUiState.Redesign(redesign)
} else {
TangemPayTxHistoryDetailsUiState.Legacy(legacy)
}
}
}

View file

@ -21,6 +21,8 @@ import com.tangem.core.ui.format.bigdecimal.getJavaCurrencyByCode
import com.tangem.core.ui.format.bigdecimal.optionalDecimals
import com.tangem.core.ui.message.SnackbarMessage
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.generated.icons.Icons
import com.tangem.core.ui.res.generated.icons.ic_arrow_refresh_20
import com.tangem.core.ui.test.TangemPayTestTags
import com.tangem.domain.models.StatusSource
import com.tangem.domain.models.TokenReceiveConfig
@ -222,7 +224,7 @@ internal class TangemPayCardPageModel @Inject constructor(
title = TextReference.Res(R.string.tangempay_card_details_reissue_card),
onClick = ::onClickReissueCard,
icon = TangemIconUM.Icon(
iconRes = CoreUiR.drawable.ic_replace_20,
imageVector = Icons.ic_arrow_refresh_20,
tintReference = {
TangemTheme.colors3.icon.primary
},

View file

@ -14,8 +14,9 @@ import com.tangem.domain.feedback.SendFeedbackEmailUseCase
import com.tangem.domain.feedback.models.FeedbackEmailType
import com.tangem.domain.tangempay.TangemPayAnalyticsEvents
import com.tangem.features.tangempay.components.TangemPayTransactionBottomSheetComponent
import com.tangem.features.tangempay.entity.TangemPayTxHistoryDetailsUM
import com.tangem.features.tangempay.entity.TangemPayTxHistoryDetailsUiStates
import com.tangem.features.tangempay.model.transformers.TangemPayTxHistoryDetailsConverter
import com.tangem.features.tangempay.model.transformers.TangemPayTxHistoryDetailsConverterV2
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import kotlinx.coroutines.flow.*
import kotlinx.coroutines.launch
@ -35,18 +36,8 @@ internal class TangemPayTxHistoryDetailsModel @Inject constructor(
) : Model() {
private val params = paramsContainer.require<TangemPayTransactionBottomSheetComponent.Params>()
val uiState: StateFlow<TangemPayTxHistoryDetailsUM>
field = MutableStateFlow(
value = TangemPayTxHistoryDetailsConverter.convert(
value = TangemPayTxHistoryDetailsConverter.Input(
item = params.transaction,
isBalanceHidden = params.isBalanceHidden,
onExplorerClick = ::openExplorer,
onDisputeClick = { dispute(customerId = params.customerId) },
onDismiss = ::dismiss,
),
),
)
val uiState: StateFlow<TangemPayTxHistoryDetailsUiStates>
field = MutableStateFlow(buildUiStates(isBalanceHidden = params.isBalanceHidden))
init {
subscribeToBalanceHiding()
@ -58,10 +49,32 @@ internal class TangemPayTxHistoryDetailsModel @Inject constructor(
private fun subscribeToBalanceHiding() {
balanceHidingSettings.isBalanceHidden()
.onEach { isBalanceHidden -> uiState.update { it.copy(isBalanceHidden = isBalanceHidden) } }
.onEach { isBalanceHidden -> uiState.update { buildUiStates(isBalanceHidden) } }
.launchIn(modelScope)
}
private fun buildUiStates(isBalanceHidden: Boolean): TangemPayTxHistoryDetailsUiStates {
val converterInput = TangemPayTxHistoryDetailsConverter.Input(
item = params.transaction,
isBalanceHidden = isBalanceHidden,
onExplorerClick = ::openExplorer,
onDisputeClick = { dispute(customerId = params.customerId) },
onDismiss = ::dismiss,
)
return TangemPayTxHistoryDetailsUiStates(
legacy = TangemPayTxHistoryDetailsConverter.convert(converterInput),
redesign = TangemPayTxHistoryDetailsConverterV2.convert(
value = TangemPayTxHistoryDetailsConverterV2.Input(
item = converterInput.item,
isBalanceHidden = converterInput.isBalanceHidden,
onExplorerClick = converterInput.onExplorerClick,
onDisputeClick = converterInput.onDisputeClick,
onDismiss = converterInput.onDismiss,
),
),
)
}
private fun openExplorer(txHash: String?) {
txHash?.let(urlOpener::openUrlExternalBrowser)
}

View file

@ -12,8 +12,8 @@ import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.utils.DateTimeFormatters
import com.tangem.domain.visa.model.TangemPayTxHistoryItem
import com.tangem.features.tangempay.details.impl.R
import com.tangem.features.tangempay.entity.ButtonState
import com.tangem.features.tangempay.entity.TangemPayTxHistoryDetailsUM
import com.tangem.features.tangempay.entity.TangemPayTxHistoryDetailsUM.ButtonState
import com.tangem.utils.StringsSigns
import com.tangem.utils.converter.Converter
import com.tangem.utils.extensions.isPositive

View file

@ -0,0 +1,322 @@
package com.tangem.features.tangempay.model.transformers
import com.tangem.core.ui.ds.image.TangemIconUM
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.extensions.stringReference
import com.tangem.core.ui.extensions.wrappedList
import com.tangem.core.ui.format.bigdecimal.fiat
import com.tangem.core.ui.format.bigdecimal.format
import com.tangem.core.ui.format.bigdecimal.price
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.generated.icons.*
import com.tangem.core.ui.utils.DateTimeFormatters
import com.tangem.domain.visa.model.TangemPayTxHistoryItem
import com.tangem.features.tangempay.details.impl.R
import com.tangem.features.tangempay.entity.ButtonState
import com.tangem.features.tangempay.entity.TangemPayTxHistoryDetailsUMV2
import com.tangem.features.tangempay.entity.TransactionLabelUM
import com.tangem.features.tangempay.entity.TransactionStateType
import com.tangem.utils.StringsSigns
import com.tangem.utils.converter.Converter
import com.tangem.utils.extensions.isPositive
import com.tangem.utils.extensions.isZero
internal object TangemPayTxHistoryDetailsConverterV2 :
Converter<TangemPayTxHistoryDetailsConverterV2.Input, TangemPayTxHistoryDetailsUMV2> {
private val dateFormatter = DateTimeFormatters.getBestFormatterBySkeleton("MMM dd yyyy, HH:mm")
override fun convert(value: Input): TangemPayTxHistoryDetailsUMV2 {
val transaction = value.item
return TangemPayTxHistoryDetailsUMV2(
isBalanceHidden = value.isBalanceHidden,
title = transaction.extractBottomSheetTitle(),
subtitle = transaction.extractDate(),
iconState = transaction.extractIcon(),
transactionTitle = transaction.extractTransactionTitle(),
transactionCategory = transaction.extractTransactionCategory(),
mcc = transaction.extractMcc(),
transactionAmount = transaction.extractAmount(),
localTransactionText = transaction.extractLocalAmount(),
label = transaction.extractTransactionLabel(),
buttonState = value.extractButtonState(),
dismiss = value.onDismiss,
)
}
private fun TangemPayTxHistoryItem.extractDate(): TextReference {
val date = DateTimeFormatters.formatDate(this.date, dateFormatter)
return stringReference(date)
}
private fun TangemPayTxHistoryItem.extractBottomSheetTitle(): TextReference {
return when (this) {
is TangemPayTxHistoryItem.Spend -> resourceReference(R.string.tangem_pay_purchase)
is TangemPayTxHistoryItem.Payment -> resourceReference(R.string.tangem_pay_withdrawal)
is TangemPayTxHistoryItem.Collateral -> when (this.type) {
TangemPayTxHistoryItem.Type.Deposit -> resourceReference(R.string.tangem_pay_deposit)
TangemPayTxHistoryItem.Type.Withdrawal -> resourceReference(R.string.tangem_pay_withdrawal)
}
is TangemPayTxHistoryItem.Fee -> resourceReference(R.string.tangem_pay_fee_title)
}
}
private fun TangemPayTxHistoryItem.extractIcon(): TangemIconUM {
return when (this) {
is TangemPayTxHistoryItem.Collateral -> when (this.type) {
TangemPayTxHistoryItem.Type.Deposit -> TangemIconUM.Icon(
imageVector = Icons.ic_arrow_down_24,
tintReference = {
TangemTheme.colors3.icon.secondary
},
)
TangemPayTxHistoryItem.Type.Withdrawal -> TangemIconUM.Icon(
imageVector = Icons.ic_arrow_up_24,
tintReference = {
TangemTheme.colors3.icon.secondary
},
)
}
is TangemPayTxHistoryItem.Fee -> {
TangemIconUM.Icon(
iconRes = R.drawable.ic_percent_24,
tintReference = {
TangemTheme.colors3.icon.secondary
},
)
}
is TangemPayTxHistoryItem.Payment -> TangemIconUM.Icon(
imageVector = Icons.ic_arrow_up_24,
tintReference = {
TangemTheme.colors3.icon.secondary
},
)
is TangemPayTxHistoryItem.Spend -> {
val merchantIcon = this.enrichedMerchantIconUrl
if (merchantIcon != null) {
TangemIconUM.Url(merchantIcon, R.drawable.ic_category_24)
} else {
TangemIconUM.Icon(
iconRes = R.drawable.ic_category_24,
tintReference = {
TangemTheme.colors3.icon.secondary
},
)
}
}
}
}
private fun TangemPayTxHistoryItem.extractTransactionTitle(): TextReference {
return when (this) {
is TangemPayTxHistoryItem.Spend -> stringReference(this.enrichedMerchantName ?: this.merchantName)
is TangemPayTxHistoryItem.Payment -> resourceReference(R.string.common_transfer)
is TangemPayTxHistoryItem.Collateral -> resourceReference(R.string.common_transfer)
is TangemPayTxHistoryItem.Fee -> {
this.description?.let(::stringReference) ?: resourceReference(R.string.tangem_pay_fee_subtitle)
}
}
}
private fun TangemPayTxHistoryItem.extractTransactionCategory(): TextReference {
return when (this) {
is TangemPayTxHistoryItem.Fee -> resourceReference(R.string.tangem_pay_fee_subtitle)
is TangemPayTxHistoryItem.Payment -> resourceReference(R.string.common_transfer)
is TangemPayTxHistoryItem.Collateral -> resourceReference(R.string.common_transfer)
is TangemPayTxHistoryItem.Spend -> extractSpendCategory()
}
}
private fun TangemPayTxHistoryItem.Spend.extractSpendCategory(): TextReference {
val merchantCategory = merchantCategory
val enrichedMerchantCategory = enrichedMerchantCategory
return when {
!merchantCategory.isNullOrEmpty() -> stringReference(merchantCategory)
!enrichedMerchantCategory.isNullOrEmpty() -> stringReference(enrichedMerchantCategory)
else -> resourceReference(R.string.tangem_pay_other)
}
}
private fun TangemPayTxHistoryItem.extractMcc(): TextReference? {
return when (this) {
is TangemPayTxHistoryItem.Spend -> merchantCategoryCode
?.takeIf { it.isNotEmpty() }
?.let(::stringReference)
is TangemPayTxHistoryItem.Fee,
is TangemPayTxHistoryItem.Payment,
is TangemPayTxHistoryItem.Collateral,
-> null
}
}
private fun TangemPayTxHistoryItem.extractAmount(): String {
return when (this) {
is TangemPayTxHistoryItem.Fee,
is TangemPayTxHistoryItem.Payment,
-> {
val amount = this.amount.format {
fiat(
fiatCurrencyCode = this@extractAmount.currency.currencyCode,
fiatCurrencySymbol = this@extractAmount.currency.symbol,
)
}
StringsSigns.MINUS + amount
}
is TangemPayTxHistoryItem.Spend -> {
val amountPrefix = when {
this.amount.isZero() -> ""
this.status == TangemPayTxHistoryItem.Status.REVERSED -> StringsSigns.MINUS
this.status == TangemPayTxHistoryItem.Status.DECLINED ||
this.amount.isPositive() -> StringsSigns.MINUS
else -> StringsSigns.PLUS
}
val amount = when (this.status) {
TangemPayTxHistoryItem.Status.DECLINED -> this.authorizedAmount
else -> this.amount
}
val formattedAmount = amount.abs().format {
fiat(
fiatCurrencyCode = this@extractAmount.currency.currencyCode,
fiatCurrencySymbol = this@extractAmount.currency.symbol,
)
}
amountPrefix + formattedAmount
}
is TangemPayTxHistoryItem.Collateral -> {
val amountPrefix = when {
this.amount.isZero() -> ""
this.amount.isPositive() -> StringsSigns.PLUS
else -> StringsSigns.MINUS
}
val amount = this.amount.abs().format {
fiat(
fiatCurrencyCode = this@extractAmount.currency.currencyCode,
fiatCurrencySymbol = this@extractAmount.currency.symbol,
)
}
amountPrefix + amount
}
}
}
private fun TangemPayTxHistoryItem.extractLocalAmount(): String? {
return when (this) {
is TangemPayTxHistoryItem.Collateral,
is TangemPayTxHistoryItem.Fee,
is TangemPayTxHistoryItem.Payment,
-> null
is TangemPayTxHistoryItem.Spend -> {
val localCurrency = this.localCurrency
val localAmount = this.localAmount
if (localCurrency != null && localAmount != null && localCurrency != currency) {
val amountPrefix = when {
localAmount.isZero() -> ""
this.status == TangemPayTxHistoryItem.Status.DECLINED ||
localAmount.isPositive() -> StringsSigns.MINUS
else -> StringsSigns.PLUS
}
val amount = localAmount.abs().format {
fiat(
fiatCurrencyCode = localCurrency.currencyCode,
fiatCurrencySymbol = localCurrency.symbol,
).price()
}
amountPrefix + amount
} else {
null
}
}
}
}
private fun TangemPayTxHistoryItem.extractTransactionLabel(): TransactionLabelUM? {
return when (this) {
is TangemPayTxHistoryItem.Payment,
is TangemPayTxHistoryItem.Collateral,
-> {
TransactionLabelUM(
transactionStateType = TransactionStateType.Completed,
icon = TangemIconUM.Icon(
imageVector = Icons.ic_success_24,
tintReference = { TangemTheme.colors3.icon.status.success },
),
title = resourceReference(R.string.tangem_pay_status_completed),
)
}
is TangemPayTxHistoryItem.Fee -> TransactionLabelUM(
transactionStateType = TransactionStateType.Completed,
icon = TangemIconUM.Icon(
imageVector = Icons.ic_success_24,
tintReference = { TangemTheme.colors3.icon.status.success },
),
title = resourceReference(R.string.tangem_pay_status_completed),
subtitle = resourceReference(R.string.tangem_pay_transaction_fee_notification_text),
)
is TangemPayTxHistoryItem.Spend -> when (this.status) {
TangemPayTxHistoryItem.Status.COMPLETED -> TransactionLabelUM(
transactionStateType = TransactionStateType.Completed,
icon = TangemIconUM.Icon(
imageVector = Icons.ic_success_24,
tintReference = { TangemTheme.colors3.icon.status.success },
),
title = resourceReference(R.string.tangem_pay_status_completed),
)
TangemPayTxHistoryItem.Status.PENDING,
TangemPayTxHistoryItem.Status.RESERVED,
-> TransactionLabelUM(
transactionStateType = TransactionStateType.InProgress,
icon = TangemIconUM.Icon(
imageVector = Icons.ic_clock_24,
tintReference = { TangemTheme.colors3.icon.status.info },
),
title = resourceReference(R.string.tangem_pay_status_pending),
)
TangemPayTxHistoryItem.Status.DECLINED -> TransactionLabelUM(
transactionStateType = TransactionStateType.Rejected,
icon = TangemIconUM.Icon(
iconRes = R.drawable.ic_warning_20,
tintReference = { TangemTheme.colors3.icon.status.error },
),
title = resourceReference(R.string.tangem_pay_status_declined),
subtitle = extractDeclinedSubtitle(),
)
TangemPayTxHistoryItem.Status.REVERSED -> TransactionLabelUM(
transactionStateType = TransactionStateType.Reversed,
icon = TangemIconUM.Icon(
imageVector = Icons.ic_success_24,
tintReference = { TangemTheme.colors3.icon.status.success },
),
title = resourceReference(R.string.tangem_pay_status_reversed),
subtitle = resourceReference(R.string.tangem_pay_transaction_reversed_notification_text),
)
TangemPayTxHistoryItem.Status.UNKNOWN -> null
}
}
}
private fun TangemPayTxHistoryItem.Spend.extractDeclinedSubtitle(): TextReference {
return if (declinedReason.isNullOrEmpty()) {
resourceReference(R.string.tangem_pay_transaction_declined_notification_text)
} else {
resourceReference(
id = R.string.tangem_pay_history_item_spend_mc_declined_reason,
formatArgs = wrappedList(requireNotNull(declinedReason)),
)
}
}
private fun Input.extractButtonState(): ButtonState {
return ButtonState(
text = resourceReference(R.string.tangem_pay_get_help),
onClick = this.onDisputeClick,
)
}
data class Input(
val item: TangemPayTxHistoryItem,
val isBalanceHidden: Boolean,
val onExplorerClick: (String?) -> Unit,
val onDisputeClick: () -> Unit,
val onDismiss: () -> Unit,
)
}

View file

@ -30,6 +30,7 @@ import com.tangem.core.ui.res.TangemColorPalette
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreview
import com.tangem.features.tangempay.details.impl.R
import com.tangem.features.tangempay.entity.ButtonState
import com.tangem.features.tangempay.entity.TangemPayTxHistoryDetailsUM
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.persistentListOf
@ -115,10 +116,7 @@ internal fun TangemPayTxHistoryDetailsContent(state: TangemPayTxHistoryDetailsUM
}
@Composable
private fun ButtonsContainer(
buttons: ImmutableList<TangemPayTxHistoryDetailsUM.ButtonState>,
modifier: Modifier = Modifier,
) {
private fun ButtonsContainer(buttons: ImmutableList<ButtonState>, modifier: Modifier = Modifier) {
Column(
modifier = modifier,
verticalArrangement = Arrangement.spacedBy(8.dp),
@ -179,7 +177,7 @@ private class TangemPayTxHistoryDetailsUMProvider : CollectionPreviewParameterPr
),
notification = null,
buttons = persistentListOf(
TangemPayTxHistoryDetailsUM.ButtonState(
ButtonState(
text = resourceReference(R.string.tangem_pay_dispute),
onClick = {},
),
@ -210,7 +208,7 @@ private class TangemPayTxHistoryDetailsUMProvider : CollectionPreviewParameterPr
containerColor = themedColor { TangemColorPalette.Amaranth.copy(alpha = 0.1F) },
),
buttons = persistentListOf(
TangemPayTxHistoryDetailsUM.ButtonState(
ButtonState(
text = resourceReference(R.string.tangem_pay_dispute),
onClick = {},
),
@ -232,7 +230,7 @@ private class TangemPayTxHistoryDetailsUMProvider : CollectionPreviewParameterPr
),
notification = null,
buttons = persistentListOf(
TangemPayTxHistoryDetailsUM.ButtonState(
ButtonState(
text = resourceReference(R.string.tangem_pay_dispute),
onClick = {},
),
@ -260,7 +258,7 @@ private class TangemPayTxHistoryDetailsUMProvider : CollectionPreviewParameterPr
containerColor = null,
),
buttons = persistentListOf(
TangemPayTxHistoryDetailsUM.ButtonState(
ButtonState(
text = resourceReference(R.string.tangem_pay_dispute),
onClick = {},
),
@ -279,7 +277,7 @@ private class TangemPayTxHistoryDetailsUMProvider : CollectionPreviewParameterPr
labelState = null,
notification = null,
buttons = persistentListOf(
TangemPayTxHistoryDetailsUM.ButtonState(
ButtonState(
text = resourceReference(R.string.tangem_pay_get_help),
onClick = {},
),
@ -298,7 +296,7 @@ private class TangemPayTxHistoryDetailsUMProvider : CollectionPreviewParameterPr
labelState = null,
notification = null,
buttons = persistentListOf(
TangemPayTxHistoryDetailsUM.ButtonState(
ButtonState(
text = resourceReference(R.string.tangem_pay_get_help),
onClick = {},
),

View file

@ -0,0 +1,389 @@
package com.tangem.features.tangempay.ui
import android.content.res.Configuration
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.text.TextAutoSize
import androidx.compose.foundation.verticalScroll
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.text.style.TextAlign
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.tooling.preview.Devices
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider
import com.tangem.core.ui.components.SpacerW
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheet
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfigContent
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetType
import com.tangem.core.ui.ds.image.TangemIcon
import com.tangem.core.ui.ds.image.TangemIconUM
import com.tangem.core.ui.ds.topbar.TangemTopBar
import com.tangem.core.ui.ds.topbar.TangemTopBarType
import com.tangem.core.ui.ds2.button.TangemButton
import com.tangem.core.ui.ds2.row.TangemRow
import com.tangem.core.ui.ds2.row.TangemRowContentLead
import com.tangem.core.ui.ds2.row.TangemRowText
import com.tangem.core.ui.ds2.row.TangemRowTextRole
import com.tangem.core.ui.extensions.orMaskWithStars
import com.tangem.core.ui.extensions.resolveReference
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.extensions.stringReference
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreviewRedesign
import com.tangem.core.ui.res.generated.icons.Icons
import com.tangem.core.ui.res.generated.icons.ic_arrow_down_24
import com.tangem.features.tangempay.details.impl.R
import com.tangem.features.tangempay.entity.ButtonState
import com.tangem.features.tangempay.entity.TangemPayTxHistoryDetailsUMV2
import com.tangem.features.tangempay.entity.TransactionLabelUM
import com.tangem.features.tangempay.entity.TransactionStateType
@Suppress("LongMethod")
@Composable
internal fun TangemPayTxHistoryDetailsContentV2(state: TangemPayTxHistoryDetailsUMV2) {
TangemBottomSheet<TangemBottomSheetConfigContent.Empty>(
config = TangemBottomSheetConfig(
isShown = true,
onDismissRequest = state.dismiss,
content = TangemBottomSheetConfigContent.Empty,
),
type = TangemBottomSheetType.Modal,
containerColor = TangemTheme.colors3.bg.secondary,
title = {
TangemTopBar(
title = state.title,
subtitle = state.subtitle,
type = TangemTopBarType.BottomSheet,
endContent = {
TangemButton(
iconStart = TangemIconUM.Icon(iconRes = R.drawable.ic_close_24),
onClick = state.dismiss,
size = TangemButton.Size.X11,
variant = TangemButton.Variant.Material,
)
},
)
},
content = {
Column(
modifier = Modifier
.fillMaxWidth()
.verticalScroll(rememberScrollState())
.padding(horizontal = TangemTheme.dimens2.x4)
.padding(bottom = TangemTheme.dimens2.x4),
horizontalAlignment = Alignment.CenterHorizontally,
) {
TransactionIcon(
iconState = state.iconState,
modifier = Modifier.padding(top = TangemTheme.dimens2.x12),
)
Text(
modifier = Modifier.padding(top = TangemTheme.dimens2.x6),
text = state.transactionAmount.orMaskWithStars(state.isBalanceHidden),
style = TangemTheme.typography3.display.medium,
color = TangemTheme.colors3.text.primary,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
autoSize = TextAutoSize.StepBased(
minFontSize = TangemTheme.typography3.body.medium.fontSize,
maxFontSize = TangemTheme.typography3.display.medium.fontSize,
),
)
TransactionSecondaryLine(
state = state,
modifier = Modifier.padding(top = TangemTheme.dimens2.x1),
)
if (state.label != null) {
TransactionLabel(
label = state.label,
modifier = Modifier
.padding(top = TangemTheme.dimens2.x12)
.fillMaxWidth(),
)
}
TransactionDetailsBlock(
state = state,
modifier = Modifier
.padding(top = TangemTheme.dimens2.x4, bottom = TangemTheme.dimens2.x2)
.fillMaxWidth(),
)
TangemButton(
modifier = Modifier
.padding(vertical = TangemTheme.dimens2.x4)
.fillMaxWidth(),
text = state.buttonState.text,
onClick = state.buttonState.onClick,
size = TangemButton.Size.X12,
)
}
},
)
}
@Composable
private fun TransactionIcon(iconState: TangemIconUM, modifier: Modifier = Modifier) {
Box(
modifier = modifier
.size(TangemTheme.dimens2.x20)
.clip(CircleShape)
.background(TangemTheme.colors3.bg.opaque.primary),
contentAlignment = Alignment.Center,
) {
TangemIcon(
tangemIconUM = iconState,
modifier = Modifier.size(
if (iconState is TangemIconUM.Icon) {
TangemTheme.dimens2.x12
} else {
TangemTheme.dimens2.x20
},
),
)
}
}
@Composable
private fun TransactionSecondaryLine(state: TangemPayTxHistoryDetailsUMV2, modifier: Modifier = Modifier) {
val secondaryText = when {
state.localTransactionText != null -> {
buildString {
append(state.localTransactionText.orMaskWithStars(state.isBalanceHidden))
append(" · ")
append(state.transactionTitle.resolveReference())
}
}
else -> state.transactionTitle.resolveReference()
}
Text(
modifier = modifier,
text = secondaryText,
style = TangemTheme.typography3.subheading.medium,
color = TangemTheme.colors3.text.secondary,
textAlign = TextAlign.Center,
maxLines = 2,
overflow = TextOverflow.Ellipsis,
)
}
@Composable
internal fun TransactionLabel(label: TransactionLabelUM, modifier: Modifier = Modifier) {
val (backgroundColor, textColor) = when (label.transactionStateType) {
TransactionStateType.Completed,
TransactionStateType.Reversed,
-> {
TangemTheme.colors3.bg.status.successSubtle to TangemTheme.colors3.text.status.success
}
TransactionStateType.InProgress -> {
TangemTheme.colors3.bg.status.infoSubtle to TangemTheme.colors3.text.status.info
}
TransactionStateType.Rejected -> {
TangemTheme.colors3.bg.status.errorSubtle to TangemTheme.colors3.text.status.error
}
}
Row(
modifier = modifier
.background(
color = backgroundColor,
shape = RoundedCornerShape(TangemTheme.dimens3.borderRadius.b250),
)
.padding(
vertical = TangemTheme.dimens2.x3,
horizontal = TangemTheme.dimens2.x4,
),
) {
Column(
modifier = Modifier.weight(1f),
verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens2.x1),
) {
Text(
text = label.title.resolveReference(),
style = TangemTheme.typography3.body.medium,
color = textColor,
)
label.subtitle?.let { text ->
Text(
text = text.resolveReference(),
style = TangemTheme.typography3.caption.medium,
color = textColor,
)
}
}
SpacerW(TangemTheme.dimens2.x1)
TangemIcon(
tangemIconUM = label.icon,
modifier = Modifier.size(TangemTheme.dimens2.x5),
)
}
}
@Composable
private fun TransactionDetailsBlock(state: TangemPayTxHistoryDetailsUMV2, modifier: Modifier = Modifier) {
Column(modifier = modifier) {
TangemRow(
divider = state.mcc != null,
contentLead = TangemRowContentLead.Start,
titleSlot = {
TangemRowText(
text = resourceReference(R.string.tangem_pay_transaction_details_category),
role = TangemRowTextRole.Title,
)
},
valueSlot = {
TangemRowText(
text = state.transactionCategory,
role = TangemRowTextRole.Value,
)
},
)
if (state.mcc != null) {
TangemRow(
contentLead = TangemRowContentLead.Start,
titleSlot = {
TangemRowText(
text = resourceReference(R.string.tangem_pay_transaction_details_mcc),
role = TangemRowTextRole.Title,
)
},
valueSlot = {
TangemRowText(
text = state.mcc,
role = TangemRowTextRole.Value,
)
},
)
}
}
}
@Preview(device = Devices.PIXEL_7_PRO)
@Preview(device = Devices.PIXEL_7_PRO, uiMode = Configuration.UI_MODE_NIGHT_YES)
@Composable
private fun TangemPayTxHistoryDetailsContentPreview(
@PreviewParameter(TangemPayTxHistoryDetailsUMProviderV2::class) state: TangemPayTxHistoryDetailsUMV2,
) {
TangemThemePreviewRedesign {
TangemPayTxHistoryDetailsContentV2(state = state)
}
}
private class TangemPayTxHistoryDetailsUMProviderV2 :
CollectionPreviewParameterProvider<TangemPayTxHistoryDetailsUMV2>(
listOf(
TangemPayTxHistoryDetailsUMV2(
isBalanceHidden = true,
title = resourceReference(R.string.tangem_pay_purchase),
subtitle = stringReference("12 June 2026, 12:40"),
iconState = TangemIconUM.Icon(iconRes = R.drawable.ic_category_24),
transactionTitle = stringReference("Starbucks"),
transactionCategory = stringReference("Food and drinks"),
mcc = stringReference("5814"),
transactionAmount = "-$5.86",
localTransactionText = null,
label = TransactionLabelUM(
transactionStateType = TransactionStateType.InProgress,
icon = TangemIconUM.Icon(iconRes = com.tangem.core.ui.R.drawable.ic_clock_24),
title = resourceReference(R.string.tangem_pay_status_pending),
),
buttonState = ButtonState(
text = resourceReference(R.string.tangem_pay_get_help),
onClick = {},
),
dismiss = {},
),
TangemPayTxHistoryDetailsUMV2(
isBalanceHidden = true,
title = resourceReference(R.string.tangem_pay_purchase),
subtitle = stringReference("12 June 2026, 12:40"),
iconState = TangemIconUM.Icon(iconRes = R.drawable.ic_category_24),
transactionTitle = stringReference("NuCaloric"),
transactionCategory = stringReference("Groceries"),
mcc = stringReference("0000"),
transactionAmount = "-$820.52",
localTransactionText = "-€696,52",
label = TransactionLabelUM(
transactionStateType = TransactionStateType.Rejected,
icon = TangemIconUM.Icon(iconRes = R.drawable.ic_token_info_24),
title = resourceReference(R.string.tangem_pay_status_declined),
subtitle = stringReference("Reason: account credit limit exceeded"),
),
buttonState = ButtonState(
text = resourceReference(R.string.tangem_pay_get_help),
onClick = {},
),
dismiss = {},
),
TangemPayTxHistoryDetailsUMV2(
isBalanceHidden = true,
title = resourceReference(R.string.tangem_pay_purchase),
subtitle = stringReference("12 June 2026, 12:40"),
iconState = TangemIconUM.Icon(iconRes = R.drawable.ic_category_24),
transactionTitle = stringReference("Starbucks"),
transactionCategory = stringReference("Food and drinks"),
mcc = null,
transactionAmount = "-$5.86",
localTransactionText = "€ 5.36",
label = TransactionLabelUM(
transactionStateType = TransactionStateType.Completed,
icon = TangemIconUM.Empty,
title = resourceReference(R.string.tangem_pay_status_completed),
),
buttonState = ButtonState(
text = resourceReference(R.string.tangem_pay_get_help),
onClick = {},
),
dismiss = {},
),
TangemPayTxHistoryDetailsUMV2(
isBalanceHidden = false,
title = resourceReference(R.string.tangem_pay_fee_title),
subtitle = stringReference("12 June 2026, 12:40"),
iconState = TangemIconUM.Icon(iconRes = R.drawable.ic_percent_24),
transactionTitle = stringReference("Service fees"),
transactionCategory = stringReference("Service fees"),
mcc = null,
transactionAmount = "-$5.86",
localTransactionText = null,
label = TransactionLabelUM(
transactionStateType = TransactionStateType.Completed,
icon = TangemIconUM.Icon(iconRes = R.drawable.ic_token_info_24),
title = resourceReference(R.string.tangem_pay_fee_title),
subtitle = resourceReference(R.string.tangem_pay_transaction_fee_notification_text),
),
buttonState = ButtonState(
text = resourceReference(R.string.tangem_pay_get_help),
onClick = {},
),
dismiss = {},
),
TangemPayTxHistoryDetailsUMV2(
isBalanceHidden = false,
title = resourceReference(R.string.tangem_pay_deposit),
subtitle = stringReference("12 June 2026, 12:40"),
iconState = TangemIconUM.Icon(imageVector = Icons.ic_arrow_down_24),
transactionTitle = resourceReference(R.string.common_transfer),
transactionCategory = resourceReference(R.string.common_transfer),
mcc = null,
transactionAmount = "+$20",
localTransactionText = null,
label = null,
buttonState = ButtonState(
text = resourceReference(R.string.tangem_pay_get_help),
onClick = {},
),
dismiss = {},
),
),
)