Updated on 2026-08-14

This commit is contained in:
Tangem 2025-10-03 17:05:44 +05:00
parent d943b499fa
commit 80f829a2aa
27 changed files with 831 additions and 279 deletions

View file

@ -35,20 +35,20 @@ data class TangemPayTxHistoryResponse(
@Json(name = "authorized_amount") val authorizedAmount: BigDecimal? = null,
@Json(name = "authorization_method") val authorizationMethod: String? = null,
@Json(name = "memo") val memo: String? = null,
@Json(name = "receipt") val receipt: Boolean? = null,
@Json(name = "merchant_name") val merchantName: String? = null,
@Json(name = "merchant_category") val merchantCategory: String? = null,
@Json(name = "merchant_category_code") val merchantCategoryCode: String? = null,
@Json(name = "receipt") val receipt: Boolean,
@Json(name = "merchant_name") val merchantName: String,
@Json(name = "merchant_category") val merchantCategory: String,
@Json(name = "merchant_category_code") val merchantCategoryCode: String,
@Json(name = "merchant_id") val merchantId: String? = null,
@Json(name = "enriched_merchant_icon") val enrichedMerchantIcon: String? = null,
@Json(name = "enriched_merchant_name") val enrichedMerchantName: String? = null,
@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 = "status") val status: String? = null,
@Json(name = "status") val status: String,
@Json(name = "declined_reason") val declinedReason: String? = null,
@Json(name = "authorized_at") val authorizedAt: DateTime? = null,
@Json(name = "posted_at") val postedAt: DateTime? = null,
@Json(name = "authorized_at") val authorizedAt: DateTime,
@Json(name = "posted_at") val postedAt: DateTime,
)
@JsonClass(generateAdapter = true)
@ -56,10 +56,10 @@ data class TangemPayTxHistoryResponse(
@Json(name = "amount") val amount: BigDecimal,
@Json(name = "currency") val currency: String,
@Json(name = "memo") val memo: String? = null,
@Json(name = "chain_id") val chainId: Long? = null,
@Json(name = "wallet_address") val walletAddress: String? = null,
@Json(name = "transaction_hash") val transactionHash: String? = null,
@Json(name = "posted_at") val postedAt: DateTime? = null,
@Json(name = "chain_id") val chainId: Long,
@Json(name = "wallet_address") val walletAddress: String,
@Json(name = "transaction_hash") val transactionHash: String,
@Json(name = "posted_at") val postedAt: DateTime?,
)
@JsonClass(generateAdapter = true)
@ -70,8 +70,8 @@ data class TangemPayTxHistoryResponse(
@Json(name = "chain_id") val chainId: Long? = null,
@Json(name = "wallet_address") val walletAddress: String? = null,
@Json(name = "transaction_hash") val transactionHash: String? = null,
@Json(name = "status") val status: String? = null,
@Json(name = "posted_at") val postedAt: DateTime? = null,
@Json(name = "status") val status: String,
@Json(name = "posted_at") val postedAt: DateTime,
)
@JsonClass(generateAdapter = true)
@ -79,6 +79,6 @@ data class TangemPayTxHistoryResponse(
@Json(name = "amount") val amount: BigDecimal,
@Json(name = "currency") val currency: String,
@Json(name = "description") val description: String? = null,
@Json(name = "posted_at") val postedAt: DateTime? = null,
@Json(name = "posted_at") val postedAt: DateTime,
)
}

File diff suppressed because one or more lines are too long

View file

@ -0,0 +1,22 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:autoMirrored="true"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#1E1E1E"
android:fillType="evenOdd"
android:pathData="M16.895,12.784C19.159,12.784 20.994,14.625 20.994,16.896C20.994,19.167 19.159,21.008 16.895,21.008C14.631,21.008 12.796,19.167 12.796,16.896C12.796,14.625 14.631,12.784 16.895,12.784ZM16.895,14.534C15.594,14.534 14.54,15.592 14.54,16.896C14.54,18.201 15.594,19.259 16.895,19.259C18.195,19.259 19.25,18.201 19.25,16.896C19.25,15.592 18.195,14.534 16.895,14.534Z" />
<path
android:fillColor="#1E1E1E"
android:pathData="M18.507,3.859C18.907,3.457 19.557,3.457 19.957,3.859C20.358,4.261 20.358,4.912 19.957,5.314L5.412,19.906C5.012,20.308 4.362,20.308 3.962,19.906C3.561,19.504 3.561,18.852 3.962,18.451L18.507,3.859Z" />
<path
android:fillColor="#1E1E1E"
android:fillType="evenOdd"
android:pathData="M7.092,3.008C9.356,3.008 11.191,4.849 11.191,7.12C11.191,9.391 9.356,11.232 7.092,11.232C4.829,11.232 2.994,9.391 2.994,7.12C2.994,4.849 4.829,3.008 7.092,3.008ZM7.092,4.758C5.792,4.758 4.738,5.816 4.738,7.12C4.738,8.425 5.792,9.483 7.092,9.483C8.393,9.483 9.447,8.425 9.447,7.12C9.447,5.816 8.393,4.758 7.092,4.758Z" />
</vector>

View file

@ -13,12 +13,15 @@ import com.tangem.domain.pay.model.CustomerInfo.ProductInstance
import com.tangem.domain.pay.model.MainScreenCustomerInfo
import com.tangem.domain.pay.model.OrderStatus
import com.tangem.domain.pay.repository.OnboardingRepository
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import kotlinx.coroutines.withContext
import javax.inject.Inject
private const val VALID_STATUS = "valid"
private const val TAG = "TangemPay: OnboardingRepository"
internal class DefaultOnboardingRepository @Inject constructor(
private val dispatcherProvider: CoroutineDispatcherProvider,
private val tangemPayApi: TangemPayApi,
private val requestHelper: TangemPayRequestPerformer,
private val tangemPayStorage: TangemPayStorage,
@ -54,8 +57,8 @@ internal class DefaultOnboardingRepository @Inject constructor(
}
}
override suspend fun createOrder(): Either<UniversalError, Unit> {
return requestHelper.runWithErrorLogs(TAG) {
override suspend fun createOrder(): Either<UniversalError, Unit> = withContext(dispatcherProvider.io) {
requestHelper.runWithErrorLogs(TAG) {
val walletAddress = requestHelper.getCustomerWalletAddress()
val result = requestHelper.requestWithPersistedToken { authHeader ->
tangemPayApi.createOrder(authHeader, body = OrderRequest(walletAddress))
@ -68,14 +71,13 @@ internal class DefaultOnboardingRepository @Inject constructor(
private fun getCustomerInfo(response: CustomerMeResponse.Result?): CustomerInfo {
val card = response?.card
val balance = response?.balance
val productInstance = response?.productInstance
val cardInfo = if (productInstance != null && card != null && balance != null) {
val paymentAccount = response?.paymentAccount
val cardInfo = if (paymentAccount != null && card != null && balance != null) {
CardInfo(
lastFourDigits = card.cardNumberEnd,
balance = balance.availableBalance,
currencyCode = balance.currency,
customerWalletAddress = productInstance.cardWalletAddress,
customerWalletAddress = paymentAccount.customerWalletAddress,
)
} else {
null

View file

@ -84,7 +84,7 @@ internal class DefaultTangemPayTxHistoryRepository @Inject constructor(
val result = requestPerformer.request { authHeader ->
visaApi.getTangemPayTxHistory(authHeader = authHeader, limit = pageSize, cursor = cursor)
}.result
val items = TangemPayTxHistoryItemConverter.convertList(result.transactions)
val items = TangemPayTxHistoryItemConverter.convertList(result.transactions).filterNotNull()
txHistoryItemsStore.store(key = customerWalletAddress, cursor = cursor ?: INITIAL_CURSOR, value = items)
}
}

View file

@ -3,49 +3,55 @@ package com.tangem.data.visa.utils
import com.tangem.datasource.api.pay.models.response.TangemPayTxHistoryResponse
import com.tangem.domain.visa.model.TangemPayTxHistoryItem
import com.tangem.utils.converter.Converter
import timber.log.Timber
import java.util.Currency
internal object TangemPayTxHistoryItemConverter :
Converter<TangemPayTxHistoryResponse.Transaction, TangemPayTxHistoryItem> {
Converter<TangemPayTxHistoryResponse.Transaction, TangemPayTxHistoryItem?> {
@Suppress("CyclomaticComplexMethod")
override fun convert(value: TangemPayTxHistoryResponse.Transaction): TangemPayTxHistoryItem {
val spend = value.spend
val collateral = value.collateral
val payment = value.payment
val fee = value.fee
override fun convert(value: TangemPayTxHistoryResponse.Transaction): TangemPayTxHistoryItem? {
return value.spend?.let { convertSpend(id = value.id, spend = it) }
?: value.payment?.let { convertPayment(id = value.id, payment = it) }
?: value.fee?.let { convertFee(id = value.id, fee = it) }
?: run {
Timber.wtf("unknown type of transaction: $value")
null
}
}
return TangemPayTxHistoryItem(
id = value.id,
date = when {
spend != null -> spend.postedAt
collateral != null -> collateral.postedAt
payment != null -> payment.postedAt
fee != null -> fee.postedAt
else -> null
},
amount = when {
spend != null -> spend.amount
collateral != null -> collateral.amount
payment != null -> payment.amount
fee != null -> fee.amount
else -> null
},
merchantName = when {
spend != null -> spend.merchantName
else -> null
},
status = when {
spend != null -> spend.status
payment != null -> payment.status
else -> null
},
currency = when {
spend != null -> spend.currency
collateral != null -> collateral.currency
payment != null -> payment.currency
fee != null -> fee.currency
else -> null
},
private fun convertSpend(id: String, spend: TangemPayTxHistoryResponse.Spend): TangemPayTxHistoryItem.Spend {
return TangemPayTxHistoryItem.Spend(
id = id,
date = spend.postedAt,
amount = spend.amount,
currency = Currency.getInstance(spend.currency),
enrichedMerchantName = spend.enrichedMerchantName,
merchantName = spend.merchantName,
enrichedMerchantCategory = spend.enrichedMerchantCategory,
merchantCategory = spend.merchantCategory,
status = spend.status,
enrichedMerchantIconUrl = spend.enrichedMerchantIcon,
)
}
private fun convertPayment(
id: String,
payment: TangemPayTxHistoryResponse.Payment,
): TangemPayTxHistoryItem.Payment {
return TangemPayTxHistoryItem.Payment(
id = id,
date = payment.postedAt,
currency = Currency.getInstance(payment.currency),
amount = payment.amount,
)
}
private fun convertFee(id: String, fee: TangemPayTxHistoryResponse.Fee): TangemPayTxHistoryItem.Fee {
return TangemPayTxHistoryItem.Fee(
id = id,
date = fee.postedAt,
currency = Currency.getInstance(fee.currency),
amount = fee.amount,
)
}
}

View file

@ -2,14 +2,38 @@ package com.tangem.domain.visa.model
import org.joda.time.DateTime
import java.math.BigDecimal
import java.util.Currency
data class TangemPayTxHistoryItem(
val id: String,
val date: DateTime?,
val amount: BigDecimal?,
val merchantName: String?,
val status: String?,
val currency: String?,
) {
val timeStampInMillis: Long = date?.millis ?: 0
sealed class TangemPayTxHistoryItem {
abstract val id: String
abstract val date: DateTime
abstract val amount: BigDecimal
abstract val currency: Currency
data class Spend(
override val id: String,
override val date: DateTime,
override val amount: BigDecimal,
override val currency: Currency,
val enrichedMerchantName: String?,
val merchantName: String,
val enrichedMerchantCategory: String?,
val merchantCategory: String,
val status: String,
val enrichedMerchantIconUrl: String?,
) : TangemPayTxHistoryItem()
data class Payment(
override val id: String,
override val date: DateTime,
override val amount: BigDecimal,
override val currency: Currency,
) : TangemPayTxHistoryItem()
data class Fee(
override val id: String,
override val date: DateTime,
override val amount: BigDecimal,
override val currency: Currency,
) : TangemPayTxHistoryItem()
}

View file

@ -1,10 +1,12 @@
package com.tangem.domain.pay.usecase
import arrow.core.Either
import com.tangem.core.error.UniversalError
import com.tangem.domain.pay.repository.OnboardingRepository
class TangemPayIssueOrderUseCase(
private val repository: OnboardingRepository,
) {
suspend operator fun invoke(): Unit? = repository.createOrder().getOrNull()
suspend operator fun invoke(): Either<UniversalError, Unit> = repository.createOrder()
}

View file

@ -442,7 +442,7 @@ private class PreviewManageTokensComponentProvider : PreviewParameterProvider<Ma
showTangemIcon = true,
params = ManageTokensComponent.Params(
source = ManageTokensSource.ONBOARDING,
userWalletId = UserWalletId("wallet_id"),
userWalletId = UserWalletId("0x"),
),
),
PreviewManageTokensComponent(
@ -455,7 +455,7 @@ private class PreviewManageTokensComponentProvider : PreviewParameterProvider<Ma
showTangemIcon = false,
params = ManageTokensComponent.Params(
source = ManageTokensSource.ONBOARDING,
userWalletId = UserWalletId("wallet_id"),
userWalletId = UserWalletId("0x"),
),
),
)

View file

@ -30,6 +30,8 @@ dependencies {
implementation(projects.domain.visa.models)
/** Compose */
implementation(deps.compose.coil)
implementation(deps.compose.constraintLayout)
implementation(deps.compose.foundation)
implementation(deps.compose.material3)
implementation(deps.compose.ui)

View file

@ -4,9 +4,9 @@ import androidx.compose.foundation.lazy.LazyListScope
import androidx.compose.foundation.lazy.LazyListState
import com.tangem.core.decompose.context.AppComponentContext
import com.tangem.core.decompose.model.getOrCreateModel
import com.tangem.features.tangempay.entity.TangemPayTxHistoryUM
import com.tangem.features.tangempay.model.TangemPayTxHistoryModel
import com.tangem.features.txhistory.entity.TxHistoryUM
import com.tangem.features.txhistory.ui.txHistoryItems
import com.tangem.features.tangempay.ui.tangemPayTxHistoryItems
import kotlinx.coroutines.flow.StateFlow
internal class DefaultTangemPayTxHistoryComponent(
@ -15,10 +15,10 @@ internal class DefaultTangemPayTxHistoryComponent(
) : AppComponentContext by appComponentContext, TangemPayTxHistoryComponent {
private val model: TangemPayTxHistoryModel = getOrCreateModel(params = params)
override val state: StateFlow<TxHistoryUM> = model.uiState
override val state: StateFlow<TangemPayTxHistoryUM> = model.uiState
override fun LazyListScope.txHistoryContent(listState: LazyListState, state: TxHistoryUM) {
txHistoryItems(listState, state)
override fun LazyListScope.txHistoryContent(listState: LazyListState, state: TangemPayTxHistoryUM) {
tangemPayTxHistoryItems(listState, state)
}
data class Params(val customerWalletAddress: String)

View file

@ -2,170 +2,150 @@ package com.tangem.features.tangempay.components.txHistory
import androidx.compose.foundation.lazy.LazyListScope
import androidx.compose.foundation.lazy.LazyListState
import com.tangem.core.ui.components.transactions.state.TransactionState
import com.tangem.core.ui.extensions.stringReference
import com.tangem.features.tangempay.details.impl.R
import com.tangem.features.txhistory.entity.TxHistoryUM
import com.tangem.features.txhistory.ui.txHistoryItems
import com.tangem.core.ui.res.TangemTheme
import com.tangem.features.tangempay.entity.TangemPayTransactionState
import com.tangem.features.tangempay.entity.TangemPayTxHistoryUM
import com.tangem.features.tangempay.ui.tangemPayTxHistoryItems
import kotlinx.collections.immutable.persistentListOf
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
internal class PreviewTangemPayTxHistoryComponent(txHistoryUM: TxHistoryUM) : TangemPayTxHistoryComponent {
override val state: StateFlow<TxHistoryUM> = MutableStateFlow(txHistoryUM)
internal class PreviewTangemPayTxHistoryComponent(txHistoryUM: TangemPayTxHistoryUM) : TangemPayTxHistoryComponent {
override val state: StateFlow<TangemPayTxHistoryUM> = MutableStateFlow(txHistoryUM)
override fun LazyListScope.txHistoryContent(listState: LazyListState, state: TxHistoryUM) {
txHistoryItems(listState, state)
override fun LazyListScope.txHistoryContent(listState: LazyListState, state: TangemPayTxHistoryUM) {
tangemPayTxHistoryItems(listState = listState, state = state)
}
companion object {
val loadingUM = TxHistoryUM.Loading(isBalanceHidden = true, onExploreClick = {})
val emptyUM = TxHistoryUM.Empty(isBalanceHidden = true, onExploreClick = {})
val contentUM = TxHistoryUM.Content(
val loadingUM = TangemPayTxHistoryUM.Loading(isBalanceHidden = true)
val emptyUM = TangemPayTxHistoryUM.Empty(isBalanceHidden = true)
val contentUM = TangemPayTxHistoryUM.Content(
isBalanceHidden = false,
loadMore = { false },
items = persistentListOf(
TxHistoryUM.TxHistoryItemUM.Title(onExploreClick = {}),
TxHistoryUM.TxHistoryItemUM.GroupTitle(title = "Today", itemKey = "Today"),
TxHistoryUM.TxHistoryItemUM.Transaction(
state = TransactionState.Content(
txHash = "signiferumque",
TangemPayTxHistoryUM.TangemPayTxHistoryItemUM.Title,
TangemPayTxHistoryUM.TangemPayTxHistoryItemUM.GroupTitle(title = "Today", itemKey = "Today"),
TangemPayTxHistoryUM.TangemPayTxHistoryItemUM.Transaction(
transaction = TangemPayTransactionState.Content.Spend(
id = "signiferumque",
onClick = {},
amount = "-4.99 USD",
amountColor = { TangemTheme.colors.text.primary1 },
time = "16:41",
status = TransactionState.Content.Status.Confirmed,
direction = TransactionState.Content.Direction.OUTGOING,
onClick = {},
iconRes = R.drawable.ic_arrow_up_24,
title = stringReference("Starbucks"),
subtitle = stringReference("Food&Drinks"),
timestamp = 3464,
iconUrl = null,
),
),
TxHistoryUM.TxHistoryItemUM.Transaction(
state = TransactionState.Content(
txHash = "signiferumque",
TangemPayTxHistoryUM.TangemPayTxHistoryItemUM.Transaction(
transaction = TangemPayTransactionState.Content.Spend(
id = "signiferumque",
amount = "-126.20 USD",
amountColor = { TangemTheme.colors.text.primary1 },
time = "12:04",
status = TransactionState.Content.Status.Confirmed,
direction = TransactionState.Content.Direction.OUTGOING,
onClick = {},
iconRes = R.drawable.ic_arrow_up_24,
title = stringReference("Wallmart"),
subtitle = stringReference("Supermarket"),
timestamp = 3465,
iconUrl = null,
),
),
TxHistoryUM.TxHistoryItemUM.GroupTitle(title = "Yesterday", itemKey = "Yesterday"),
TxHistoryUM.TxHistoryItemUM.Transaction(
state = TransactionState.Content(
txHash = "signiferumque",
TangemPayTxHistoryUM.TangemPayTxHistoryItemUM.GroupTitle(title = "Yesterday", itemKey = "Yesterday"),
TangemPayTxHistoryUM.TangemPayTxHistoryItemUM.Transaction(
transaction = TangemPayTransactionState.Content.Spend(
id = "signiferumque",
amount = "-4.99 USD",
amountColor = { TangemTheme.colors.text.primary1 },
time = "21:41",
status = TransactionState.Content.Status.Confirmed,
direction = TransactionState.Content.Direction.OUTGOING,
onClick = {},
iconRes = R.drawable.ic_arrow_up_24,
title = stringReference("Starbucks"),
subtitle = stringReference("Food&Drinks"),
timestamp = 3464,
iconUrl = null,
),
),
TxHistoryUM.TxHistoryItemUM.Transaction(
state = TransactionState.Content(
txHash = "signiferumque",
TangemPayTxHistoryUM.TangemPayTxHistoryItemUM.Transaction(
transaction = TangemPayTransactionState.Content.Spend(
id = "signiferumque",
amount = "-126.20 USD",
amountColor = { TangemTheme.colors.text.primary1 },
time = "10:04",
status = TransactionState.Content.Status.Confirmed,
direction = TransactionState.Content.Direction.OUTGOING,
onClick = {},
iconRes = R.drawable.ic_arrow_up_24,
title = stringReference("Wallmart"),
subtitle = stringReference("Supermarket"),
timestamp = 3465,
iconUrl = null,
),
),
TxHistoryUM.TxHistoryItemUM.Transaction(
state = TransactionState.Content(
txHash = "signiferumque",
TangemPayTxHistoryUM.TangemPayTxHistoryItemUM.Transaction(
transaction = TangemPayTransactionState.Content.Spend(
id = "signiferumque",
amount = "-4.99 USD",
amountColor = { TangemTheme.colors.text.primary1 },
time = "19:41",
status = TransactionState.Content.Status.Confirmed,
direction = TransactionState.Content.Direction.OUTGOING,
onClick = {},
iconRes = R.drawable.ic_arrow_up_24,
title = stringReference("Starbucks"),
subtitle = stringReference("Food&Drinks"),
timestamp = 3464,
iconUrl = null,
),
),
TxHistoryUM.TxHistoryItemUM.Transaction(
state = TransactionState.Content(
txHash = "signiferumque",
TangemPayTxHistoryUM.TangemPayTxHistoryItemUM.Transaction(
transaction = TangemPayTransactionState.Content.Spend(
id = "signiferumque",
amount = "-126.20 USD",
amountColor = { TangemTheme.colors.text.primary1 },
time = "18:04",
status = TransactionState.Content.Status.Confirmed,
direction = TransactionState.Content.Direction.OUTGOING,
onClick = {},
iconRes = R.drawable.ic_arrow_up_24,
title = stringReference("Wallmart"),
subtitle = stringReference("Supermarket"),
timestamp = 3465,
iconUrl = null,
),
),
TxHistoryUM.TxHistoryItemUM.Transaction(
state = TransactionState.Content(
txHash = "signiferumque",
TangemPayTxHistoryUM.TangemPayTxHistoryItemUM.Transaction(
transaction = TangemPayTransactionState.Content.Spend(
id = "signiferumque",
amount = "-4.99 USD",
amountColor = { TangemTheme.colors.text.primary1 },
time = "17:41",
status = TransactionState.Content.Status.Confirmed,
direction = TransactionState.Content.Direction.OUTGOING,
onClick = {},
iconRes = R.drawable.ic_arrow_up_24,
title = stringReference("Starbucks"),
subtitle = stringReference("Food&Drinks"),
timestamp = 3464,
iconUrl = null,
),
),
TxHistoryUM.TxHistoryItemUM.Transaction(
state = TransactionState.Content(
txHash = "signiferumque",
TangemPayTxHistoryUM.TangemPayTxHistoryItemUM.Transaction(
transaction = TangemPayTransactionState.Content.Spend(
id = "signiferumque",
amount = "-126.20 USD",
amountColor = { TangemTheme.colors.text.primary1 },
time = "16:04",
status = TransactionState.Content.Status.Confirmed,
direction = TransactionState.Content.Direction.OUTGOING,
onClick = {},
iconRes = R.drawable.ic_arrow_up_24,
title = stringReference("Wallmart"),
subtitle = stringReference("Supermarket"),
timestamp = 3465,
iconUrl = null,
),
),
TxHistoryUM.TxHistoryItemUM.Transaction(
state = TransactionState.Content(
txHash = "signiferumque",
TangemPayTxHistoryUM.TangemPayTxHistoryItemUM.Transaction(
transaction = TangemPayTransactionState.Content.Spend(
id = "signiferumque",
amount = "-4.99 USD",
amountColor = { TangemTheme.colors.text.primary1 },
time = "15:41",
status = TransactionState.Content.Status.Confirmed,
direction = TransactionState.Content.Direction.OUTGOING,
onClick = {},
iconRes = R.drawable.ic_arrow_up_24,
title = stringReference("Starbucks"),
subtitle = stringReference("Food&Drinks"),
timestamp = 3464,
iconUrl = null,
),
),
TxHistoryUM.TxHistoryItemUM.Transaction(
state = TransactionState.Content(
txHash = "signiferumque",
TangemPayTxHistoryUM.TangemPayTxHistoryItemUM.Transaction(
transaction = TangemPayTransactionState.Content.Spend(
id = "signiferumque",
amount = "-126.20 USD",
amountColor = { TangemTheme.colors.text.primary1 },
time = "14:04",
status = TransactionState.Content.Status.Confirmed,
direction = TransactionState.Content.Direction.OUTGOING,
onClick = {},
iconRes = R.drawable.ic_arrow_up_24,
title = stringReference("Wallmart"),
subtitle = stringReference("Supermarket"),
timestamp = 3465,
iconUrl = null,
),
),
),

View file

@ -2,10 +2,10 @@ package com.tangem.features.tangempay.components.txHistory
import androidx.compose.foundation.lazy.LazyListScope
import androidx.compose.foundation.lazy.LazyListState
import com.tangem.features.txhistory.entity.TxHistoryUM
import com.tangem.features.tangempay.entity.TangemPayTxHistoryUM
import kotlinx.coroutines.flow.StateFlow
internal interface TangemPayTxHistoryComponent {
val state: StateFlow<TxHistoryUM>
fun LazyListScope.txHistoryContent(listState: LazyListState, state: TxHistoryUM)
val state: StateFlow<TangemPayTxHistoryUM>
fun LazyListScope.txHistoryContent(listState: LazyListState, state: TangemPayTxHistoryUM)
}

View file

@ -0,0 +1,54 @@
package com.tangem.features.tangempay.entity
import androidx.compose.runtime.Composable
import androidx.compose.ui.graphics.Color
import com.tangem.core.ui.extensions.TextReference
internal sealed interface TangemPayTransactionState {
val id: String
data class Loading(override val id: String) : TangemPayTransactionState
sealed interface Content : TangemPayTransactionState {
val onClick: () -> Unit
val amount: String
val amountColor: @Composable (() -> Color)
val title: TextReference
val subtitle: TextReference
val time: String
data class Spend(
override val id: String,
override val onClick: () -> Unit,
override val amount: String,
override val amountColor: @Composable (() -> Color),
override val title: TextReference,
override val subtitle: TextReference,
override val time: String,
val iconUrl: String?,
) : Content
data class Payment(
override val id: String,
override val onClick: () -> Unit,
override val amount: String,
override val amountColor: @Composable (() -> Color),
override val title: TextReference,
override val subtitle: TextReference,
override val time: String,
val isIncome: Boolean,
) : Content
data class Fee(
override val id: String,
override val onClick: () -> Unit,
override val amount: String,
override val amountColor: @Composable (() -> Color),
override val title: TextReference,
override val subtitle: TextReference,
override val time: String,
) : Content
}
}

View file

@ -0,0 +1,45 @@
package com.tangem.features.tangempay.entity
import com.tangem.core.ui.components.transactions.state.TxHistoryState
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.persistentListOf
internal sealed interface TangemPayTxHistoryUM {
val isBalanceHidden: Boolean
data class Loading(override val isBalanceHidden: Boolean) : TangemPayTxHistoryUM {
val items = persistentListOf(
TangemPayTxHistoryItemUM.Title,
TangemPayTxHistoryItemUM.Transaction(TangemPayTransactionState.Loading("LOADING_TX_HASH_1")),
TangemPayTxHistoryItemUM.Transaction(TangemPayTransactionState.Loading("LOADING_TX_HASH_2")),
TangemPayTxHistoryItemUM.Transaction(TangemPayTransactionState.Loading("LOADING_TX_HASH_3")),
)
}
data class Empty(override val isBalanceHidden: Boolean) : TangemPayTxHistoryUM
data class Error(override val isBalanceHidden: Boolean, val onReload: () -> Unit) : TangemPayTxHistoryUM
data class Content(
override val isBalanceHidden: Boolean,
val items: ImmutableList<TangemPayTxHistoryItemUM>,
val loadMore: () -> Boolean,
) : TangemPayTxHistoryUM
fun copySealed(isBalanceHidden: Boolean): TangemPayTxHistoryUM {
return when (this) {
is Content -> copy(isBalanceHidden = isBalanceHidden)
is Empty -> copy(isBalanceHidden = isBalanceHidden)
is Error -> copy(isBalanceHidden = isBalanceHidden)
is Loading -> copy(isBalanceHidden = isBalanceHidden)
}
}
sealed interface TangemPayTxHistoryItemUM {
data object Title : TangemPayTxHistoryItemUM
data class GroupTitle(val title: String, val itemKey: String) : TangemPayTxHistoryItemUM {
val legacyGroupTitle = TxHistoryState.TxHistoryItemState.GroupTitle(title = title, itemKey = itemKey)
}
data class Transaction(val transaction: TangemPayTransactionState) : TangemPayTxHistoryItemUM
}
}

View file

@ -6,10 +6,11 @@ import com.tangem.core.decompose.model.Model
import com.tangem.core.decompose.model.ParamsContainer
import com.tangem.domain.balancehiding.GetBalanceHidingSettingsUseCase
import com.tangem.domain.tangempay.repository.TangemPayTxHistoryRepository
import com.tangem.domain.visa.model.TangemPayTxHistoryItem
import com.tangem.features.tangempay.components.txHistory.DefaultTangemPayTxHistoryComponent
import com.tangem.features.tangempay.entity.TangemPayTxHistoryUM
import com.tangem.features.tangempay.utils.TangemPayTxHistoryListManager
import com.tangem.features.txhistory.entity.TxHistoryUM
import com.tangem.features.txhistory.utils.TxHistoryUiActions
import com.tangem.features.tangempay.utils.TangemPayTxHistoryUiActions
import com.tangem.pagination.PaginationStatus
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import kotlinx.collections.immutable.ImmutableList
@ -25,7 +26,7 @@ internal class TangemPayTxHistoryModel @Inject constructor(
override val dispatchers: CoroutineDispatcherProvider,
tangemPayTxHistoryRepository: TangemPayTxHistoryRepository,
paramsContainer: ParamsContainer,
) : Model(), TxHistoryUiActions {
) : Model(), TangemPayTxHistoryUiActions {
private val params: DefaultTangemPayTxHistoryComponent.Params = paramsContainer.require()
private val listManager = TangemPayTxHistoryListManager(
@ -35,8 +36,8 @@ internal class TangemPayTxHistoryModel @Inject constructor(
txHistoryUiActions = this,
)
val uiState: StateFlow<TxHistoryUM>
field = MutableStateFlow<TxHistoryUM>(getLoadingState(isBalanceHidden = true))
val uiState: StateFlow<TangemPayTxHistoryUM>
field = MutableStateFlow<TangemPayTxHistoryUM>(getLoadingState(isBalanceHidden = true))
init {
handleBalanceHiding()
@ -50,19 +51,19 @@ internal class TangemPayTxHistoryModel @Inject constructor(
private fun subscribeToUiItemChanges() {
listManager.uiItems
.onEach { updateState(it) }
.onEach(::updateState)
.launchIn(modelScope)
listManager.paginationStatus
.onEach { paginationStatus -> handlePaginationStatus(paginationStatus) }
.launchIn(modelScope)
}
private fun updateState(items: ImmutableList<TxHistoryUM.TxHistoryItemUM>) {
private fun updateState(items: ImmutableList<TangemPayTxHistoryUM.TangemPayTxHistoryItemUM>) {
uiState.update { state ->
if (state is TxHistoryUM.Content) {
if (state is TangemPayTxHistoryUM.Content) {
state.copy(items = items)
} else {
TxHistoryUM.Content(
TangemPayTxHistoryUM.Content(
items = items,
isBalanceHidden = state.isBalanceHidden,
loadMore = ::loadMoreItems,
@ -91,11 +92,8 @@ internal class TangemPayTxHistoryModel @Inject constructor(
}
fun reload() {
// fast exit
if (uiState.value is TxHistoryUM.NotSupported) return
uiState.update { state ->
state as? TxHistoryUM.Content ?: getLoadingState(state.isBalanceHidden)
state as? TangemPayTxHistoryUM.Content ?: getLoadingState(state.isBalanceHidden)
}
modelScope.launch { listManager.reload() }
}
@ -106,23 +104,15 @@ internal class TangemPayTxHistoryModel @Inject constructor(
.launchIn(modelScope)
}
override fun openExplorer() {
Timber.d("onExploreClick: open explorer")
override fun onTransactionClick(item: TangemPayTxHistoryItem) {
Timber.d("onTransactionClick: $item")
}
override fun openTxInExplorer(txHash: String) {
Timber.d("openTxInExplorer: $txHash")
private fun getErrorState(isBalanceHidden: Boolean): TangemPayTxHistoryUM.Error {
return TangemPayTxHistoryUM.Error(isBalanceHidden = isBalanceHidden, onReload = ::reload)
}
private fun getErrorState(isBalanceHidden: Boolean): TxHistoryUM.Error {
return TxHistoryUM.Error(
isBalanceHidden = isBalanceHidden,
onReloadClick = ::reload,
onExploreClick = ::openExplorer,
)
}
private fun getLoadingState(isBalanceHidden: Boolean): TxHistoryUM.Loading {
return TxHistoryUM.Loading(isBalanceHidden = isBalanceHidden, onExploreClick = ::openExplorer)
private fun getLoadingState(isBalanceHidden: Boolean): TangemPayTxHistoryUM.Loading {
return TangemPayTxHistoryUM.Loading(isBalanceHidden = isBalanceHidden)
}
}

View file

@ -1,40 +1,80 @@
package com.tangem.features.tangempay.model.transformers
import com.tangem.core.ui.components.transactions.state.TransactionState
import com.tangem.core.ui.extensions.capitalize
import com.tangem.core.ui.extensions.stringReference
import com.tangem.core.ui.format.bigdecimal.fiat
import com.tangem.core.ui.format.bigdecimal.format
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.txhistory.utils.TxHistoryUiActions
import com.tangem.utils.StringsSigns
import com.tangem.features.tangempay.entity.TangemPayTransactionState
import com.tangem.features.tangempay.utils.TangemPayTxHistoryUiActions
import com.tangem.utils.converter.Converter
import com.tangem.utils.extensions.isPositive
import org.joda.time.DateTimeZone
import java.util.Currency
internal class TangemPayTxHistoryItemsConverter(
private val txHistoryUiActions: TxHistoryUiActions,
) : Converter<TangemPayTxHistoryItem, TransactionState> {
override fun convert(value: TangemPayTxHistoryItem): TransactionState {
val localDate = value.date?.withZone(DateTimeZone.getDefault())
val currency = Currency.getInstance(value.currency)
val amount = value.amount.format {
fiat(fiatCurrencyCode = currency.currencyCode, fiatCurrencySymbol = currency.symbol)
private val txHistoryUiActions: TangemPayTxHistoryUiActions,
) : Converter<TangemPayTxHistoryItem, TangemPayTransactionState.Content> {
override fun convert(value: TangemPayTxHistoryItem): TangemPayTransactionState.Content {
return when (value) {
is TangemPayTxHistoryItem.Spend -> convertSpend(spend = value)
is TangemPayTxHistoryItem.Payment -> convertPayment(payment = value)
is TangemPayTxHistoryItem.Fee -> convertFee(fee = value)
}
}
return TransactionState.Content(
txHash = value.id,
amount = "${StringsSigns.MINUS}$amount",
time = localDate?.let { DateTimeFormatters.formatDate(it, DateTimeFormatters.timeFormatter) } ?: "",
status = TransactionState.Content.Status.Confirmed,
direction = TransactionState.Content.Direction.OUTGOING,
iconRes = R.drawable.ic_arrow_up_24,
title = stringReference(value = value.merchantName?.capitalize() ?: "Unknown merchant"),
subtitle = stringReference("How to get merchant type?"),
timestamp = localDate?.millis ?: 0,
onClick = { txHistoryUiActions.openTxInExplorer(value.id) },
private fun convertSpend(spend: TangemPayTxHistoryItem.Spend): TangemPayTransactionState.Content.Spend {
val localDate = spend.date.withZone(DateTimeZone.getDefault())
val amount = spend.amount.format {
fiat(fiatCurrencyCode = spend.currency.currencyCode, fiatCurrencySymbol = spend.currency.symbol)
}
return TangemPayTransactionState.Content.Spend(
id = spend.id,
onClick = { txHistoryUiActions.onTransactionClick(spend) },
amount = amount,
amountColor = { TangemTheme.colors.text.primary1 },
title = stringReference(spend.enrichedMerchantName ?: spend.merchantName),
subtitle = stringReference(spend.enrichedMerchantCategory ?: spend.merchantCategory),
time = DateTimeFormatters.formatDate(localDate, DateTimeFormatters.timeFormatter),
iconUrl = spend.enrichedMerchantIconUrl,
)
}
private fun convertPayment(payment: TangemPayTxHistoryItem.Payment): TangemPayTransactionState.Content.Payment {
val amount = payment.amount.format {
fiat(fiatCurrencyCode = payment.currency.currencyCode, fiatCurrencySymbol = payment.currency.symbol)
}
val title = if (payment.amount.isPositive()) "Deposit" else "Withdrawal"
return TangemPayTransactionState.Content.Payment(
id = payment.id,
onClick = { txHistoryUiActions.onTransactionClick(payment) },
amount = amount,
amountColor = {
if (payment.amount.isPositive()) {
TangemTheme.colors.text.accent
} else {
TangemTheme.colors.text.primary1
}
},
title = stringReference(title),
subtitle = stringReference("Transfers"),
time = DateTimeFormatters.formatDate(payment.date, DateTimeFormatters.timeFormatter),
isIncome = payment.amount.isPositive(),
)
}
private fun convertFee(fee: TangemPayTxHistoryItem.Fee): TangemPayTransactionState.Content.Fee {
val amount = fee.amount.format {
fiat(fiatCurrencyCode = fee.currency.currencyCode, fiatCurrencySymbol = fee.currency.symbol)
}
return TangemPayTransactionState.Content.Fee(
id = fee.id,
onClick = { txHistoryUiActions.onTransactionClick(fee) },
amount = amount,
amountColor = { TangemTheme.colors.text.primary1 },
title = stringReference("Fee"),
subtitle = stringReference("Service fees"),
time = DateTimeFormatters.formatDate(fee.date, DateTimeFormatters.timeFormatter),
)
}
}

View file

@ -0,0 +1,373 @@
package com.tangem.features.tangempay.ui
import androidx.annotation.DrawableRes
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyListScope
import androidx.compose.foundation.lazy.LazyListState
import androidx.compose.foundation.lazy.itemsIndexed
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material3.Icon
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.style.TextOverflow
import androidx.constraintlayout.compose.ChainStyle
import androidx.constraintlayout.compose.ConstraintLayout
import androidx.constraintlayout.compose.Dimension
import coil.compose.rememberAsyncImagePainter
import com.tangem.core.ui.R
import com.tangem.core.ui.components.CircleShimmer
import com.tangem.core.ui.components.RectangleShimmer
import com.tangem.core.ui.components.list.InfiniteListHandler
import com.tangem.core.ui.components.transactions.TxHistoryGroupTitle
import com.tangem.core.ui.decorations.roundedShapeItemDecoration
import com.tangem.core.ui.extensions.orMaskWithStars
import com.tangem.core.ui.extensions.resolveReference
import com.tangem.core.ui.extensions.stringResourceSafe
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.test.TransactionHistoryBlockTestTags
import com.tangem.features.tangempay.entity.TangemPayTransactionState
import com.tangem.features.tangempay.entity.TangemPayTxHistoryUM
private const val LOAD_ITEMS_BUFFER = 20
internal fun LazyListScope.tangemPayTxHistoryItems(listState: LazyListState, state: TangemPayTxHistoryUM) {
when (state) {
is TangemPayTxHistoryUM.Content -> contentItems(listState = listState, state = state)
is TangemPayTxHistoryUM.Empty -> TODO("[REDACTED_JIRA]")
is TangemPayTxHistoryUM.Error -> TODO("[REDACTED_JIRA]")
is TangemPayTxHistoryUM.Loading -> loadingItems(state = state)
}
}
private fun LazyListScope.contentItems(listState: LazyListState, state: TangemPayTxHistoryUM.Content) {
itemsIndexed(
items = state.items,
key = { index, item ->
when (item) {
is TangemPayTxHistoryUM.TangemPayTxHistoryItemUM.GroupTitle -> item.itemKey
is TangemPayTxHistoryUM.TangemPayTxHistoryItemUM.Title -> index + item.hashCode()
is TangemPayTxHistoryUM.TangemPayTxHistoryItemUM.Transaction ->
item.transaction.id + (item.transaction as? TangemPayTransactionState.Content)?.hashCode()
}
},
contentType = { _, item -> item::class.java },
itemContent = { index, item ->
TangemPayTxHistoryListItem(
state = item,
isBalanceHidden = state.isBalanceHidden,
modifier = Modifier.roundedShapeItemDecoration(
currentIndex = index,
lastIndex = state.items.lastIndex,
),
)
},
)
item {
InfiniteListHandler(
listState = listState,
buffer = LOAD_ITEMS_BUFFER,
onLoadMore = state.loadMore,
)
}
}
private fun LazyListScope.loadingItems(state: TangemPayTxHistoryUM.Loading) {
itemsIndexed(
items = state.items,
key = { _, item ->
when (item) {
is TangemPayTxHistoryUM.TangemPayTxHistoryItemUM.GroupTitle -> item.itemKey
is TangemPayTxHistoryUM.TangemPayTxHistoryItemUM.Title -> item.hashCode()
is TangemPayTxHistoryUM.TangemPayTxHistoryItemUM.Transaction ->
item.transaction.id + (item.transaction as? TangemPayTransactionState.Content)?.hashCode()
}
},
contentType = { _, item -> item::class.java },
itemContent = { index, item ->
TangemPayTxHistoryListItem(
state = item,
isBalanceHidden = true,
modifier = Modifier.roundedShapeItemDecoration(
currentIndex = index,
lastIndex = state.items.lastIndex,
),
)
},
)
}
@Composable
private fun TangemPayTxHistoryListItem(
state: TangemPayTxHistoryUM.TangemPayTxHistoryItemUM,
isBalanceHidden: Boolean,
modifier: Modifier = Modifier,
) {
when (state) {
is TangemPayTxHistoryUM.TangemPayTxHistoryItemUM.GroupTitle -> {
TxHistoryGroupTitle(config = state.legacyGroupTitle, modifier = modifier)
}
is TangemPayTxHistoryUM.TangemPayTxHistoryItemUM.Title -> {
TangemPayTxHistoryTitle(modifier = modifier)
}
is TangemPayTxHistoryUM.TangemPayTxHistoryItemUM.Transaction -> {
TangemPayTransaction(
transactionState = state.transaction,
isBalanceHidden = isBalanceHidden,
modifier = modifier,
)
}
}
}
@Composable
private fun TangemPayTxHistoryTitle(modifier: Modifier = Modifier) {
Text(
text = stringResourceSafe(id = R.string.common_transactions),
color = TangemTheme.colors.text.tertiary,
style = TangemTheme.typography.subtitle2,
modifier = modifier
.background(TangemTheme.colors.background.primary)
.padding(top = TangemTheme.dimens.spacing12)
.padding(horizontal = TangemTheme.dimens.spacing12)
.fillMaxWidth()
.heightIn(min = TangemTheme.dimens.size24)
.testTag(TransactionHistoryBlockTestTags.TITLE_TEXT),
)
}
@Suppress("LongMethod")
@Composable
private fun TangemPayTransaction(
transactionState: TangemPayTransactionState,
isBalanceHidden: Boolean,
modifier: Modifier = Modifier,
) {
Surface(
modifier = modifier
.background(TangemTheme.colors.background.primary)
.clickable(
enabled = transactionState is TangemPayTransactionState.Content,
onClick = (transactionState as? TangemPayTransactionState.Content)?.onClick ?: {},
)
.fillMaxWidth()
.heightIn(min = TangemTheme.dimens.size56)
.padding(
vertical = TangemTheme.dimens.spacing8,
horizontal = TangemTheme.dimens.spacing12,
),
color = TangemTheme.colors.background.primary,
) {
@Suppress("DestructuringDeclarationWithTooManyEntries")
ConstraintLayout(modifier = Modifier.fillMaxWidth()) {
val (iconItem, titleItem, subtitleItem, amountItem, timestampItem) = createRefs()
createVerticalChain(titleItem, subtitleItem, chainStyle = ChainStyle.Spread)
createVerticalChain(amountItem, timestampItem, chainStyle = ChainStyle.Spread)
Icon(
state = transactionState,
modifier = Modifier
.size(TangemTheme.dimens.size40)
.constrainAs(iconItem) {
start.linkTo(parent.start)
centerVerticallyTo(parent)
},
)
Title(
state = transactionState,
modifier = Modifier
.padding(
start = TangemTheme.dimens.spacing12,
end = TangemTheme.dimens.spacing4,
)
.constrainAs(titleItem) {
top.linkTo(parent.top)
bottom.linkTo(subtitleItem.top)
start.linkTo(iconItem.end)
end.linkTo(amountItem.start)
width = Dimension.fillToConstraints
},
)
Subtitle(
state = transactionState,
modifier = Modifier
.padding(
start = TangemTheme.dimens.spacing12,
end = TangemTheme.dimens.spacing4,
)
.constrainAs(subtitleItem) {
top.linkTo(titleItem.bottom)
bottom.linkTo(parent.bottom)
start.linkTo(iconItem.end)
end.linkTo(timestampItem.start)
width = Dimension.fillToConstraints
},
)
Amount(
state = transactionState,
isBalanceHidden = isBalanceHidden,
modifier = Modifier.constrainAs(amountItem) {
top.linkTo(parent.top)
bottom.linkTo(timestampItem.top)
start.linkTo(titleItem.end)
end.linkTo(parent.end)
width = Dimension.fillToConstraints
},
)
Timestamp(
state = transactionState,
modifier = Modifier.constrainAs(timestampItem) {
top.linkTo(amountItem.bottom)
bottom.linkTo(parent.bottom)
end.linkTo(parent.end)
},
)
}
}
}
@Composable
private fun Icon(state: TangemPayTransactionState, modifier: Modifier = Modifier) {
when (state) {
is TangemPayTransactionState.Content.Spend -> {
if (state.iconUrl != null) {
RemoteIcon(modifier = modifier, url = state.iconUrl)
} else {
LocalStaticIcon(modifier = modifier, id = R.drawable.ic_category_24)
}
}
is TangemPayTransactionState.Content.Fee -> LocalStaticIcon(modifier = modifier, id = R.drawable.ic_percent_24)
is TangemPayTransactionState.Content.Payment -> LocalStaticIcon(
modifier = modifier,
id = if (state.isIncome) R.drawable.ic_arrow_down_24 else R.drawable.ic_arrow_up_24,
)
is TangemPayTransactionState.Loading -> {
CircleShimmer(modifier = modifier.size(TangemTheme.dimens.size40))
}
}
}
@Composable
private fun RemoteIcon(url: String, modifier: Modifier = Modifier) {
Icon(
modifier = modifier
.size(TangemTheme.dimens.size40)
.clip(CircleShape),
painter = rememberAsyncImagePainter(url),
contentDescription = null,
tint = Color.Unspecified,
)
}
@Composable
private fun LocalStaticIcon(@DrawableRes id: Int, modifier: Modifier = Modifier) {
Box(
modifier = modifier
.size(TangemTheme.dimens.size40)
.background(color = TangemTheme.colors.icon.secondary.copy(alpha = 0.1F), shape = CircleShape),
) {
Icon(
painter = painterResource(id),
contentDescription = null,
modifier = Modifier
.size(TangemTheme.dimens.size20)
.align(Alignment.Center),
tint = TangemTheme.colors.icon.informative,
)
}
}
@Composable
private fun Title(state: TangemPayTransactionState, modifier: Modifier = Modifier) {
when (state) {
is TangemPayTransactionState.Content -> {
Text(
modifier = modifier,
text = state.title.resolveReference(),
color = TangemTheme.colors.text.primary1,
style = TangemTheme.typography.subtitle2,
overflow = TextOverflow.Ellipsis,
maxLines = 1,
)
}
is TangemPayTransactionState.Loading -> {
RectangleShimmer(
modifier = modifier.size(width = TangemTheme.dimens.size70, height = TangemTheme.dimens.size12),
)
}
}
}
@Composable
private fun Subtitle(state: TangemPayTransactionState, modifier: Modifier = Modifier) {
when (state) {
is TangemPayTransactionState.Content -> {
Text(
text = state.subtitle.resolveReference(),
modifier = modifier,
textAlign = TextAlign.Start,
color = TangemTheme.colors.text.tertiary,
style = TangemTheme.typography.caption2,
)
}
is TangemPayTransactionState.Loading -> {
RectangleShimmer(
modifier = modifier.size(width = TangemTheme.dimens.size52, height = TangemTheme.dimens.size12),
)
}
}
}
@Composable
private fun Amount(state: TangemPayTransactionState, isBalanceHidden: Boolean, modifier: Modifier = Modifier) {
when (state) {
is TangemPayTransactionState.Content -> {
Text(
text = state.amount.orMaskWithStars(isBalanceHidden),
modifier = modifier,
textAlign = TextAlign.End,
color = state.amountColor(),
style = TangemTheme.typography.body2,
)
}
is TangemPayTransactionState.Loading -> {
RectangleShimmer(
modifier = modifier.size(width = TangemTheme.dimens.size40, height = TangemTheme.dimens.size12),
)
}
}
}
@Composable
private fun Timestamp(state: TangemPayTransactionState, modifier: Modifier = Modifier) {
when (state) {
is TangemPayTransactionState.Content -> {
Text(
text = state.time,
modifier = modifier,
textAlign = TextAlign.End,
color = TangemTheme.colors.text.tertiary,
style = TangemTheme.typography.caption2,
)
}
is TangemPayTransactionState.Loading -> {
RectangleShimmer(
modifier = modifier.size(width = TangemTheme.dimens.size40, height = TangemTheme.dimens.size12),
)
}
}
}

View file

@ -4,8 +4,7 @@ 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.model.TangemPayTxHistoryItem
import com.tangem.features.txhistory.entity.TxHistoryUM
import com.tangem.features.txhistory.utils.TxHistoryUiActions
import com.tangem.features.tangempay.entity.TangemPayTxHistoryUM
import com.tangem.pagination.BatchAction
import com.tangem.pagination.BatchListState
import com.tangem.pagination.PaginationStatus
@ -23,7 +22,7 @@ internal class TangemPayTxHistoryListManager(
private val repository: TangemPayTxHistoryRepository,
private val dispatchers: CoroutineDispatcherProvider,
private val customerWalletAddress: String,
private val txHistoryUiActions: TxHistoryUiActions,
private val txHistoryUiActions: TangemPayTxHistoryUiActions,
) {
private val jobHolder = JobHolder()
private val actionsFlow: MutableSharedFlow<TangemPayTxHistoryBatchAction> = MutableSharedFlow(
@ -33,7 +32,7 @@ internal class TangemPayTxHistoryListManager(
private val state: MutableStateFlow<TangemPayTxHistoryState> = MutableStateFlow(TangemPayTxHistoryState())
private val uiManager = TangemPayTxHistoryUiManager(state = state, txHistoryUiActions = txHistoryUiActions)
val uiItems: Flow<ImmutableList<TxHistoryUM.TxHistoryItemUM>> = uiManager.items
val uiItems: Flow<ImmutableList<TangemPayTxHistoryUM.TangemPayTxHistoryItemUM>> = uiManager.items
val paginationStatus: Flow<PaginationStatus<*>> = state.map { it.status }.distinctUntilChanged()
suspend fun launchPagination() = coroutineScope {

View file

@ -1,10 +1,10 @@
package com.tangem.features.tangempay.utils
import com.tangem.features.txhistory.entity.TxHistoryUM
import com.tangem.features.tangempay.entity.TangemPayTxHistoryUM
import com.tangem.pagination.Batch
import com.tangem.pagination.PaginationStatus
data class TangemPayTxHistoryState(
internal data class TangemPayTxHistoryState(
val status: PaginationStatus<*> = PaginationStatus.None,
val uiBatches: List<Batch<Int, List<TxHistoryUM.TxHistoryItemUM>>> = listOf(),
val uiBatches: List<Batch<Int, List<TangemPayTxHistoryUM.TangemPayTxHistoryItemUM>>> = listOf(),
)

View file

@ -0,0 +1,7 @@
package com.tangem.features.tangempay.utils
import com.tangem.domain.visa.model.TangemPayTxHistoryItem
internal interface TangemPayTxHistoryUiActions {
fun onTransactionClick(item: TangemPayTxHistoryItem)
}

View file

@ -2,9 +2,8 @@ package com.tangem.features.tangempay.utils
import com.tangem.core.ui.utils.toDateFormatWithTodayYesterday
import com.tangem.domain.visa.model.TangemPayTxHistoryItem
import com.tangem.features.tangempay.entity.TangemPayTxHistoryUM
import com.tangem.features.tangempay.model.transformers.TangemPayTxHistoryItemsConverter
import com.tangem.features.txhistory.entity.TxHistoryUM
import com.tangem.features.txhistory.utils.TxHistoryUiActions
import com.tangem.pagination.Batch
import com.tangem.pagination.PaginationStatus
import kotlinx.collections.immutable.ImmutableList
@ -15,11 +14,11 @@ import java.util.UUID
internal class TangemPayTxHistoryUiManager(
private val state: MutableStateFlow<TangemPayTxHistoryState>,
private val txHistoryUiActions: TxHistoryUiActions,
private val txHistoryUiActions: TangemPayTxHistoryUiActions,
) {
@OptIn(ExperimentalCoroutinesApi::class)
val items: Flow<ImmutableList<TxHistoryUM.TxHistoryItemUM>> = state
val items: Flow<ImmutableList<TangemPayTxHistoryUM.TangemPayTxHistoryItemUM>> = state
// filter initial states, since we dont emit loading items as UI items
.filter {
it.status !is PaginationStatus.None &&
@ -38,7 +37,7 @@ internal class TangemPayTxHistoryUiManager(
fun createOrUpdateUiBatches(
newCurrencyBatches: List<Batch<Int, List<TangemPayTxHistoryItem>>>,
clearUiBatches: Boolean,
): List<Batch<Int, List<TxHistoryUM.TxHistoryItemUM>>> {
): List<Batch<Int, List<TangemPayTxHistoryUM.TangemPayTxHistoryItemUM>>> {
val currentUiBatches = state.value.uiBatches
val batches = if (clearUiBatches) mutableListOf() else currentUiBatches.toMutableList()
@ -68,51 +67,58 @@ internal class TangemPayTxHistoryUiManager(
return batches
}
private fun generateUiItems(key: Int, data: List<TangemPayTxHistoryItem>): List<TxHistoryUM.TxHistoryItemUM> {
val items = mutableListOf<TxHistoryUM.TxHistoryItemUM>()
private fun generateUiItems(
key: Int,
data: List<TangemPayTxHistoryItem>,
): List<TangemPayTxHistoryUM.TangemPayTxHistoryItemUM> {
val items = mutableListOf<TangemPayTxHistoryUM.TangemPayTxHistoryItemUM>()
// Add title for the first batch
if (key == 0) {
items.add(TxHistoryUM.TxHistoryItemUM.Title(onExploreClick = txHistoryUiActions::openExplorer))
items.add(TangemPayTxHistoryUM.TangemPayTxHistoryItemUM.Title)
}
// Process batch items only if there are any
if (data.isNotEmpty()) {
// Add first item with its group title
val firstItem = data.first()
val firstDate = firstItem.timeStampInMillis.toDateFormatWithTodayYesterday()
val firstDate = firstItem.date.millis.toDateFormatWithTodayYesterday()
items.add(
TxHistoryUM.TxHistoryItemUM.GroupTitle(
TangemPayTxHistoryUM.TangemPayTxHistoryItemUM.GroupTitle(
title = firstDate,
itemKey = UUID.randomUUID().toString(),
),
)
items.add(TxHistoryUM.TxHistoryItemUM.Transaction(txHistoryItemConverter.convert(firstItem)))
items.add(
TangemPayTxHistoryUM.TangemPayTxHistoryItemUM.Transaction(txHistoryItemConverter.convert(firstItem)),
)
// Process remaining items with date separators when needed
data.zipWithNext { current, next ->
val currentDate = current.timeStampInMillis.toDateFormatWithTodayYesterday()
val nextDate = next.timeStampInMillis.toDateFormatWithTodayYesterday()
val currentDate = current.date.millis.toDateFormatWithTodayYesterday()
val nextDate = next.date.millis.toDateFormatWithTodayYesterday()
if (currentDate != nextDate) {
items.add(
TxHistoryUM.TxHistoryItemUM.GroupTitle(
TangemPayTxHistoryUM.TangemPayTxHistoryItemUM.GroupTitle(
title = nextDate,
itemKey = UUID.randomUUID().toString(),
),
)
}
items.add(TxHistoryUM.TxHistoryItemUM.Transaction(txHistoryItemConverter.convert(next)))
items.add(
TangemPayTxHistoryUM.TangemPayTxHistoryItemUM.Transaction(txHistoryItemConverter.convert(next)),
)
}
}
return items
}
private fun List<TxHistoryUM.TxHistoryItemUM>.transactionItemsSizeNotEqual(
private fun List<TangemPayTxHistoryUM.TangemPayTxHistoryItemUM>.transactionItemsSizeNotEqual(
txInfos: List<TangemPayTxHistoryItem>,
): Boolean {
return this.filterIsInstance<TxHistoryUM.TxHistoryItemUM.Transaction>().size != txInfos.size
return this.filterIsInstance<TangemPayTxHistoryUM.TangemPayTxHistoryItemUM.Transaction>().size != txInfos.size
}
}

View file

@ -1,6 +1,6 @@
package com.tangem.features.txhistory.utils
interface TxHistoryUiActions {
internal interface TxHistoryUiActions {
fun openExplorer()
fun openTxInExplorer(txHash: String)

View file

@ -58,6 +58,7 @@ dependencies {
implementation(projects.core.decompose)
implementation(projects.core.datasource)
implementation(projects.core.res)
implementation(projects.core.error)
implementation(projects.libs.crypto)
implementation(projects.libs.blockchainSdk)

View file

@ -356,42 +356,28 @@ internal class WalletModel @Inject constructor(
}
private suspend fun refreshTangemPayInfo() {
val newState = withContext(dispatchers.io) {
tangemPayMainScreenCustomerInfoUseCase()?.let { info ->
TangemPayStateTransformer(
val info = tangemPayMainScreenCustomerInfoUseCase()
if (info != null) {
stateHolder.update(
transformer = TangemPayStateTransformer(
value = info,
onIssueOrderClick = ::issueOrder,
onContinueKycClick = innerWalletRouter::openTangemPayOnboarding,
cardOnClick = ::tangemPayCardOnClick,
).transform(stateHolder.uiState.value.tangemPayState)
}
} ?: return
stateHolder.update { it.copy(tangemPayState = newState) }
openDetails = innerWalletRouter::openTangemPayDetails,
),
)
}
}
private fun issueOrder() {
modelScope.launch {
stateHolder.update {
it.copy(
tangemPayState = TangemPayStateTransformer(issueProgressState = true)
.transform(it.tangemPayState),
)
}
withContext(dispatchers.io) {
tangemPayIssueOrderUseCase()
} ?: stateHolder.update {
it.copy(
tangemPayState = TangemPayStateTransformer(issueState = true, onIssueOrderClick = ::issueOrder)
.transform(it.tangemPayState),
)
stateHolder.update(TangemPayStateTransformer(issueProgressState = true))
tangemPayIssueOrderUseCase().onLeft {
stateHolder.update(TangemPayStateTransformer(issueState = true, onIssueOrderClick = ::issueOrder))
}
}
}
private fun tangemPayCardOnClick(customerWalletAddress: String, cardNumberEnd: String) {
innerWalletRouter.openTangemPayDetails(customerWalletAddress, cardNumberEnd)
}
private fun needToRefreshTimer() {
modelScope.launch {
delay(REFRESH_WALLET_BACKGROUND_TIMER_MILLIS)

View file

@ -6,26 +6,29 @@ import com.tangem.core.ui.format.bigdecimal.fiat
import com.tangem.core.ui.format.bigdecimal.format
import com.tangem.domain.pay.model.CustomerInfo.CardInfo
import com.tangem.domain.pay.model.MainScreenCustomerInfo
import com.tangem.domain.pay.model.OrderStatus.NOT_ISSUED
import com.tangem.domain.pay.model.OrderStatus.CANCELED
import com.tangem.domain.pay.model.OrderStatus.NOT_ISSUED
import com.tangem.feature.wallet.presentation.wallet.state.model.TangemPayState
import com.tangem.feature.wallet.presentation.wallet.state.model.TangemPayState.Progress
import com.tangem.utils.transformer.Transformer
import com.tangem.feature.wallet.presentation.wallet.state.model.WalletScreenState
import java.util.Currency
internal class TangemPayStateTransformer(
private val value: MainScreenCustomerInfo? = null,
private val onIssueOrderClick: () -> Unit = {},
private val onContinueKycClick: () -> Unit = {},
private val cardOnClick: (customerWalletAddress: String, cardNumberEnd: String) -> Unit = { _, _ -> },
private val openDetails: (customerWalletAddress: String, cardNumberEnd: String) -> Unit = { _, _ -> },
private val issueProgressState: Boolean = false,
private val issueState: Boolean = false,
) : Transformer<TangemPayState> {
) : WalletScreenStateTransformer {
override fun transform(prevState: TangemPayState): TangemPayState = when {
issueProgressState -> createIssueProgressState()
issueState -> createIssueState()
else -> createInitialState()
override fun transform(prevState: WalletScreenState): WalletScreenState {
val tangemPayState = when {
issueProgressState -> createIssueProgressState()
issueState -> createIssueState()
else -> createInitialState()
}
return prevState.copy(tangemPayState = tangemPayState)
}
private fun createInitialState(): TangemPayState {
@ -64,7 +67,7 @@ internal class TangemPayStateTransformer(
private fun getCardInfoState(cardInfo: CardInfo): TangemPayState = TangemPayState.Card(
lastFourDigits = TextReference.Str("*${cardInfo.lastFourDigits}"),
balanceText = TextReference.Str(getBalanceText(cardInfo)),
onClick = { cardOnClick(cardInfo.customerWalletAddress, cardInfo.lastFourDigits) },
onClick = { openDetails(cardInfo.customerWalletAddress, cardInfo.lastFourDigits) },
)
private fun getBalanceText(cardInfo: CardInfo): String {

View file

@ -1,7 +1,6 @@
package com.tangem.feature.wallet.presentation.wallet.ui.components.singlecurrency
import androidx.compose.foundation.Image
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material3.Surface
@ -22,11 +21,10 @@ import com.tangem.feature.wallet.presentation.wallet.state.model.TangemPayState
@Composable
internal fun TangemPayCardMainBlock(state: TangemPayState.Card, modifier: Modifier = Modifier) {
Surface(
modifier = modifier
.fillMaxWidth()
.clickable { state.onClick() },
modifier = modifier.fillMaxWidth(),
shape = TangemTheme.shapes.roundedCornersXMedium,
color = TangemTheme.colors.background.primary,
onClick = state.onClick,
) {
Row(
modifier = Modifier