Updated on 2026-08-14

This commit is contained in:
Tangem 2026-07-01 16:07:28 +03:00
parent 1f8386c05e
commit 0bfc7eee60
11 changed files with 112 additions and 9 deletions

View file

@ -17,6 +17,12 @@ interface TangemPayApi {
@Query("limit") limit: Int = TX_HISTORY_PAGING_DEFAULT_LIMIT,
): ApiResponse<TangemPayTxHistoryResponse>
@GET("v1/customer/transactions/{transaction_id}")
suspend fun getCustomerTransaction(
@Header("Authorization") authHeader: String,
@Path("transaction_id") transactionId: String,
): ApiResponse<TangemPayTransactionResponse>
@GET("v1/customer/kyc")
suspend fun getKycAccess(@Header("Authorization") authHeader: String): ApiResponse<KycAccessInfoResponse>

View file

@ -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,
)

View file

@ -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,

View file

@ -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<VisaApiError, TangemPayTxHistoryItem?> {
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)

View file

@ -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,
)
}

View file

@ -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

View file

@ -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<VisaApiError, TangemPayTxHistoryItem?>
}

View file

@ -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,

View file

@ -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<TangemPayTransactionBottomSheetComponent.Params>()
val uiState: StateFlow<TangemPayTxHistoryDetailsUiStates>
field = MutableStateFlow(buildUiStates(isBalanceHidden = params.isBalanceHidden))
private val transaction = MutableStateFlow(params.transaction)
val uiState: StateFlow<TangemPayTxHistoryDetailsUiStates> = 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) },

View file

@ -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),

View file

@ -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",