Updated on 2026-08-14
This commit is contained in:
parent
2eee398c36
commit
8c93c3b23c
7 changed files with 419 additions and 43 deletions
|
|
@ -44,7 +44,7 @@ internal data class TangemPayTxHistoryDetailsUMV2(
|
|||
val subtitle: TextReference,
|
||||
val iconState: TangemIconUM,
|
||||
val transactionTitle: TextReference,
|
||||
val card: TextReference?,
|
||||
val detail: TransactionDetailUM?,
|
||||
val transactionCategory: TextReference,
|
||||
val mcc: TextReference?,
|
||||
val transactionAmount: String,
|
||||
|
|
@ -68,6 +68,15 @@ internal enum class TransactionStateType {
|
|||
Reversed,
|
||||
}
|
||||
|
||||
@Immutable
|
||||
internal sealed interface TransactionDetailUM {
|
||||
data object Loading : TransactionDetailUM
|
||||
data class Content(val card: TextReference) : TransactionDetailUM
|
||||
data class Error(val onRefreshClick: () -> Unit) : TransactionDetailUM
|
||||
}
|
||||
|
||||
internal enum class TransactionLoadState { Loading, Loaded, Error }
|
||||
|
||||
@Immutable
|
||||
internal sealed interface TangemPayTxHistoryDetailsUiState {
|
||||
data class Legacy(val state: TangemPayTxHistoryDetailsUM) : TangemPayTxHistoryDetailsUiState
|
||||
|
|
|
|||
|
|
@ -17,9 +17,11 @@ import com.tangem.domain.tangempay.repository.TangemPayTxHistoryRepository
|
|||
import com.tangem.domain.visa.model.TangemPayTxHistoryItem
|
||||
import com.tangem.features.tangempay.components.TangemPayTransactionBottomSheetComponent
|
||||
import com.tangem.features.tangempay.entity.TangemPayTxHistoryDetailsUiStates
|
||||
import com.tangem.features.tangempay.entity.TransactionLoadState
|
||||
import com.tangem.features.tangempay.model.transformers.TangemPayTxHistoryDetailsConverter
|
||||
import com.tangem.features.tangempay.model.transformers.TangemPayTxHistoryDetailsConverterV2
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.flow.*
|
||||
import kotlinx.coroutines.launch
|
||||
import javax.inject.Inject
|
||||
|
|
@ -41,16 +43,23 @@ internal class TangemPayTxHistoryDetailsModel @Inject constructor(
|
|||
private val params = paramsContainer.require<TangemPayTransactionBottomSheetComponent.Params>()
|
||||
|
||||
private val transaction = MutableStateFlow(params.transaction)
|
||||
private val transactionLoadState = MutableStateFlow(TransactionLoadState.Loading)
|
||||
private var loadTransactionJob: Job? = null
|
||||
|
||||
val uiState: StateFlow<TangemPayTxHistoryDetailsUiStates> = combine(
|
||||
balanceHidingSettings.isBalanceHidden(),
|
||||
transaction,
|
||||
) { isBalanceHidden, transaction ->
|
||||
buildUiStates(isBalanceHidden, transaction)
|
||||
transactionLoadState,
|
||||
) { isBalanceHidden, transaction, transactionLoadState ->
|
||||
buildUiStates(isBalanceHidden, transaction, transactionLoadState)
|
||||
}.stateIn(
|
||||
scope = modelScope,
|
||||
started = SharingStarted.Eagerly,
|
||||
initialValue = buildUiStates(isBalanceHidden = params.isBalanceHidden, transaction = params.transaction),
|
||||
initialValue = buildUiStates(
|
||||
isBalanceHidden = params.isBalanceHidden,
|
||||
transaction = params.transaction,
|
||||
transactionLoadState = transactionLoadState.value,
|
||||
),
|
||||
)
|
||||
|
||||
init {
|
||||
|
|
@ -64,17 +73,25 @@ internal class TangemPayTxHistoryDetailsModel @Inject constructor(
|
|||
private fun loadTransaction() {
|
||||
val current = params.transaction
|
||||
if (current !is TangemPayTxHistoryItem.Spend) return
|
||||
modelScope.launch {
|
||||
if (loadTransactionJob?.isActive == true) return
|
||||
loadTransactionJob = modelScope.launch {
|
||||
transactionLoadState.value = TransactionLoadState.Loading
|
||||
tangemPayTxHistoryRepository.getTransaction(
|
||||
userWalletId = params.userWalletId,
|
||||
transactionId = current.id,
|
||||
).onRight { loaded -> if (loaded != null) transaction.value = loaded }
|
||||
).onRight { loaded ->
|
||||
if (loaded != null) transaction.value = loaded
|
||||
transactionLoadState.value = TransactionLoadState.Loaded
|
||||
}.onLeft {
|
||||
transactionLoadState.value = TransactionLoadState.Error
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun buildUiStates(
|
||||
isBalanceHidden: Boolean,
|
||||
transaction: TangemPayTxHistoryItem,
|
||||
transactionLoadState: TransactionLoadState,
|
||||
): TangemPayTxHistoryDetailsUiStates {
|
||||
val converterInput = TangemPayTxHistoryDetailsConverter.Input(
|
||||
item = transaction,
|
||||
|
|
@ -89,8 +106,10 @@ internal class TangemPayTxHistoryDetailsModel @Inject constructor(
|
|||
value = TangemPayTxHistoryDetailsConverterV2.Input(
|
||||
item = converterInput.item,
|
||||
isBalanceHidden = converterInput.isBalanceHidden,
|
||||
transactionLoadState = transactionLoadState,
|
||||
onExplorerClick = converterInput.onExplorerClick,
|
||||
onDisputeClick = converterInput.onDisputeClick,
|
||||
onCardRefreshClick = ::loadTransaction,
|
||||
onDismiss = converterInput.onDismiss,
|
||||
),
|
||||
),
|
||||
|
|
|
|||
|
|
@ -15,7 +15,9 @@ import com.tangem.domain.visa.model.TangemPayTxHistoryItem
|
|||
import com.tangem.features.tangempay.details.impl.R
|
||||
import com.tangem.features.tangempay.entity.ButtonState
|
||||
import com.tangem.features.tangempay.entity.TangemPayTxHistoryDetailsUMV2
|
||||
import com.tangem.features.tangempay.entity.TransactionDetailUM
|
||||
import com.tangem.features.tangempay.entity.TransactionLabelUM
|
||||
import com.tangem.features.tangempay.entity.TransactionLoadState
|
||||
import com.tangem.features.tangempay.entity.TransactionStateType
|
||||
import com.tangem.utils.StringsSigns
|
||||
import com.tangem.utils.converter.Converter
|
||||
|
|
@ -34,7 +36,7 @@ internal object TangemPayTxHistoryDetailsConverterV2 :
|
|||
subtitle = transaction.extractDate(),
|
||||
iconState = transaction.extractIcon(),
|
||||
transactionTitle = transaction.extractTransactionTitle(),
|
||||
card = transaction.extractCard(),
|
||||
detail = value.extractDetail(),
|
||||
transactionCategory = transaction.extractTransactionCategory(),
|
||||
mcc = transaction.extractMcc(),
|
||||
transactionAmount = transaction.extractAmount(),
|
||||
|
|
@ -290,8 +292,16 @@ internal object TangemPayTxHistoryDetailsConverterV2 :
|
|||
}
|
||||
}
|
||||
|
||||
private fun TangemPayTxHistoryItem.extractCard(): TextReference? {
|
||||
if (this !is TangemPayTxHistoryItem.Spend) return null
|
||||
private fun Input.extractDetail(): TransactionDetailUM? {
|
||||
val spend = item as? TangemPayTxHistoryItem.Spend ?: return null
|
||||
return when (transactionLoadState) {
|
||||
TransactionLoadState.Loading -> TransactionDetailUM.Loading
|
||||
TransactionLoadState.Error -> TransactionDetailUM.Error(onRefreshClick = onCardRefreshClick)
|
||||
TransactionLoadState.Loaded -> spend.extractCardValue()?.let(TransactionDetailUM::Content)
|
||||
}
|
||||
}
|
||||
|
||||
private fun TangemPayTxHistoryItem.Spend.extractCardValue(): TextReference? {
|
||||
val name = cardName?.takeIf { it.isNotEmpty() }
|
||||
val last4 = cardNumberLast4?.takeIf { it.isNotEmpty() }
|
||||
return when {
|
||||
|
|
@ -312,8 +322,10 @@ internal object TangemPayTxHistoryDetailsConverterV2 :
|
|||
data class Input(
|
||||
val item: TangemPayTxHistoryItem,
|
||||
val isBalanceHidden: Boolean,
|
||||
val transactionLoadState: TransactionLoadState,
|
||||
val onExplorerClick: (String?) -> Unit,
|
||||
val onDisputeClick: () -> Unit,
|
||||
val onCardRefreshClick: () -> Unit,
|
||||
val onDismiss: () -> Unit,
|
||||
)
|
||||
}
|
||||
|
|
@ -2,12 +2,14 @@ package com.tangem.features.tangempay.ui
|
|||
|
||||
import android.content.res.Configuration
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.foundation.text.TextAutoSize
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
|
|
@ -30,23 +32,16 @@ import com.tangem.core.ui.ds.image.TangemIconUM
|
|||
import com.tangem.core.ui.ds.topbar.TangemTopBar
|
||||
import com.tangem.core.ui.ds.topbar.TangemTopBarType
|
||||
import com.tangem.core.ui.ds2.button.TangemButton
|
||||
import com.tangem.core.ui.ds2.row.TangemRow
|
||||
import com.tangem.core.ui.ds2.row.TangemRowContentLead
|
||||
import com.tangem.core.ui.ds2.row.TangemRowText
|
||||
import com.tangem.core.ui.ds2.row.TangemRowTextRole
|
||||
import com.tangem.core.ui.extensions.orMaskWithStars
|
||||
import com.tangem.core.ui.extensions.resolveReference
|
||||
import com.tangem.core.ui.extensions.resourceReference
|
||||
import com.tangem.core.ui.extensions.stringReference
|
||||
import com.tangem.core.ui.ds2.row.*
|
||||
import com.tangem.core.ui.ds2.shimmers.TangemShimmer
|
||||
import com.tangem.core.ui.extensions.*
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.core.ui.res.TangemThemePreviewRedesign
|
||||
import com.tangem.core.ui.res.generated.icons.Icons
|
||||
import com.tangem.core.ui.res.generated.icons.ic_arrow_down_24
|
||||
import com.tangem.core.ui.res.generated.icons.ic_arrow_refresh_20
|
||||
import com.tangem.features.tangempay.details.impl.R
|
||||
import com.tangem.features.tangempay.entity.ButtonState
|
||||
import com.tangem.features.tangempay.entity.TangemPayTxHistoryDetailsUMV2
|
||||
import com.tangem.features.tangempay.entity.TransactionLabelUM
|
||||
import com.tangem.features.tangempay.entity.TransactionStateType
|
||||
import com.tangem.features.tangempay.entity.*
|
||||
|
||||
@Suppress("LongMethod")
|
||||
@Composable
|
||||
|
|
@ -233,23 +228,11 @@ internal fun TransactionLabel(label: TransactionLabelUM, modifier: Modifier = Mo
|
|||
@Composable
|
||||
private fun TransactionDetailsBlock(state: TangemPayTxHistoryDetailsUMV2, modifier: Modifier = Modifier) {
|
||||
Column(modifier = modifier) {
|
||||
if (state.card != null) {
|
||||
TangemRow(
|
||||
divider = true,
|
||||
contentLead = TangemRowContentLead.Start,
|
||||
titleSlot = {
|
||||
TangemRowText(
|
||||
text = resourceReference(R.string.tangempay_common_card),
|
||||
role = TangemRowTextRole.Title,
|
||||
)
|
||||
},
|
||||
valueSlot = {
|
||||
TangemRowText(
|
||||
text = state.card,
|
||||
role = TangemRowTextRole.Value,
|
||||
)
|
||||
},
|
||||
)
|
||||
when (val detail = state.detail) {
|
||||
null -> Unit
|
||||
TransactionDetailUM.Loading -> CardRowShimmer()
|
||||
is TransactionDetailUM.Content -> CardRow(value = detail.card)
|
||||
is TransactionDetailUM.Error -> CardRowError(onRefreshClick = detail.onRefreshClick)
|
||||
}
|
||||
TangemRow(
|
||||
divider = state.mcc != null,
|
||||
|
|
@ -287,6 +270,79 @@ private fun TransactionDetailsBlock(state: TangemPayTxHistoryDetailsUMV2, modifi
|
|||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun CardRow(value: TextReference, modifier: Modifier = Modifier) {
|
||||
TangemRow(
|
||||
modifier = modifier,
|
||||
divider = true,
|
||||
contentLead = TangemRowContentLead.Start,
|
||||
titleSlot = {
|
||||
TangemRowText(
|
||||
text = resourceReference(R.string.tangempay_common_card),
|
||||
role = TangemRowTextRole.Title,
|
||||
)
|
||||
},
|
||||
valueSlot = {
|
||||
TangemRowText(
|
||||
text = value,
|
||||
role = TangemRowTextRole.Value,
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun CardRowShimmer(modifier: Modifier = Modifier) {
|
||||
TangemRow(
|
||||
modifier = modifier,
|
||||
divider = true,
|
||||
contentLead = TangemRowContentLead.Start,
|
||||
titleSlot = {
|
||||
TangemShimmer(style = TangemTheme.typography3.body.medium)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun CardRowError(onRefreshClick: () -> Unit, modifier: Modifier = Modifier) {
|
||||
TangemRow(
|
||||
modifier = modifier,
|
||||
divider = true,
|
||||
contentLead = TangemRowContentLead.Start,
|
||||
verticalAlignment = TangemRowVerticalAlignment.Center,
|
||||
titleSlot = {
|
||||
TangemRowText(
|
||||
text = resourceReference(R.string.tangempay_common_card),
|
||||
role = TangemRowTextRole.Title,
|
||||
)
|
||||
},
|
||||
valueSlot = {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.clip(RoundedCornerShape(4.dp))
|
||||
.clickable(onClick = onRefreshClick)
|
||||
.padding(2.dp),
|
||||
horizontalArrangement = Arrangement.spacedBy(4.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
Text(
|
||||
text = resourceReference(R.string.tangempay_common_error_loading).resolveReference(),
|
||||
style = TangemTheme.typography3.body.medium,
|
||||
color = TangemTheme.colors3.text.status.error,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
)
|
||||
Icon(
|
||||
modifier = Modifier.size(20.dp),
|
||||
imageVector = Icons.ic_arrow_refresh_20,
|
||||
contentDescription = null,
|
||||
tint = TangemTheme.colors3.icon.status.error,
|
||||
)
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
@Preview(device = Devices.PIXEL_7_PRO)
|
||||
@Preview(device = Devices.PIXEL_7_PRO, uiMode = Configuration.UI_MODE_NIGHT_YES)
|
||||
@Composable
|
||||
|
|
@ -307,7 +363,7 @@ private class TangemPayTxHistoryDetailsUMProviderV2 :
|
|||
subtitle = stringReference("12 June 2026, 12:40"),
|
||||
iconState = TangemIconUM.Icon(iconRes = R.drawable.ic_category_24),
|
||||
transactionTitle = stringReference("Starbucks"),
|
||||
card = stringReference("Basic card *9092"),
|
||||
detail = TransactionDetailUM.Content(stringReference("Basic card *9092")),
|
||||
transactionCategory = stringReference("Food and drinks"),
|
||||
mcc = stringReference("5814"),
|
||||
transactionAmount = "-$5.86",
|
||||
|
|
@ -329,7 +385,7 @@ private class TangemPayTxHistoryDetailsUMProviderV2 :
|
|||
subtitle = stringReference("12 June 2026, 12:40"),
|
||||
iconState = TangemIconUM.Icon(iconRes = R.drawable.ic_category_24),
|
||||
transactionTitle = stringReference("NuCaloric"),
|
||||
card = stringReference("Basic card *9092"),
|
||||
detail = TransactionDetailUM.Content(stringReference("Basic card *9092")),
|
||||
transactionCategory = stringReference("Groceries"),
|
||||
mcc = stringReference("0000"),
|
||||
transactionAmount = "-$820.52",
|
||||
|
|
@ -352,7 +408,7 @@ private class TangemPayTxHistoryDetailsUMProviderV2 :
|
|||
subtitle = stringReference("12 June 2026, 12:40"),
|
||||
iconState = TangemIconUM.Icon(iconRes = R.drawable.ic_category_24),
|
||||
transactionTitle = stringReference("Starbucks"),
|
||||
card = stringReference("Basic card *9092"),
|
||||
detail = TransactionDetailUM.Content(stringReference("Basic card *9092")),
|
||||
transactionCategory = stringReference("Food and drinks"),
|
||||
mcc = null,
|
||||
transactionAmount = "-$5.86",
|
||||
|
|
@ -374,7 +430,7 @@ private class TangemPayTxHistoryDetailsUMProviderV2 :
|
|||
subtitle = stringReference("12 June 2026, 12:40"),
|
||||
iconState = TangemIconUM.Icon(iconRes = R.drawable.ic_percent_24),
|
||||
transactionTitle = stringReference("Service fees"),
|
||||
card = null,
|
||||
detail = null,
|
||||
transactionCategory = stringReference("Service fees"),
|
||||
mcc = null,
|
||||
transactionAmount = "-$5.86",
|
||||
|
|
@ -397,7 +453,7 @@ private class TangemPayTxHistoryDetailsUMProviderV2 :
|
|||
subtitle = stringReference("12 June 2026, 12:40"),
|
||||
iconState = TangemIconUM.Icon(imageVector = Icons.ic_arrow_down_24),
|
||||
transactionTitle = resourceReference(R.string.common_transfer),
|
||||
card = null,
|
||||
detail = null,
|
||||
transactionCategory = resourceReference(R.string.common_transfer),
|
||||
mcc = null,
|
||||
transactionAmount = "+$20",
|
||||
|
|
@ -409,5 +465,49 @@ private class TangemPayTxHistoryDetailsUMProviderV2 :
|
|||
),
|
||||
dismiss = {},
|
||||
),
|
||||
TangemPayTxHistoryDetailsUMV2(
|
||||
isBalanceHidden = false,
|
||||
title = resourceReference(R.string.tangem_pay_purchase),
|
||||
subtitle = stringReference("12 June 2026, 12:40"),
|
||||
iconState = TangemIconUM.Icon(iconRes = R.drawable.ic_category_24),
|
||||
transactionTitle = stringReference("Starbucks"),
|
||||
detail = TransactionDetailUM.Loading,
|
||||
transactionCategory = stringReference("Food and drinks"),
|
||||
mcc = stringReference("5814"),
|
||||
transactionAmount = "-$5.86",
|
||||
localTransactionText = null,
|
||||
label = TransactionLabelUM(
|
||||
transactionStateType = TransactionStateType.Completed,
|
||||
icon = TangemIconUM.Icon(iconRes = R.drawable.ic_token_info_24),
|
||||
title = resourceReference(R.string.tangem_pay_status_completed),
|
||||
),
|
||||
buttonState = ButtonState(
|
||||
text = resourceReference(R.string.tangem_pay_get_help),
|
||||
onClick = {},
|
||||
),
|
||||
dismiss = {},
|
||||
),
|
||||
TangemPayTxHistoryDetailsUMV2(
|
||||
isBalanceHidden = false,
|
||||
title = resourceReference(R.string.tangem_pay_purchase),
|
||||
subtitle = stringReference("12 June 2026, 12:40"),
|
||||
iconState = TangemIconUM.Icon(iconRes = R.drawable.ic_category_24),
|
||||
transactionTitle = stringReference("Starbucks"),
|
||||
detail = TransactionDetailUM.Error(onRefreshClick = {}),
|
||||
transactionCategory = stringReference("Food and drinks"),
|
||||
mcc = stringReference("5814"),
|
||||
transactionAmount = "-$5.86",
|
||||
localTransactionText = null,
|
||||
label = TransactionLabelUM(
|
||||
transactionStateType = TransactionStateType.Completed,
|
||||
icon = TangemIconUM.Icon(iconRes = R.drawable.ic_token_info_24),
|
||||
title = resourceReference(R.string.tangem_pay_status_completed),
|
||||
),
|
||||
buttonState = ButtonState(
|
||||
text = resourceReference(R.string.tangem_pay_get_help),
|
||||
onClick = {},
|
||||
),
|
||||
dismiss = {},
|
||||
),
|
||||
),
|
||||
)
|
||||
|
|
@ -3,6 +3,46 @@ package com.tangem.features.tangempay
|
|||
import com.tangem.domain.models.pay.TangemPayCard
|
||||
import com.tangem.domain.models.pay.TangemPayCardFrozenState
|
||||
import com.tangem.domain.models.pay.TangemPayCardState
|
||||
import com.tangem.domain.visa.model.TangemPayTxHistoryItem
|
||||
import org.joda.time.DateTime
|
||||
import java.math.BigDecimal
|
||||
import java.util.Currency
|
||||
|
||||
internal fun spendTransaction(
|
||||
id: String = "tx_1",
|
||||
cardName: String? = "Basic card",
|
||||
cardNumberLast4: String? = "9092",
|
||||
status: TangemPayTxHistoryItem.Status = TangemPayTxHistoryItem.Status.COMPLETED,
|
||||
): TangemPayTxHistoryItem.Spend = TangemPayTxHistoryItem.Spend(
|
||||
id = id,
|
||||
jsonRepresentation = "{}",
|
||||
date = DateTime(0L),
|
||||
amount = BigDecimal.TEN,
|
||||
currency = Currency.getInstance("USD"),
|
||||
authorizedAmount = BigDecimal.TEN,
|
||||
localAmount = null,
|
||||
localCurrency = null,
|
||||
enrichedMerchantName = null,
|
||||
merchantName = "Merchant",
|
||||
enrichedMerchantCategory = null,
|
||||
merchantCategoryCode = null,
|
||||
merchantCategory = null,
|
||||
status = status,
|
||||
enrichedMerchantIconUrl = null,
|
||||
declinedReason = null,
|
||||
cardName = cardName,
|
||||
cardNumberLast4 = cardNumberLast4,
|
||||
)
|
||||
|
||||
internal fun paymentTransaction(id: String = "tx_payment_1"): TangemPayTxHistoryItem.Payment =
|
||||
TangemPayTxHistoryItem.Payment(
|
||||
id = id,
|
||||
jsonRepresentation = "{}",
|
||||
date = DateTime(0L),
|
||||
amount = BigDecimal.TEN,
|
||||
currency = Currency.getInstance("USD"),
|
||||
transactionHash = null,
|
||||
)
|
||||
|
||||
internal fun tangemPayCard(
|
||||
id: String = "card_1",
|
||||
|
|
|
|||
|
|
@ -0,0 +1,195 @@
|
|||
package com.tangem.features.tangempay.model
|
||||
|
||||
import android.text.format.DateFormat
|
||||
import arrow.core.Either
|
||||
import arrow.core.left
|
||||
import arrow.core.right
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import com.tangem.core.decompose.model.MutableParamsContainer
|
||||
import com.tangem.core.ui.extensions.stringReference
|
||||
import com.tangem.domain.balancehiding.GetBalanceHidingSettingsUseCase
|
||||
import com.tangem.domain.models.wallet.UserWalletId
|
||||
import com.tangem.domain.tangempay.repository.TangemPayTxHistoryRepository
|
||||
import com.tangem.domain.visa.error.VisaApiError
|
||||
import com.tangem.domain.visa.model.TangemPayTxHistoryItem
|
||||
import com.tangem.features.tangempay.components.TangemPayTransactionBottomSheetComponent
|
||||
import com.tangem.features.tangempay.entity.TransactionDetailUM
|
||||
import com.tangem.features.tangempay.paymentTransaction
|
||||
import com.tangem.features.tangempay.spendTransaction
|
||||
import com.tangem.utils.coroutines.TestingCoroutineDispatcherProvider
|
||||
import io.mockk.clearMocks
|
||||
import io.mockk.coEvery
|
||||
import io.mockk.coVerify
|
||||
import io.mockk.every
|
||||
import io.mockk.mockk
|
||||
import io.mockk.mockkStatic
|
||||
import io.mockk.unmockkStatic
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.awaitCancellation
|
||||
import kotlinx.coroutines.flow.flowOf
|
||||
import kotlinx.coroutines.test.StandardTestDispatcher
|
||||
import kotlinx.coroutines.test.TestScope
|
||||
import kotlinx.coroutines.test.advanceUntilIdle
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.jupiter.api.AfterEach
|
||||
import org.junit.jupiter.api.BeforeEach
|
||||
import org.junit.jupiter.api.Test
|
||||
import org.junit.jupiter.api.TestInstance
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
|
||||
internal class TangemPayTxHistoryDetailsModelTest {
|
||||
|
||||
private val userWalletId = UserWalletId("123")
|
||||
private val repository: TangemPayTxHistoryRepository = mockk()
|
||||
private val balanceHidingSettings: GetBalanceHidingSettingsUseCase = mockk()
|
||||
|
||||
@BeforeEach
|
||||
fun setup() {
|
||||
clearMocks(repository, balanceHidingSettings)
|
||||
every { balanceHidingSettings.isBalanceHidden() } returns flowOf(false)
|
||||
mockkStatic(DateFormat::class)
|
||||
every { DateFormat.getBestDateTimePattern(any(), any()) } answers { secondArg() }
|
||||
}
|
||||
|
||||
@AfterEach
|
||||
fun tearDown() {
|
||||
unmockkStatic(DateFormat::class)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN spend transaction WHEN detail fetch in flight THEN card is Loading`() = runTest {
|
||||
// Arrange
|
||||
coEvery { repository.getTransaction(any(), any()) } coAnswers { awaitCancellation() }
|
||||
val model = createModel(testScope = this, transaction = spendTransaction())
|
||||
|
||||
// Act
|
||||
advanceUntilIdle()
|
||||
|
||||
// Assert
|
||||
assertThat(model.uiState.value.redesign.detail).isEqualTo(TransactionDetailUM.Loading)
|
||||
model.onDestroy()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN spend transaction WHEN detail fetch succeeds THEN card shows loaded name and last4`() = runTest {
|
||||
// Arrange — the sheet opens with no card info, the detail fetch back-fills it
|
||||
val loaded = spendTransaction(cardName = "Loaded card", cardNumberLast4 = "4321")
|
||||
coEvery { repository.getTransaction(any(), any()) } returns loaded.right()
|
||||
val model = createModel(
|
||||
testScope = this,
|
||||
transaction = spendTransaction(cardName = null, cardNumberLast4 = null),
|
||||
)
|
||||
|
||||
// Act
|
||||
advanceUntilIdle()
|
||||
|
||||
// Assert
|
||||
assertThat(model.uiState.value.redesign.detail)
|
||||
.isEqualTo(TransactionDetailUM.Content(stringReference("Loaded card *4321")))
|
||||
model.onDestroy()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN spend transaction WHEN detail fetch succeeds without card info THEN card row is hidden`() = runTest {
|
||||
// Arrange
|
||||
coEvery { repository.getTransaction(any(), any()) } returns
|
||||
spendTransaction(cardName = null, cardNumberLast4 = null).right()
|
||||
val model = createModel(testScope = this, transaction = spendTransaction())
|
||||
|
||||
// Act
|
||||
advanceUntilIdle()
|
||||
|
||||
// Assert
|
||||
assertThat(model.uiState.value.redesign.detail).isNull()
|
||||
model.onDestroy()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN spend transaction WHEN detail fetch fails THEN card is Error`() = runTest {
|
||||
// Arrange
|
||||
coEvery { repository.getTransaction(any(), any()) } returns VisaApiError.Unspecified.left()
|
||||
val model = createModel(testScope = this, transaction = spendTransaction())
|
||||
|
||||
// Act
|
||||
advanceUntilIdle()
|
||||
|
||||
// Assert
|
||||
assertThat(model.uiState.value.redesign.detail).isInstanceOf(TransactionDetailUM.Error::class.java)
|
||||
model.onDestroy()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN card error WHEN refresh clicked and fetch succeeds THEN card shows value`() = runTest {
|
||||
// Arrange — first fetch fails, retry succeeds
|
||||
val loaded = spendTransaction(cardName = "Loaded card", cardNumberLast4 = "4321")
|
||||
coEvery {
|
||||
repository.getTransaction(any(), any())
|
||||
} returnsMany listOf<Either<VisaApiError, TangemPayTxHistoryItem?>>(
|
||||
VisaApiError.Unspecified.left(),
|
||||
loaded.right(),
|
||||
)
|
||||
val model = createModel(testScope = this, transaction = spendTransaction())
|
||||
advanceUntilIdle()
|
||||
val errorState = model.uiState.value.redesign.detail
|
||||
assertThat(errorState).isInstanceOf(TransactionDetailUM.Error::class.java)
|
||||
|
||||
// Act — tap refresh
|
||||
(errorState as TransactionDetailUM.Error).onRefreshClick()
|
||||
advanceUntilIdle()
|
||||
|
||||
// Assert
|
||||
assertThat(model.uiState.value.redesign.detail)
|
||||
.isEqualTo(TransactionDetailUM.Content(stringReference("Loaded card *4321")))
|
||||
coVerify(exactly = 2) { repository.getTransaction(any(), any()) }
|
||||
model.onDestroy()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN non-spend transaction WHEN model created THEN card is hidden and detail not fetched`() = runTest {
|
||||
// Arrange
|
||||
val model = createModel(testScope = this, transaction = paymentTransaction())
|
||||
|
||||
// Act
|
||||
advanceUntilIdle()
|
||||
|
||||
// Assert
|
||||
assertThat(model.uiState.value.redesign.detail).isNull()
|
||||
coVerify(exactly = 0) { repository.getTransaction(any(), any()) }
|
||||
model.onDestroy()
|
||||
}
|
||||
|
||||
private fun createModel(
|
||||
testScope: TestScope,
|
||||
transaction: TangemPayTxHistoryItem,
|
||||
): TangemPayTxHistoryDetailsModel {
|
||||
val params = TangemPayTransactionBottomSheetComponent.Params(
|
||||
isBalanceHidden = false,
|
||||
transaction = transaction,
|
||||
userWalletId = userWalletId,
|
||||
customerId = "customer_1",
|
||||
onDismiss = {},
|
||||
)
|
||||
return TangemPayTxHistoryDetailsModel(
|
||||
dispatchers = testScope.createTestingCoroutineDispatcherProvider(),
|
||||
getWalletMetaInfoUseCase = mockk(relaxed = true),
|
||||
sendFeedbackEmailUseCase = mockk(relaxed = true),
|
||||
urlOpener = mockk(relaxed = true),
|
||||
balanceHidingSettings = balanceHidingSettings,
|
||||
tangemPayTxHistoryRepository = repository,
|
||||
analytics = mockk(relaxed = true),
|
||||
paramsContainer = MutableParamsContainer(params),
|
||||
)
|
||||
}
|
||||
|
||||
private fun TestScope.createTestingCoroutineDispatcherProvider(): TestingCoroutineDispatcherProvider {
|
||||
val testDispatcher = StandardTestDispatcher(testScheduler)
|
||||
return TestingCoroutineDispatcherProvider(
|
||||
main = testDispatcher,
|
||||
mainImmediate = testDispatcher,
|
||||
io = testDispatcher,
|
||||
default = testDispatcher,
|
||||
single = testDispatcher,
|
||||
)
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue