Updated on 2026-08-14

This commit is contained in:
Tangem 2026-07-23 11:44:11 +03:00
parent 6d9297f4f4
commit 5e8785f5b8
5 changed files with 119 additions and 12 deletions

View file

@ -24,7 +24,6 @@ import com.tangem.features.tangempay.entity.TangemPayDetailsNavigation
import com.tangem.features.tangempay.model.TangemPayDetailsModel
import com.tangem.features.tangempay.ui.TangemPayDetailsScreenV2
import com.tangem.features.tangempay.utils.VA_DAILY_DEPOSIT_LIMIT_PLACEHOLDER
import com.tangem.features.tangempay.utils.requireLoaded
import com.tangem.features.tangempay.utils.toRequisitesRows
import com.tangem.features.tangempay.utils.userWalletId
import com.tangem.features.tokendetails.ExpressTransactionsComponent
@ -125,8 +124,8 @@ internal class TangemPayDetailsComponent(
params = TangemPayTransactionBottomSheetComponent.Params(
transaction = navigation.transaction,
isBalanceHidden = navigation.isBalanceHidden,
userWalletId = params.initialStatus.userWalletId,
customerId = params.initialStatus.requireLoaded().customerId,
userWalletId = navigation.userWalletId,
customerId = navigation.customerId,
onDismiss = model.bottomSheetNavigation::dismiss,
),
)

View file

@ -43,6 +43,8 @@ internal sealed class TangemPayDetailsNavigation {
data class TransactionDetails(
val transaction: TangemPayTxHistoryItem,
val isBalanceHidden: Boolean,
val userWalletId: UserWalletId,
val customerId: String,
) : TangemPayDetailsNavigation()
@Serializable

View file

@ -412,6 +412,13 @@ internal class TangemPayDetailsModel @Inject constructor(
}
override fun onTransactionClick(item: TangemPayTxHistoryItem) {
val customerId = currentStatus.value.customerId ?: run {
TangemLogger.withTag("TangemPayDetailsModel").w(
"CustomerId is null, cannot open transaction details. " +
"Status: ${currentStatus.value.value.typeName}",
)
return
}
val (type, status) = when (item) {
is TangemPayTxHistoryItem.Collateral -> "collateral" to "unknown"
is TangemPayTxHistoryItem.Fee -> "fee" to "unknown"
@ -423,6 +430,8 @@ internal class TangemPayDetailsModel @Inject constructor(
configuration = TangemPayDetailsNavigation.TransactionDetails(
transaction = item,
isBalanceHidden = uiState.value.isBalanceHidden,
userWalletId = userWalletId,
customerId = customerId,
),
)
}

View file

@ -18,6 +18,30 @@ internal val AccountStatus.Payment.cryptoCurrency: CryptoCurrency.Token
else -> error("TangemPayDetails opened with unsupported status: $v")
}
internal val AccountStatus.Payment.customerId: String?
get() = when (val v = value) {
is PaymentAccountStatusValue.Loaded -> v.customerId
is PaymentAccountStatusValue.Deactivated -> v.customerId
else -> null
}
internal val PaymentAccountStatusValue.typeName: String
get() = when (this) {
PaymentAccountStatusValue.Empty -> "Empty"
PaymentAccountStatusValue.Loading -> "Loading"
PaymentAccountStatusValue.NotCreated -> "NotCreated"
is PaymentAccountStatusValue.UnderReview -> "UnderReview"
is PaymentAccountStatusValue.IssuingCard -> "IssuingCard"
is PaymentAccountStatusValue.AwaitingPlanSelection -> "AwaitingPlanSelection"
is PaymentAccountStatusValue.Inactive -> "Inactive"
is PaymentAccountStatusValue.Deactivated -> "Deactivated"
is PaymentAccountStatusValue.Loaded -> "Loaded"
PaymentAccountStatusValue.Error.ExposedDevice -> "Error.ExposedDevice"
PaymentAccountStatusValue.Error.Unavailable -> "Error.Unavailable"
PaymentAccountStatusValue.Error.NotSynced -> "Error.NotSynced"
is PaymentAccountStatusValue.Error.CardIssueFailed -> "Error.CardIssueFailed"
}
internal val AccountStatus.Payment.tariffPlan: TangemPayCustomerTariffPlan?
get() = when (val v = value) {
is PaymentAccountStatusValue.Inactive -> v.tariffPlan.tariff
@ -30,10 +54,6 @@ internal val AccountStatus.Payment.tariffPlan: TangemPayCustomerTariffPlan?
internal val PaymentAccountStatusValue.Loaded.isFresh: Boolean
get() = source.isActual() && error == null
internal fun AccountStatus.Payment.requireLoaded(): PaymentAccountStatusValue.Loaded =
value as? PaymentAccountStatusValue.Loaded
?: error("Card-detail subflow requires Loaded status, got ${value::class.simpleName}")
internal inline fun <T> AccountStatus.Payment.ifLoadedOrNull(call: (PaymentAccountStatusValue.Loaded) -> T): T? {
val value = value
return if (value is PaymentAccountStatusValue.Loaded) {

View file

@ -2,6 +2,7 @@ package com.tangem.features.tangempay.model
import arrow.core.right
import com.google.common.truth.Truth.assertThat
import com.tangem.core.analytics.api.AnalyticsEventHandler
import com.tangem.core.decompose.model.MutableParamsContainer
import com.tangem.domain.models.StatusSource
import com.tangem.domain.models.account.AccountStatus
@ -11,6 +12,8 @@ import com.tangem.domain.models.pay.TangemPayDetailsInitialRoute
import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.domain.pay.flow.PaymentAccountStatusSupplier
import com.tangem.domain.pay.repository.TangemPayCardDetailsRepository
import com.tangem.domain.tangempay.TangemPayAnalyticsEvents
import com.tangem.domain.visa.model.TangemPayTxHistoryItem
import com.tangem.features.tangempay.addFundsButton
import com.tangem.features.tangempay.components.TangemPayDetailsContainerComponent
import com.tangem.features.tangempay.entity.TangemPayDetailsBalanceBlockState
@ -20,6 +23,7 @@ import com.tangem.utils.coroutines.TestingCoroutineDispatcherProvider
import io.mockk.coEvery
import io.mockk.every
import io.mockk.mockk
import io.mockk.verify
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.test.StandardTestDispatcher
@ -37,6 +41,7 @@ internal class TangemPayDetailsModelTest {
private val paymentAccountStatusSupplier: PaymentAccountStatusSupplier = mockk()
private val cardDetailsRepository: TangemPayCardDetailsRepository = mockk(relaxed = true)
private val analytics: AnalyticsEventHandler = mockk(relaxed = true)
@ParameterizedTest
@MethodSource("provideMutedCases")
@ -58,16 +63,37 @@ internal class TangemPayDetailsModelTest {
model.onDestroy()
}
@ParameterizedTest
@MethodSource("provideTransactionClickCases")
fun `GIVEN status WHEN transaction clicked THEN opens details only when customerId present`(
case: TransactionClickCase,
) = runTest {
// Arrange
val model = createModel(testScope = this, statusValue = case.status)
advanceUntilIdle()
// Act
model.onTransactionClick(mockk<TangemPayTxHistoryItem.Payment>(relaxed = true))
// Assert
verify(exactly = if (case.expectedOpened) 1 else 0) {
analytics.send(ofType<TangemPayAnalyticsEvents.TransactionInListClicked>())
}
model.onDestroy()
}
private fun createModel(
testScope: TestScope,
statusSource: StatusSource,
frozenState: TangemPayCardFrozenState,
availableForWithdrawal: BigDecimal,
statusSource: StatusSource = StatusSource.ACTUAL,
frozenState: TangemPayCardFrozenState = TangemPayCardFrozenState.Unfrozen,
availableForWithdrawal: BigDecimal = BigDecimal.ZERO,
accountError: PaymentAccountStatusValue.Error? = null,
statusValue: PaymentAccountStatusValue? = null,
): TangemPayDetailsModel {
val loaded: PaymentAccountStatusValue.Loaded = mockk(relaxed = true) {
every { source } returns statusSource
every { error } returns accountError
every { customerId } returns "customer-id"
every { cards } returns listOf(tangemPayCard())
every { balance } returns PaymentAccountStatusValue.Balance(
fiatBalance = PaymentAccountStatusValue.FiatBalance(
@ -85,7 +111,7 @@ internal class TangemPayDetailsModelTest {
)
}
val paymentStatus: AccountStatus.Payment = mockk(relaxed = true) {
every { value } returns loaded
every { value } returns (statusValue ?: loaded)
every { account } returns mockk(relaxed = true) {
every { userWalletId } returns this@TangemPayDetailsModelTest.userWalletId
}
@ -103,7 +129,7 @@ internal class TangemPayDetailsModelTest {
paramsContainer = MutableParamsContainer(params),
paymentAccountStatusSupplier = paymentAccountStatusSupplier,
dispatchers = testScope.createTestingCoroutineDispatcherProvider(),
analytics = mockk(relaxed = true),
analytics = analytics,
router = mockk(relaxed = true),
urlOpener = mockk(relaxed = true),
cardDetailsRepository = cardDetailsRepository,
@ -150,6 +176,14 @@ internal class TangemPayDetailsModelTest {
val accountError: PaymentAccountStatusValue.Error? = null,
)
internal data class TransactionClickCase(
val name: String,
val status: PaymentAccountStatusValue?,
val expectedOpened: Boolean,
) {
override fun toString(): String = name
}
private companion object {
@JvmStatic
fun provideFreezeCases() = listOf(
@ -195,5 +229,48 @@ internal class TangemPayDetailsModelTest {
expectedMuted = true,
),
)
@JvmStatic
fun provideTransactionClickCases() = listOf(
TransactionClickCase(
name = "deactivated account (has customerId) -> opens",
status = deactivatedStatus(id = "customer-id"),
expectedOpened = true,
),
TransactionClickCase(
name = "loaded account (has customerId) -> opens",
status = null,
expectedOpened = true,
),
TransactionClickCase(
name = "loading status (no customerId) -> ignored",
status = PaymentAccountStatusValue.Loading,
expectedOpened = false,
),
TransactionClickCase(
name = "not created status (no customerId) -> ignored",
status = PaymentAccountStatusValue.NotCreated,
expectedOpened = false,
),
)
private fun deactivatedStatus(id: String): PaymentAccountStatusValue.Deactivated = mockk(relaxed = true) {
every { source } returns StatusSource.ACTUAL
every { customerId } returns id
every { balance } returns PaymentAccountStatusValue.Balance(
fiatBalance = PaymentAccountStatusValue.FiatBalance(
availableBalance = BigDecimal.ZERO,
currency = "USD",
),
cryptoBalance = PaymentAccountStatusValue.CryptoBalance(
id = "id",
chainId = 1L,
depositAddress = "address",
tokenContractAddress = "contract",
balance = BigDecimal.ZERO,
),
availableForWithdrawal = BigDecimal.ZERO,
)
}
}
}