diff --git a/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/components/TangemPayDetailsComponent.kt b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/components/TangemPayDetailsComponent.kt index 2fc1148589..64cefd4620 100644 --- a/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/components/TangemPayDetailsComponent.kt +++ b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/components/TangemPayDetailsComponent.kt @@ -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, ), ) diff --git a/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/entity/TangemPayDetailsNavigation.kt b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/entity/TangemPayDetailsNavigation.kt index 82b020d3f7..6731577156 100644 --- a/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/entity/TangemPayDetailsNavigation.kt +++ b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/entity/TangemPayDetailsNavigation.kt @@ -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 diff --git a/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/model/TangemPayDetailsModel.kt b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/model/TangemPayDetailsModel.kt index c643dcdfea..635c7699e9 100644 --- a/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/model/TangemPayDetailsModel.kt +++ b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/model/TangemPayDetailsModel.kt @@ -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, ), ) } diff --git a/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/utils/PaymentAccountStatusExt.kt b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/utils/PaymentAccountStatusExt.kt index 3c67567d87..d5f47b70a5 100644 --- a/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/utils/PaymentAccountStatusExt.kt +++ b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/utils/PaymentAccountStatusExt.kt @@ -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 AccountStatus.Payment.ifLoadedOrNull(call: (PaymentAccountStatusValue.Loaded) -> T): T? { val value = value return if (value is PaymentAccountStatusValue.Loaded) { diff --git a/features/tangempay/details/impl/src/test/kotlin/com/tangem/features/tangempay/model/TangemPayDetailsModelTest.kt b/features/tangempay/details/impl/src/test/kotlin/com/tangem/features/tangempay/model/TangemPayDetailsModelTest.kt index 51ec8a4352..1935328a5e 100644 --- a/features/tangempay/details/impl/src/test/kotlin/com/tangem/features/tangempay/model/TangemPayDetailsModelTest.kt +++ b/features/tangempay/details/impl/src/test/kotlin/com/tangem/features/tangempay/model/TangemPayDetailsModelTest.kt @@ -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(relaxed = true)) + + // Assert + verify(exactly = if (case.expectedOpened) 1 else 0) { + analytics.send(ofType()) + } + 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, + ) + } } } \ No newline at end of file