Updated on 2026-08-14

This commit is contained in:
Tangem 2026-07-08 13:19:07 +05:00
parent 8e9417d04c
commit ee846a2738
10 changed files with 147 additions and 7 deletions

View file

@ -125,6 +125,9 @@ internal class TangemPayCardPageScreenComponent(
dailyDepositLimit = VA_DAILY_DEPOSIT_LIMIT_PLACEHOLDER,
shouldSkipIntro = true,
listener = VirtualAccountAddFundsListener { model.bottomSheetNavigation.dismiss() },
onDetailsShown = model::onVaBankingDetailsShown,
onShareClicked = model::onVaShareDetailsClicked,
onFieldCopied = model::onVaFieldCopied,
),
)
is TangemPayCardNavigation.Receive -> tokenReceiveComponentFactory.create(

View file

@ -154,6 +154,9 @@ internal class TangemPayDetailsComponent(
dailyDepositLimit = VA_DAILY_DEPOSIT_LIMIT_PLACEHOLDER,
shouldSkipIntro = true,
listener = VirtualAccountAddFundsListener { model.bottomSheetNavigation.dismiss() },
onDetailsShown = model::onVaBankingDetailsShown,
onShareClicked = model::onVaShareDetailsClicked,
onFieldCopied = model::onVaFieldCopied,
),
)
is TangemPayDetailsNavigation.IssueAdditionalCard -> TangemPayIssueAdditionalCardComponent(

View file

@ -1,12 +1,14 @@
package com.tangem.features.tangempay.model
import androidx.compose.runtime.Stable
import com.tangem.core.analytics.api.AnalyticsEventHandler
import com.tangem.core.decompose.di.ModelScoped
import com.tangem.core.decompose.model.Model
import com.tangem.core.decompose.model.ParamsContainer
import com.tangem.domain.models.ReceiveAddressModel
import com.tangem.domain.models.ReceiveAddressModel.DisplayType
import com.tangem.domain.pay.model.TangemPayTopUpData
import com.tangem.domain.tangempay.TangemPayAnalyticsEvents
import com.tangem.features.tangempay.TangemPayFeatureToggles
import com.tangem.features.tangempay.components.TangemPayAddFundsComponent
import com.tangem.features.tangempay.entity.TangemPayAddFundsUM
@ -21,13 +23,22 @@ internal class TangemPayAddFundsModel @Inject constructor(
paramsContainer: ParamsContainer,
override val dispatchers: CoroutineDispatcherProvider,
private val tangemPayFeatureToggles: TangemPayFeatureToggles,
private val virtualAccountToggles: VirtualAccountFeatureToggles,
virtualAccountToggles: VirtualAccountFeatureToggles,
analytics: AnalyticsEventHandler,
) : Model() {
private val params = paramsContainer.require<TangemPayAddFundsComponent.Params>()
private val isBankTransferShown = virtualAccountToggles.isVaMvp0Enabled && params.virtualAccountOnramp != null
val uiState: TangemPayAddFundsUM = getInitialState()
init {
if (isBankTransferShown) {
analytics.send(TangemPayAnalyticsEvents.VaTopupButtonShowed())
}
}
private fun getInitialState(): TangemPayAddFundsUM {
val data = TangemPayTopUpData(
currency = params.cryptoCurrency,
@ -45,7 +56,7 @@ internal class TangemPayAddFundsModel @Inject constructor(
return TangemPayAddFundsUMConverter(
listener = params.listener,
isRedesignEnabled = tangemPayFeatureToggles.isRedesignEnabled,
shouldShowBankTransfer = virtualAccountToggles.isVaMvp0Enabled && params.virtualAccountOnramp != null,
shouldShowBankTransfer = isBankTransferShown,
).convert(data)
}

View file

@ -489,6 +489,7 @@ internal class TangemPayCardPageModel @Inject constructor(
override fun onClickBankTransfer() {
val loaded = currentStatus.value.ifLoadedOrNull { it } ?: return
val onramp = loaded.virtualAccount ?: return
analytics.send(TangemPayAnalyticsEvents.VaTopupButtonClicked())
bottomSheetNavigation.dismiss()
bottomSheetNavigation.activate(
TangemPayCardNavigation.VirtualAccountDeposit(
@ -500,6 +501,7 @@ internal class TangemPayCardPageModel @Inject constructor(
}
fun onVirtualAccountOrderCreated() {
analytics.send(TangemPayAnalyticsEvents.VaSuccessScreenActivation())
bottomSheetNavigation.dismiss()
router.push(TangemPayCardDetailsInnerRoute.VirtualAccountDepositSuccess)
}
@ -514,6 +516,18 @@ internal class TangemPayCardPageModel @Inject constructor(
)
}
fun onVaBankingDetailsShown() {
analytics.send(TangemPayAnalyticsEvents.VaBankingDetailsShowed())
}
fun onVaShareDetailsClicked() {
analytics.send(TangemPayAnalyticsEvents.VaShareDetailsButtonClicked())
}
fun onVaFieldCopied(field: String) {
analytics.send(TangemPayAnalyticsEvents.VaCopyFieldClicked(field))
}
override fun onDismissAddFunds() {
bottomSheetNavigation.dismiss()
}

View file

@ -314,6 +314,7 @@ internal class TangemPayDetailsModel @Inject constructor(
override fun onClickBankTransfer() {
val loaded = currentStatus.value.ifLoadedOrNull { it } ?: return
val onramp = loaded.virtualAccount ?: return
analytics.send(TangemPayAnalyticsEvents.VaTopupButtonClicked())
bottomSheetNavigation.dismiss()
bottomSheetNavigation.activate(
TangemPayDetailsNavigation.VirtualAccountDeposit(
@ -325,6 +326,7 @@ internal class TangemPayDetailsModel @Inject constructor(
}
fun onVirtualAccountOrderCreated() {
analytics.send(TangemPayAnalyticsEvents.VaSuccessScreenActivation())
bottomSheetNavigation.dismiss()
router.push(TangemPayAccountDetailsInnerRoute.VirtualAccountDepositSuccess)
}
@ -339,6 +341,18 @@ internal class TangemPayDetailsModel @Inject constructor(
)
}
fun onVaBankingDetailsShown() {
analytics.send(TangemPayAnalyticsEvents.VaBankingDetailsShowed())
}
fun onVaShareDetailsClicked() {
analytics.send(TangemPayAnalyticsEvents.VaShareDetailsButtonClicked())
}
fun onVaFieldCopied(field: String) {
analytics.send(TangemPayAnalyticsEvents.VaCopyFieldClicked(field))
}
override fun onClickReceive(data: TangemPayTopUpData) {
analytics.send(TangemPayAnalyticsEvents.ReceiveFundsClicked())
bottomSheetNavigation.dismiss()

View file

@ -1,6 +1,7 @@
package com.tangem.features.tangempay.model
import androidx.compose.runtime.Stable
import com.tangem.core.analytics.api.AnalyticsEventHandler
import com.tangem.core.decompose.di.ModelScoped
import com.tangem.core.decompose.model.Model
import com.tangem.core.decompose.model.ParamsContainer
@ -10,6 +11,7 @@ import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.message.ToastMessage
import com.tangem.domain.models.account.VirtualAccountOnramp
import com.tangem.domain.pay.usecase.CreateVirtualAccountOrderUseCase
import com.tangem.domain.tangempay.TangemPayAnalyticsEvents
import com.tangem.features.tangempay.components.TangemPayVirtualAccountDepositComponent
import com.tangem.features.tangempay.details.impl.R
import com.tangem.features.tangempay.entity.TangemPayVirtualAccountDepositUM
@ -29,6 +31,7 @@ internal class TangemPayVirtualAccountDepositModel @Inject constructor(
private val urlOpener: UrlOpener,
private val uiMessageSender: UiMessageSender,
private val createVirtualAccountOrderUseCase: CreateVirtualAccountOrderUseCase,
private val analytics: AnalyticsEventHandler,
) : Model() {
private val params = paramsContainer.require<TangemPayVirtualAccountDepositComponent.Params>()
@ -55,14 +58,29 @@ internal class TangemPayVirtualAccountDepositModel @Inject constructor(
),
)
init {
val event = if (params.virtualAccountOnramp is VirtualAccountOnramp.Eligible) {
TangemPayAnalyticsEvents.VaConditionsPopupShowedFirstTime()
} else {
TangemPayAnalyticsEvents.VaConditionsPopupShowed()
}
analytics.send(event)
}
fun onDismiss() {
params.onDismiss()
}
private fun onShowDetailsClick() {
when (params.virtualAccountOnramp) {
is VirtualAccountOnramp.Available -> params.onShowDetails(params.virtualAccountOnramp)
VirtualAccountOnramp.Eligible -> createVirtualAccountOrder()
is VirtualAccountOnramp.Available -> {
analytics.send(TangemPayAnalyticsEvents.VaShowDetailsClicked())
params.onShowDetails(params.virtualAccountOnramp)
}
VirtualAccountOnramp.Eligible -> {
analytics.send(TangemPayAnalyticsEvents.VaShowDetailsFirstTimeClicked())
createVirtualAccountOrder()
}
}
}

View file

@ -4,6 +4,7 @@ import arrow.core.Either
import arrow.core.left
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.core.decompose.ui.UiMessageSender
import com.tangem.core.navigation.url.UrlOpener
@ -12,6 +13,7 @@ import com.tangem.domain.models.account.BankCredentials
import com.tangem.domain.models.account.VirtualAccountOnramp
import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.domain.pay.usecase.CreateVirtualAccountOrderUseCase
import com.tangem.domain.tangempay.TangemPayAnalyticsEvents
import com.tangem.domain.visa.error.VisaApiError
import com.tangem.features.tangempay.components.TangemPayVirtualAccountDepositComponent
import com.tangem.utils.coroutines.TestingCoroutineDispatcherProvider
@ -39,10 +41,11 @@ internal class TangemPayVirtualAccountDepositModelTest {
private val createVirtualAccountOrderUseCase: CreateVirtualAccountOrderUseCase = mockk()
private val onShowDetails: (VirtualAccountOnramp.Available) -> Unit = mockk(relaxed = true)
private val onOrderCreated: () -> Unit = mockk(relaxed = true)
private val analytics: AnalyticsEventHandler = mockk(relaxed = true)
@BeforeEach
fun resetMocks() {
clearMocks(createVirtualAccountOrderUseCase, onShowDetails, onOrderCreated, uiMessageSender)
clearMocks(createVirtualAccountOrderUseCase, onShowDetails, onOrderCreated, uiMessageSender, analytics)
}
@Test
@ -58,6 +61,8 @@ internal class TangemPayVirtualAccountDepositModelTest {
// Assert
verify(exactly = 1) { onShowDetails(onramp) }
coVerify(exactly = 0) { createVirtualAccountOrderUseCase(any(), any()) }
verify(exactly = 1) { analytics.send(ofType<TangemPayAnalyticsEvents.VaConditionsPopupShowed>()) }
verify(exactly = 1) { analytics.send(ofType<TangemPayAnalyticsEvents.VaShowDetailsClicked>()) }
}
@Test
@ -74,6 +79,8 @@ internal class TangemPayVirtualAccountDepositModelTest {
coVerify(exactly = 1) { createVirtualAccountOrderUseCase(userWalletId, paymentAccountAddress) }
verify(exactly = 1) { onOrderCreated() }
assertThat(model.uiState.value.isLoading).isFalse()
verify(exactly = 1) { analytics.send(ofType<TangemPayAnalyticsEvents.VaConditionsPopupShowedFirstTime>()) }
verify(exactly = 1) { analytics.send(ofType<TangemPayAnalyticsEvents.VaShowDetailsFirstTimeClicked>()) }
}
@Test
@ -130,6 +137,7 @@ internal class TangemPayVirtualAccountDepositModelTest {
urlOpener = urlOpener,
uiMessageSender = uiMessageSender,
createVirtualAccountOrderUseCase = createVirtualAccountOrderUseCase,
analytics = analytics,
)
private fun bankCredentials() = BankCredentials(

View file

@ -18,6 +18,10 @@ interface VirtualAccountAddFundsBottomSheetComponent : ComposableBottomSheetComp
val dailyDepositLimit: String,
val listener: VirtualAccountAddFundsListener,
val shouldSkipIntro: Boolean = false,
// Analytics hooks — supplied by callers that track this sheet (e.g. TangemPay VA topup); no-op otherwise.
val onDetailsShown: () -> Unit = {},
val onShareClicked: () -> Unit = {},
val onFieldCopied: (fieldName: String) -> Unit = {},
)
data class RequisitesRow(

View file

@ -35,6 +35,13 @@ internal class VirtualAccountAddFundsModel @Inject constructor(
),
)
init {
if (params.shouldSkipIntro) {
// send analytics
params.onDetailsShown()
}
}
fun onDismiss() {
params.listener.onAddFundsDismiss()
}
@ -44,6 +51,7 @@ internal class VirtualAccountAddFundsModel @Inject constructor(
)
private fun showDetailsContent() {
params.onDetailsShown()
uiState.update { state -> state.copy(content = buildDetailsContent()) }
}
@ -52,13 +60,19 @@ internal class VirtualAccountAddFundsModel @Inject constructor(
.map { detailItem(label = it.title, value = it.value) }
.toImmutableList(),
dailyLimit = params.dailyDepositLimit,
onShareClick = { shareManager.shareText(buildShareText()) },
onShareClick = {
params.onShareClicked()
shareManager.shareText(buildShareText())
},
)
private fun detailItem(label: String, value: String) = VirtualAccountAddFundsUM.DetailItem(
label = stringReference(label),
value = value,
onCopyClick = { clipboardManager.setText(text = value, isSensitive = true) },
onCopyClick = {
params.onFieldCopied(label)
clipboardManager.setText(text = value, isSensitive = true)
},
)
private fun buildShareText(): String {