From 0bfc7eee60cacf9bc8cb80aa51fd835d3e0daf80 Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 1 Jul 2026 16:07:28 +0300 Subject: [PATCH] Updated on 2026-08-14 --- .../tangem/datasource/api/pay/TangemPayApi.kt | 6 +++ .../response/TangemPayTransactionResponse.kt | 10 +++++ .../response/TangemPayTxHistoryResponse.kt | 2 + .../DefaultTangemPayTxHistoryRepository.kt | 13 ++++++ .../utils/TangemPayTxHistoryItemConverter.kt | 2 + .../visa/model/TangemPayTxHistoryItem.kt | 2 + .../TangemPayTxHistoryRepository.kt | 9 +++++ .../entity/TangemPayTxHistoryDetailsUM.kt | 1 + .../model/TangemPayTxHistoryDetailsModel.kt | 40 ++++++++++++++----- .../TangemPayTxHistoryDetailsConverterV2.kt | 13 ++++++ .../tangempay/ui/TangempayTxDetailsUiV2.kt | 23 +++++++++++ 11 files changed, 112 insertions(+), 9 deletions(-) create mode 100644 core/datasource/src/main/java/com/tangem/datasource/api/pay/models/response/TangemPayTransactionResponse.kt diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/pay/TangemPayApi.kt b/core/datasource/src/main/java/com/tangem/datasource/api/pay/TangemPayApi.kt index 37f8928d79..55b9d94ee3 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/pay/TangemPayApi.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/pay/TangemPayApi.kt @@ -17,6 +17,12 @@ interface TangemPayApi { @Query("limit") limit: Int = TX_HISTORY_PAGING_DEFAULT_LIMIT, ): ApiResponse + @GET("v1/customer/transactions/{transaction_id}") + suspend fun getCustomerTransaction( + @Header("Authorization") authHeader: String, + @Path("transaction_id") transactionId: String, + ): ApiResponse + @GET("v1/customer/kyc") suspend fun getKycAccess(@Header("Authorization") authHeader: String): ApiResponse diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/pay/models/response/TangemPayTransactionResponse.kt b/core/datasource/src/main/java/com/tangem/datasource/api/pay/models/response/TangemPayTransactionResponse.kt new file mode 100644 index 0000000000..12370550f9 --- /dev/null +++ b/core/datasource/src/main/java/com/tangem/datasource/api/pay/models/response/TangemPayTransactionResponse.kt @@ -0,0 +1,10 @@ +package com.tangem.datasource.api.pay.models.response + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +/** Response of `GET v1/customer/transactions/{transaction_id}` — a single transaction by its id. */ +@JsonClass(generateAdapter = true) +data class TangemPayTransactionResponse( + @Json(name = "result") val result: TangemPayTxHistoryResponse.Transaction, +) \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/pay/models/response/TangemPayTxHistoryResponse.kt b/core/datasource/src/main/java/com/tangem/datasource/api/pay/models/response/TangemPayTxHistoryResponse.kt index 009b96d99c..11d9be3def 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/pay/models/response/TangemPayTxHistoryResponse.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/pay/models/response/TangemPayTxHistoryResponse.kt @@ -44,6 +44,8 @@ data class TangemPayTxHistoryResponse( @Json(name = "enriched_merchant_category") val enrichedMerchantCategory: String? = null, @Json(name = "card_id") val cardId: String? = null, @Json(name = "card_type") val cardType: String? = null, + @Json(name = "card_display_name") val cardDisplayName: String? = null, + @Json(name = "card_number_end") val cardNumberEnd: String? = null, @Json(name = "status") val status: String, @Json(name = "declined_reason") val declinedReason: String? = null, @Json(name = "authorized_at") val authorizedAt: DateTime, diff --git a/data/visa/src/main/kotlin/com/tangem/data/pay/repository/DefaultTangemPayTxHistoryRepository.kt b/data/visa/src/main/kotlin/com/tangem/data/pay/repository/DefaultTangemPayTxHistoryRepository.kt index 90d85dada1..a4f44f7d6f 100644 --- a/data/visa/src/main/kotlin/com/tangem/data/pay/repository/DefaultTangemPayTxHistoryRepository.kt +++ b/data/visa/src/main/kotlin/com/tangem/data/pay/repository/DefaultTangemPayTxHistoryRepository.kt @@ -1,5 +1,6 @@ package com.tangem.data.pay.repository +import arrow.core.Either import com.squareup.moshi.Moshi import com.tangem.data.common.cache.CacheRegistry import com.tangem.data.visa.utils.TangemPayTxHistoryItemConverter @@ -11,6 +12,7 @@ import com.tangem.domain.tangempay.model.TangemPayTxHistoryListBatchFlow import com.tangem.domain.tangempay.model.TangemPayTxHistoryListBatchingContext import com.tangem.domain.tangempay.model.TangemPayTxHistoryListConfig import com.tangem.domain.tangempay.repository.TangemPayTxHistoryRepository +import com.tangem.domain.visa.error.VisaApiError import com.tangem.domain.visa.model.TangemPayTxHistoryItem import com.tangem.pagination.BatchFetchResult import com.tangem.pagination.BatchListSource @@ -99,6 +101,17 @@ internal class DefaultTangemPayTxHistoryRepository @Inject constructor( return "tangem_pay_tx_history_${userWalletId.stringValue}_${cursor ?: INITIAL_CURSOR}" } + override suspend fun getTransaction( + userWalletId: UserWalletId, + transactionId: String, + ): Either { + return requestPerformer.performRequest(userWalletId = userWalletId) { authHeader -> + visaApi.getCustomerTransaction(authHeader = authHeader, transactionId = transactionId) + }.map { response -> + txHistoryItemConverter.convert(response.result) + } + } + private suspend fun fetch(userWalletId: UserWalletId, cursor: String?, pageSize: Int) { requestPerformer.performRequest(userWalletId = userWalletId) { authHeader -> visaApi.getTangemPayTxHistory(authHeader = authHeader, limit = pageSize, cursor = cursor) diff --git a/data/visa/src/main/kotlin/com/tangem/data/visa/utils/TangemPayTxHistoryItemConverter.kt b/data/visa/src/main/kotlin/com/tangem/data/visa/utils/TangemPayTxHistoryItemConverter.kt index a25181a85b..899e667a29 100644 --- a/data/visa/src/main/kotlin/com/tangem/data/visa/utils/TangemPayTxHistoryItemConverter.kt +++ b/data/visa/src/main/kotlin/com/tangem/data/visa/utils/TangemPayTxHistoryItemConverter.kt @@ -55,6 +55,8 @@ internal class TangemPayTxHistoryItemConverter(moshi: Moshi) : status = TangemPayTxHistoryItemStatusConverter.convert(spend.status), enrichedMerchantIconUrl = spend.enrichedMerchantIcon, declinedReason = spend.declinedReason, + cardName = spend.cardDisplayName, + cardNumberLast4 = spend.cardNumberEnd, ) } diff --git a/domain/visa/models/src/main/kotlin/com/tangem/domain/visa/model/TangemPayTxHistoryItem.kt b/domain/visa/models/src/main/kotlin/com/tangem/domain/visa/model/TangemPayTxHistoryItem.kt index 8f2196bef9..a2d2720c1e 100644 --- a/domain/visa/models/src/main/kotlin/com/tangem/domain/visa/model/TangemPayTxHistoryItem.kt +++ b/domain/visa/models/src/main/kotlin/com/tangem/domain/visa/model/TangemPayTxHistoryItem.kt @@ -31,6 +31,8 @@ sealed class TangemPayTxHistoryItem { val status: Status, val enrichedMerchantIconUrl: String?, val declinedReason: String?, + val cardName: String? = null, + val cardNumberLast4: String? = null, ) : TangemPayTxHistoryItem() @Serializable diff --git a/domain/visa/src/main/kotlin/com/tangem/domain/tangempay/repository/TangemPayTxHistoryRepository.kt b/domain/visa/src/main/kotlin/com/tangem/domain/tangempay/repository/TangemPayTxHistoryRepository.kt index 50bf30ba9e..bb981f82de 100644 --- a/domain/visa/src/main/kotlin/com/tangem/domain/tangempay/repository/TangemPayTxHistoryRepository.kt +++ b/domain/visa/src/main/kotlin/com/tangem/domain/tangempay/repository/TangemPayTxHistoryRepository.kt @@ -1,8 +1,11 @@ package com.tangem.domain.tangempay.repository +import arrow.core.Either import com.tangem.domain.models.wallet.UserWalletId import com.tangem.domain.tangempay.model.TangemPayTxHistoryListBatchFlow import com.tangem.domain.tangempay.model.TangemPayTxHistoryListBatchingContext +import com.tangem.domain.visa.error.VisaApiError +import com.tangem.domain.visa.model.TangemPayTxHistoryItem interface TangemPayTxHistoryRepository { fun getTxHistoryBatchFlow( @@ -10,4 +13,10 @@ interface TangemPayTxHistoryRepository { batchSize: Int, context: TangemPayTxHistoryListBatchingContext, ): TangemPayTxHistoryListBatchFlow + + /** Loads a single transaction via `GET v1/customer/transactions/{transactionId}`. */ + suspend fun getTransaction( + userWalletId: UserWalletId, + transactionId: String, + ): Either } \ No newline at end of file diff --git a/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/entity/TangemPayTxHistoryDetailsUM.kt b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/entity/TangemPayTxHistoryDetailsUM.kt index aa732fd535..3cc10af2ab 100644 --- a/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/entity/TangemPayTxHistoryDetailsUM.kt +++ b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/entity/TangemPayTxHistoryDetailsUM.kt @@ -44,6 +44,7 @@ internal data class TangemPayTxHistoryDetailsUMV2( val subtitle: TextReference, val iconState: TangemIconUM, val transactionTitle: TextReference, + val card: TextReference?, val transactionCategory: TextReference, val mcc: TextReference?, val transactionAmount: String, diff --git a/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/model/TangemPayTxHistoryDetailsModel.kt b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/model/TangemPayTxHistoryDetailsModel.kt index ca8d041322..1a36dcd1c4 100644 --- a/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/model/TangemPayTxHistoryDetailsModel.kt +++ b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/model/TangemPayTxHistoryDetailsModel.kt @@ -13,6 +13,8 @@ import com.tangem.domain.feedback.GetWalletMetaInfoUseCase import com.tangem.domain.feedback.SendFeedbackEmailUseCase import com.tangem.domain.feedback.models.FeedbackEmailType import com.tangem.domain.tangempay.TangemPayAnalyticsEvents +import com.tangem.domain.tangempay.repository.TangemPayTxHistoryRepository +import com.tangem.domain.visa.model.TangemPayTxHistoryItem import com.tangem.features.tangempay.components.TangemPayTransactionBottomSheetComponent import com.tangem.features.tangempay.entity.TangemPayTxHistoryDetailsUiStates import com.tangem.features.tangempay.model.transformers.TangemPayTxHistoryDetailsConverter @@ -31,31 +33,51 @@ internal class TangemPayTxHistoryDetailsModel @Inject constructor( private val sendFeedbackEmailUseCase: SendFeedbackEmailUseCase, private val urlOpener: UrlOpener, private val balanceHidingSettings: GetBalanceHidingSettingsUseCase, + private val tangemPayTxHistoryRepository: TangemPayTxHistoryRepository, private val analytics: AnalyticsEventHandler, paramsContainer: ParamsContainer, ) : Model() { private val params = paramsContainer.require() - val uiState: StateFlow - field = MutableStateFlow(buildUiStates(isBalanceHidden = params.isBalanceHidden)) + + private val transaction = MutableStateFlow(params.transaction) + + val uiState: StateFlow = combine( + balanceHidingSettings.isBalanceHidden(), + transaction, + ) { isBalanceHidden, transaction -> + buildUiStates(isBalanceHidden, transaction) + }.stateIn( + scope = modelScope, + started = SharingStarted.Eagerly, + initialValue = buildUiStates(isBalanceHidden = params.isBalanceHidden, transaction = params.transaction), + ) init { - subscribeToBalanceHiding() + loadTransaction() } fun dismiss() { params.onDismiss() } - private fun subscribeToBalanceHiding() { - balanceHidingSettings.isBalanceHidden() - .onEach { isBalanceHidden -> uiState.update { buildUiStates(isBalanceHidden) } } - .launchIn(modelScope) + private fun loadTransaction() { + val current = params.transaction + if (current !is TangemPayTxHistoryItem.Spend) return + modelScope.launch { + tangemPayTxHistoryRepository.getTransaction( + userWalletId = params.userWalletId, + transactionId = current.id, + ).onRight { loaded -> if (loaded != null) transaction.value = loaded } + } } - private fun buildUiStates(isBalanceHidden: Boolean): TangemPayTxHistoryDetailsUiStates { + private fun buildUiStates( + isBalanceHidden: Boolean, + transaction: TangemPayTxHistoryItem, + ): TangemPayTxHistoryDetailsUiStates { val converterInput = TangemPayTxHistoryDetailsConverter.Input( - item = params.transaction, + item = transaction, isBalanceHidden = isBalanceHidden, onExplorerClick = ::openExplorer, onDisputeClick = { dispute(customerId = params.customerId) }, diff --git a/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/model/transformers/TangemPayTxHistoryDetailsConverterV2.kt b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/model/transformers/TangemPayTxHistoryDetailsConverterV2.kt index 49ff1574e7..717983e7eb 100644 --- a/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/model/transformers/TangemPayTxHistoryDetailsConverterV2.kt +++ b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/model/transformers/TangemPayTxHistoryDetailsConverterV2.kt @@ -34,6 +34,7 @@ internal object TangemPayTxHistoryDetailsConverterV2 : subtitle = transaction.extractDate(), iconState = transaction.extractIcon(), transactionTitle = transaction.extractTransactionTitle(), + card = transaction.extractCard(), transactionCategory = transaction.extractTransactionCategory(), mcc = transaction.extractMcc(), transactionAmount = transaction.extractAmount(), @@ -289,6 +290,18 @@ internal object TangemPayTxHistoryDetailsConverterV2 : } } + private fun TangemPayTxHistoryItem.extractCard(): TextReference? { + if (this !is TangemPayTxHistoryItem.Spend) return null + val name = cardName?.takeIf { it.isNotEmpty() } + val last4 = cardNumberLast4?.takeIf { it.isNotEmpty() } + return when { + name != null && last4 != null -> stringReference("$name *$last4") + last4 != null -> stringReference("*$last4") + name != null -> stringReference(name) + else -> null + } + } + private fun Input.extractButtonState(): ButtonState { return ButtonState( text = resourceReference(R.string.tangem_pay_get_help), diff --git a/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/ui/TangempayTxDetailsUiV2.kt b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/ui/TangempayTxDetailsUiV2.kt index 814c0fc4cd..a3eff7a77c 100644 --- a/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/ui/TangempayTxDetailsUiV2.kt +++ b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/ui/TangempayTxDetailsUiV2.kt @@ -233,6 +233,24 @@ internal fun TransactionLabel(label: TransactionLabelUM, modifier: Modifier = Mo @Composable private fun TransactionDetailsBlock(state: TangemPayTxHistoryDetailsUMV2, modifier: Modifier = Modifier) { Column(modifier = modifier) { + if (state.card != null) { + TangemRow( + divider = true, + contentLead = TangemRowContentLead.Start, + titleSlot = { + TangemRowText( + text = resourceReference(R.string.tangempay_common_card), + role = TangemRowTextRole.Title, + ) + }, + valueSlot = { + TangemRowText( + text = state.card, + role = TangemRowTextRole.Value, + ) + }, + ) + } TangemRow( divider = state.mcc != null, contentLead = TangemRowContentLead.Start, @@ -289,6 +307,7 @@ private class TangemPayTxHistoryDetailsUMProviderV2 : subtitle = stringReference("12 June 2026, 12:40"), iconState = TangemIconUM.Icon(iconRes = R.drawable.ic_category_24), transactionTitle = stringReference("Starbucks"), + card = stringReference("Basic card *9092"), transactionCategory = stringReference("Food and drinks"), mcc = stringReference("5814"), transactionAmount = "-$5.86", @@ -310,6 +329,7 @@ private class TangemPayTxHistoryDetailsUMProviderV2 : subtitle = stringReference("12 June 2026, 12:40"), iconState = TangemIconUM.Icon(iconRes = R.drawable.ic_category_24), transactionTitle = stringReference("NuCaloric"), + card = stringReference("Basic card *9092"), transactionCategory = stringReference("Groceries"), mcc = stringReference("0000"), transactionAmount = "-$820.52", @@ -332,6 +352,7 @@ private class TangemPayTxHistoryDetailsUMProviderV2 : subtitle = stringReference("12 June 2026, 12:40"), iconState = TangemIconUM.Icon(iconRes = R.drawable.ic_category_24), transactionTitle = stringReference("Starbucks"), + card = stringReference("Basic card *9092"), transactionCategory = stringReference("Food and drinks"), mcc = null, transactionAmount = "-$5.86", @@ -353,6 +374,7 @@ private class TangemPayTxHistoryDetailsUMProviderV2 : subtitle = stringReference("12 June 2026, 12:40"), iconState = TangemIconUM.Icon(iconRes = R.drawable.ic_percent_24), transactionTitle = stringReference("Service fees"), + card = null, transactionCategory = stringReference("Service fees"), mcc = null, transactionAmount = "-$5.86", @@ -375,6 +397,7 @@ private class TangemPayTxHistoryDetailsUMProviderV2 : subtitle = stringReference("12 June 2026, 12:40"), iconState = TangemIconUM.Icon(imageVector = Icons.ic_arrow_down_24), transactionTitle = resourceReference(R.string.common_transfer), + card = null, transactionCategory = resourceReference(R.string.common_transfer), mcc = null, transactionAmount = "+$20",