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 f3273753ed..04496bd572 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 @@ -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, ) } \ No newline at end of file diff --git a/core/ui/src/main/res/drawable/ic_category_24.xml b/core/ui/src/main/res/drawable/ic_category_24.xml new file mode 100644 index 0000000000..7fd56ce3ed --- /dev/null +++ b/core/ui/src/main/res/drawable/ic_category_24.xml @@ -0,0 +1,12 @@ + + + + + diff --git a/core/ui/src/main/res/drawable/ic_percent_24.xml b/core/ui/src/main/res/drawable/ic_percent_24.xml new file mode 100644 index 0000000000..241b3ba85c --- /dev/null +++ b/core/ui/src/main/res/drawable/ic_percent_24.xml @@ -0,0 +1,22 @@ + + + + + + + + + diff --git a/data/visa/src/main/kotlin/com/tangem/data/pay/repository/DefaultOnboardingRepository.kt b/data/visa/src/main/kotlin/com/tangem/data/pay/repository/DefaultOnboardingRepository.kt index 450444bf67..b005177a40 100644 --- a/data/visa/src/main/kotlin/com/tangem/data/pay/repository/DefaultOnboardingRepository.kt +++ b/data/visa/src/main/kotlin/com/tangem/data/pay/repository/DefaultOnboardingRepository.kt @@ -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 { - return requestHelper.runWithErrorLogs(TAG) { + override suspend fun createOrder(): Either = 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 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 b2006f60c4..be43b2bd72 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 @@ -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) } } 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 9c4257091e..4a01623666 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 @@ -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 { + Converter { - @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, ) } } \ No newline at end of file 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 2e97128461..65d960d01c 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 @@ -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() } \ No newline at end of file diff --git a/domain/visa/src/main/kotlin/com/tangem/domain/pay/usecase/TangemPayIssueOrderUseCase.kt b/domain/visa/src/main/kotlin/com/tangem/domain/pay/usecase/TangemPayIssueOrderUseCase.kt index 2cf992c3d8..6127e5225b 100644 --- a/domain/visa/src/main/kotlin/com/tangem/domain/pay/usecase/TangemPayIssueOrderUseCase.kt +++ b/domain/visa/src/main/kotlin/com/tangem/domain/pay/usecase/TangemPayIssueOrderUseCase.kt @@ -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 = repository.createOrder() } \ No newline at end of file diff --git a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/ui/ManageTokensScreen.kt b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/ui/ManageTokensScreen.kt index ebb75ad905..e7426ae87c 100644 --- a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/ui/ManageTokensScreen.kt +++ b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/ui/ManageTokensScreen.kt @@ -442,7 +442,7 @@ private class PreviewManageTokensComponentProvider : PreviewParameterProvider = model.uiState + override val state: StateFlow = 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) diff --git a/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/components/txHistory/PreviewTangemPayTxHistoryComponent.kt b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/components/txHistory/PreviewTangemPayTxHistoryComponent.kt index 61d078d853..b430960c14 100644 --- a/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/components/txHistory/PreviewTangemPayTxHistoryComponent.kt +++ b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/components/txHistory/PreviewTangemPayTxHistoryComponent.kt @@ -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 = MutableStateFlow(txHistoryUM) +internal class PreviewTangemPayTxHistoryComponent(txHistoryUM: TangemPayTxHistoryUM) : TangemPayTxHistoryComponent { + override val state: StateFlow = 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, ), ), ), diff --git a/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/components/txHistory/TangemPayTxHistoryComponent.kt b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/components/txHistory/TangemPayTxHistoryComponent.kt index 3b538ef7fd..8bc58a08f9 100644 --- a/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/components/txHistory/TangemPayTxHistoryComponent.kt +++ b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/components/txHistory/TangemPayTxHistoryComponent.kt @@ -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 - fun LazyListScope.txHistoryContent(listState: LazyListState, state: TxHistoryUM) + val state: StateFlow + fun LazyListScope.txHistoryContent(listState: LazyListState, state: TangemPayTxHistoryUM) } \ No newline at end of file diff --git a/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/entity/TangemPayTransactionState.kt b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/entity/TangemPayTransactionState.kt new file mode 100644 index 0000000000..675103d42e --- /dev/null +++ b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/entity/TangemPayTransactionState.kt @@ -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 + } +} \ No newline at end of file diff --git a/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/entity/TangemPayTxHistoryUM.kt b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/entity/TangemPayTxHistoryUM.kt new file mode 100644 index 0000000000..4bc4f8207b --- /dev/null +++ b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/entity/TangemPayTxHistoryUM.kt @@ -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, + 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 + } +} \ No newline at end of file diff --git a/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/model/TangemPayTxHistoryModel.kt b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/model/TangemPayTxHistoryModel.kt index f7b8a89c8b..1b3778feb0 100644 --- a/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/model/TangemPayTxHistoryModel.kt +++ b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/model/TangemPayTxHistoryModel.kt @@ -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 - field = MutableStateFlow(getLoadingState(isBalanceHidden = true)) + val uiState: StateFlow + field = MutableStateFlow(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) { + private fun updateState(items: ImmutableList) { 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) } } \ No newline at end of file diff --git a/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/model/transformers/TangemPayTxHistoryItemsConverter.kt b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/model/transformers/TangemPayTxHistoryItemsConverter.kt index 3e07907e7b..9184fac026 100644 --- a/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/model/transformers/TangemPayTxHistoryItemsConverter.kt +++ b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/model/transformers/TangemPayTxHistoryItemsConverter.kt @@ -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 { - 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 { + 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), ) } } \ No newline at end of file diff --git a/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/ui/TangemPayTxHistoryUi.kt b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/ui/TangemPayTxHistoryUi.kt new file mode 100644 index 0000000000..b7c07cbc03 --- /dev/null +++ b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/ui/TangemPayTxHistoryUi.kt @@ -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), + ) + } + } +} \ No newline at end of file diff --git a/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/utils/TangemPayTxHistoryListManager.kt b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/utils/TangemPayTxHistoryListManager.kt index 05ec6ef1d9..3ccf9cf202 100644 --- a/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/utils/TangemPayTxHistoryListManager.kt +++ b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/utils/TangemPayTxHistoryListManager.kt @@ -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 = MutableSharedFlow( @@ -33,7 +32,7 @@ internal class TangemPayTxHistoryListManager( private val state: MutableStateFlow = MutableStateFlow(TangemPayTxHistoryState()) private val uiManager = TangemPayTxHistoryUiManager(state = state, txHistoryUiActions = txHistoryUiActions) - val uiItems: Flow> = uiManager.items + val uiItems: Flow> = uiManager.items val paginationStatus: Flow> = state.map { it.status }.distinctUntilChanged() suspend fun launchPagination() = coroutineScope { diff --git a/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/utils/TangemPayTxHistoryState.kt b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/utils/TangemPayTxHistoryState.kt index b9b28372b3..5717238dda 100644 --- a/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/utils/TangemPayTxHistoryState.kt +++ b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/utils/TangemPayTxHistoryState.kt @@ -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>> = listOf(), + val uiBatches: List>> = listOf(), ) \ No newline at end of file diff --git a/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/utils/TangemPayTxHistoryUiActions.kt b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/utils/TangemPayTxHistoryUiActions.kt new file mode 100644 index 0000000000..08b2f82527 --- /dev/null +++ b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/utils/TangemPayTxHistoryUiActions.kt @@ -0,0 +1,7 @@ +package com.tangem.features.tangempay.utils + +import com.tangem.domain.visa.model.TangemPayTxHistoryItem + +internal interface TangemPayTxHistoryUiActions { + fun onTransactionClick(item: TangemPayTxHistoryItem) +} \ No newline at end of file diff --git a/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/utils/TangemPayTxHistoryUiManager.kt b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/utils/TangemPayTxHistoryUiManager.kt index fc3dc5f1df..0c4fb3698e 100644 --- a/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/utils/TangemPayTxHistoryUiManager.kt +++ b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/utils/TangemPayTxHistoryUiManager.kt @@ -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, - private val txHistoryUiActions: TxHistoryUiActions, + private val txHistoryUiActions: TangemPayTxHistoryUiActions, ) { @OptIn(ExperimentalCoroutinesApi::class) - val items: Flow> = state + val items: Flow> = 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>>, clearUiBatches: Boolean, - ): List>> { + ): List>> { 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): List { - val items = mutableListOf() + private fun generateUiItems( + key: Int, + data: List, + ): List { + val items = mutableListOf() // 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.transactionItemsSizeNotEqual( + private fun List.transactionItemsSizeNotEqual( txInfos: List, ): Boolean { - return this.filterIsInstance().size != txInfos.size + return this.filterIsInstance().size != txInfos.size } } \ No newline at end of file diff --git a/features/txhistory/api/src/main/kotlin/com/tangem/features/txhistory/utils/TxHistoryUiActions.kt b/features/txhistory/impl/src/main/kotlin/com/tangem/features/txhistory/utils/TxHistoryUiActions.kt similarity index 73% rename from features/txhistory/api/src/main/kotlin/com/tangem/features/txhistory/utils/TxHistoryUiActions.kt rename to features/txhistory/impl/src/main/kotlin/com/tangem/features/txhistory/utils/TxHistoryUiActions.kt index 9f7922aeda..57a50a9014 100644 --- a/features/txhistory/api/src/main/kotlin/com/tangem/features/txhistory/utils/TxHistoryUiActions.kt +++ b/features/txhistory/impl/src/main/kotlin/com/tangem/features/txhistory/utils/TxHistoryUiActions.kt @@ -1,6 +1,6 @@ package com.tangem.features.txhistory.utils -interface TxHistoryUiActions { +internal interface TxHistoryUiActions { fun openExplorer() fun openTxInExplorer(txHash: String) diff --git a/features/wallet/impl/build.gradle.kts b/features/wallet/impl/build.gradle.kts index b622e8fe42..4b2119adcb 100644 --- a/features/wallet/impl/build.gradle.kts +++ b/features/wallet/impl/build.gradle.kts @@ -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) diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/WalletModel.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/WalletModel.kt index 8fd08667d7..c843cb1153 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/WalletModel.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/WalletModel.kt @@ -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) diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/TangemPayStateTransformer.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/TangemPayStateTransformer.kt index b1d5ef746f..9e367377ba 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/TangemPayStateTransformer.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/TangemPayStateTransformer.kt @@ -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 { +) : 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 { diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/singlecurrency/TangemPayCardMainBlock.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/singlecurrency/TangemPayCardMainBlock.kt index 35c2b614fc..c108284448 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/singlecurrency/TangemPayCardMainBlock.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/singlecurrency/TangemPayCardMainBlock.kt @@ -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