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

@ -1677,7 +1677,7 @@
<string name="tangem_pay_card_details_unable_to_rename_card_title">Unable to rename card</string>
<string name="tangem_pay_card_frozen">Card frozen</string>
<string name="tangem_pay_card_payment">Card payment</string>
<string name="tangem_pay_close_card_popup_description">It will disappear from payment account</string>
<string name="tangem_pay_close_card_popup_description">It will disappear from the app</string>
<string name="tangem_pay_close_card_popup_primary_button_title">Close card</string>
<string name="tangem_pay_close_card_popup_secondary_button_title">Go back</string>
<string name="tangem_pay_close_card_popup_title">Close your card?</string>
@ -1698,6 +1698,7 @@
<string name="tangem_pay_history_item_spend_mcc">MCC %s</string>
<string name="tangem_pay_other">Other</string>
<string name="tangem_pay_pin_code_title">PIN-code</string>
<string name="tangem_pay_purchase">Purchase</string>
<string name="tangem_pay_rooted_device_subtitle">Unable to use on rooted devices</string>
<string name="tangem_pay_status_completed">Completed</string>
<string name="tangem_pay_status_declined">Declined</string>
@ -1706,6 +1707,8 @@
<string name="tangem_pay_terms_fees_limits">Terms, Fees &amp; Limits</string>
<string name="tangem_pay_terms_limits">Terms and fees</string>
<string name="tangem_pay_transaction_declined_notification_text">The bank rejected this transaction request.</string>
<string name="tangem_pay_transaction_details_category">Category</string>
<string name="tangem_pay_transaction_details_mcc">MCC</string>
<string name="tangem_pay_transaction_fee_notification_text">A fee is charged in accordance with the service tariffs</string>
<string name="tangem_pay_transaction_reversed_notification_text">The transaction was partially or fully reversed by the merchant</string>
<string name="tangem_pay_unfreeze_card_alert_body">Keep using your money. You can freeze anytime.</string>
@ -1824,6 +1827,24 @@
<string name="tangempay_kyc_rejected_description_span">your profile.</string>
<string name="tangempay_maximum_cards_issued_description">You can have up to 3 cards. Delete one to add a new card.</string>
<string name="tangempay_maximum_cards_issued_title">Maximum Cards Issued</string>
<string name="tangempay_newonboard_Q1_body">Yes to operate a regulated Visa card, identity verification is mandatory. KYC is handled by Sumsub (compliance partner).</string>
<string name="tangempay_newonboard_Q1_title">Do I have to share my docs?</string>
<string name="tangempay_newonboard_Q2_body">No. KYC applies only to the Tangem Pay account. Your Tangem Wallet itself remains a separate, self-custodial, non-KYC environment.</string>
<string name="tangempay_newonboard_Q2_title">Does the KYC associate with my wallet?</string>
<string name="tangempay_newonboard_Q3_body">Sumsub a globally regulated KYC provider, trusted by 4,000+ financial institutions verifies your identity and securely stores the results under ISO 27001 and SOC 2 standards.</string>
<string name="tangempay_newonboard_Q3_title">Who stores my personal data and how is it protected?</string>
<string name="tangempay_newonboard_Q4_body">Card balance nominated in USDC on Polygon, but you can use any asset (USDT, SOL, ETH, BTC, XRP etc.) to fund it using Tangem\'s convenient built-in swap mechanisms.</string>
<string name="tangempay_newonboard_Q4_title">What crypto can I spend?</string>
<string name="tangempay_newonboard_body">Spend crypto anywhere — no banks, no middlemen, no exchanges. The power of self-custody meets everyday payments.</string>
<string name="tangempay_newonboard_bottomleft_body">Buy online and via Apple Pay</string>
<string name="tangempay_newonboard_bottomleft_title">Accepted worldwide</string>
<string name="tangempay_newonboard_bottomright_body">Use anywhere in the world</string>
<string name="tangempay_newonboard_bottomright_title">FX fee is just 1%</string>
<string name="tangempay_newonboard_title">Get your Tangem Pay Card</string>
<string name="tangempay_newonboard_topleft_body">Pay what you see</string>
<string name="tangempay_newonboard_topleft_title">No purchase fees, \n1 USDC = 1 USD</string>
<string name="tangempay_newonboard_topright_body">No surprises</string>
<string name="tangempay_newonboard_topright_title">$0 monthly fee\n$0 topup fee</string>
<string name="tangempay_onboarding_banner_description">Get your free Tangem Visa virtual card</string>
<string name="tangempay_onboarding_banner_title">Use USDC for everyday payments</string>
<string name="tangempay_onboarding_get_card_button_text">Get card</string>
@ -1852,16 +1873,16 @@
<string name="tangempay_reissue_card_title">Replace your card?</string>
<string name="tangempay_service_unavailable_description">Were fixing a technical issue. Please try again later.</string>
<string name="tangempay_service_unavailable_title">Service temporarily unavailable</string>
<string name="tangempay_service_unreachable_try_later">Unable to display details. However, card payments are still working.</string>
<string name="tangempay_service_unreachable_try_later">The service is currently unreachable. Please try again later.</string>
<string name="tangempay_set_pin_code">Set \nPIN code</string>
<string name="tangempay_status_deactivated">Card deactivated</string>
<string name="tangempay_status_deactivated">Account closed</string>
<string name="tangempay_status_replacing">Replacing your card</string>
<string name="tangempay_sync_needed">Use your card or ring to renew session</string>
<string name="tangempay_sync_needed_body">Use your card or ring to renew session</string>
<string name="tangempay_sync_needed_button">Renew session</string>
<string name="tangempay_sync_needed_title">Payment account session expired</string>
<string name="tangempay_tangem_visa_card">Use USDC for everyday payments</string>
<string name="tangempay_temporarily_unavailable">Tangem Pay is temporarily unreachable</string>
<string name="tangempay_temporarily_unavailable">Tangem Pay is temporarily unavailable</string>
<string name="tangempay_title">Tangem Pay</string>
<string name="tangempay_topup_receive_body">Send USDC Polygon to your accounts address </string>
<string name="tangempay_topup_receive_title">From another wallet or exchange</string>

View file

@ -1,21 +1,14 @@
package com.tangem.core.ui.ds2.surface
import androidx.compose.foundation.BorderStroke
import androidx.compose.foundation.LocalIndication
import androidx.compose.foundation.background
import androidx.compose.foundation.border
import androidx.compose.foundation.clickable
import androidx.compose.foundation.*
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.ripple.RippleAlpha
import androidx.compose.material3.LocalRippleConfiguration
import androidx.compose.material3.RippleConfiguration
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.NonRestartableComposable
import androidx.compose.runtime.ReadOnlyComposable
import androidx.compose.runtime.remember
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.geometry.Offset
@ -94,6 +87,7 @@ fun TangemSurface(
onClick = onClick!!,
)
},
contentAlignment = Alignment.Center,
) {
content()
}

View file

@ -1,27 +0,0 @@
<!--
~ Copyright (C) 2026 The Android Open Source Project
~
~ Licensed under the Apache License, Version 2.0 (the "License");
~ you may not use this file except in compliance with the License.
~ You may obtain a copy of the License at
~
~ http://www.apache.org/licenses/LICENSE-2.0
~
~ Unless required by applicable law or agreed to in writing, software
~ distributed under the License is distributed on an "AS IS" BASIS,
~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
~ See the License for the specific language governing permissions and
~ limitations under the License.
-->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="20dp"
android:height="20dp"
android:viewportWidth="20"
android:viewportHeight="20">
<path
android:pathData="M15.75,9.25C16.164,9.25 16.5,9.586 16.5,10C16.5,13.59 13.59,16.5 10,16.5C7.986,16.5 6.19,15.581 5,14.144V14.791C5,15.205 4.664,15.541 4.25,15.541C3.836,15.541 3.5,15.205 3.5,14.791V12.396C3.5,11.981 3.836,11.646 4.25,11.646H6.646C7.06,11.646 7.395,11.981 7.396,12.396C7.396,12.81 7.06,13.146 6.646,13.146H6.123C7.04,14.276 8.434,15 10,15C12.762,15 15,12.762 15,10C15,9.586 15.336,9.25 15.75,9.25Z"
android:fillColor="#ffffff"/>
<path
android:pathData="M10,3.5C12.014,3.5 13.81,4.419 15,5.855V5.208C15,4.794 15.336,4.458 15.75,4.458C16.164,4.458 16.5,4.794 16.5,5.208V7.604C16.5,8.018 16.164,8.354 15.75,8.354H13.354C12.94,8.353 12.604,8.018 12.604,7.604C12.605,7.19 12.941,6.854 13.354,6.854H13.876C12.96,5.723 11.565,5 10,5C7.238,5 5,7.238 5,10C5,10.414 4.664,10.75 4.25,10.75C3.836,10.75 3.5,10.414 3.5,10C3.5,6.41 6.41,3.5 10,3.5Z"
android:fillColor="#ffffff"/>
</vector>

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 = {},
),
),
)