Updated on 2026-08-14
This commit is contained in:
parent
179ee8f01b
commit
ffc2e8a6db
6 changed files with 250 additions and 6 deletions
|
|
@ -92,4 +92,9 @@ dependencies {
|
|||
/** DI */
|
||||
implementation(deps.hilt.android)
|
||||
kapt(deps.hilt.kapt)
|
||||
|
||||
/** Test */
|
||||
testImplementation(deps.test.junit)
|
||||
testImplementation(deps.test.truth)
|
||||
testImplementation(deps.test.mockk)
|
||||
}
|
||||
|
|
@ -0,0 +1,38 @@
|
|||
package com.tangem.features.swap.v2.impl.amount.analytics
|
||||
|
||||
import com.tangem.core.analytics.api.AnalyticsEventHandler
|
||||
import com.tangem.domain.express.models.ExpressError
|
||||
import com.tangem.features.swap.v2.impl.common.entity.SwapQuoteUM
|
||||
import com.tangem.features.swap.v2.impl.sendviaswap.analytics.SendWithSwapAnalyticEvents
|
||||
import com.tangem.features.swap.v2.impl.sendviaswap.analytics.SendWithSwapAnalyticsErrorMessages
|
||||
|
||||
internal class SwapAmountAnalyticsSender(
|
||||
private val analyticsEventHandler: AnalyticsEventHandler,
|
||||
) {
|
||||
|
||||
private var lastSentErrorMessage: String? = null
|
||||
|
||||
fun sendErrorIfNeeded(quotes: List<SwapQuoteUM>, selectedQuote: SwapQuoteUM?) {
|
||||
val errorMessage = resolveErrorMessage(quotes, selectedQuote)
|
||||
if (errorMessage == lastSentErrorMessage) return
|
||||
lastSentErrorMessage = errorMessage
|
||||
if (errorMessage != null) {
|
||||
analyticsEventHandler.send(
|
||||
SendWithSwapAnalyticEvents.SendWithSwapError(
|
||||
errorScreen = SendWithSwapAnalyticEvents.ErrorScreen.Amount,
|
||||
message = errorMessage,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun resolveErrorMessage(quotes: List<SwapQuoteUM>, selectedQuote: SwapQuoteUM?): String? {
|
||||
if (quotes.isEmpty()) return SendWithSwapAnalyticsErrorMessages.EXPRESS_QUOTE_NO_PROVIDERS
|
||||
val error = (selectedQuote as? SwapQuoteUM.Error)?.expressError ?: return null
|
||||
return when (error) {
|
||||
is ExpressError.AmountError.TooSmallError -> SendWithSwapAnalyticsErrorMessages.MIN_AMOUNT
|
||||
is ExpressError.AmountError.TooBigError -> SendWithSwapAnalyticsErrorMessages.MAX_AMOUNT
|
||||
else -> "${SendWithSwapAnalyticsErrorMessages.EXPRESS_QUOTE}: code=${error.code}"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -46,6 +46,7 @@ import com.tangem.features.swap.v2.impl.amount.SwapAmountComponentParams
|
|||
import com.tangem.features.swap.v2.impl.amount.SwapAmountReduceListener
|
||||
import com.tangem.features.swap.v2.impl.amount.SwapAmountUpdateListener
|
||||
import com.tangem.features.swap.v2.impl.amount.analytics.SwapAmountAnalyticEvents
|
||||
import com.tangem.features.swap.v2.impl.amount.analytics.SwapAmountAnalyticsSender
|
||||
import com.tangem.features.swap.v2.impl.amount.entity.SwapAmountFieldUM
|
||||
import com.tangem.features.swap.v2.impl.amount.entity.SwapAmountUM
|
||||
import com.tangem.features.swap.v2.impl.amount.model.converter.SwapQuoteUMConverter
|
||||
|
|
@ -56,6 +57,7 @@ import com.tangem.features.swap.v2.impl.common.entity.SwapQuoteUM
|
|||
import com.tangem.features.swap.v2.impl.sendviaswap.SendWithSwapRoute
|
||||
import com.tangem.features.swap.v2.impl.sendviaswap.analytics.SendWithSwapAnalyticEvents
|
||||
import com.tangem.features.swap.v2.impl.sendviaswap.analytics.SendWithSwapAnalyticEvents.NoticeFixedRate.toAnalyticsRateType
|
||||
import com.tangem.features.swap.v2.impl.sendviaswap.analytics.SendWithSwapAnalyticsErrorMessages
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import com.tangem.utils.coroutines.Debouncer
|
||||
import com.tangem.utils.coroutines.PeriodicTask
|
||||
|
|
@ -120,7 +122,8 @@ internal class SwapAmountModel @Inject constructor(
|
|||
val rateInfoNavigation: SlotNavigation<ExpressRateType> = SlotNavigation()
|
||||
|
||||
private var isShowBestRateAnimation: Boolean = false
|
||||
private var lastAmountScreenOpenedCurrencyId: CryptoCurrency.ID? = null
|
||||
private var isAmountScreenOpenedSent: Boolean = false
|
||||
private val amountAnalyticsSender = SwapAmountAnalyticsSender(analyticsEventHandler)
|
||||
|
||||
private var autoUpdateSubscriberJob: Job? = null
|
||||
|
||||
|
|
@ -608,7 +611,7 @@ internal class SwapAmountModel @Inject constructor(
|
|||
@Suppress("NullableToStringCall")
|
||||
TangemLogger.e(
|
||||
"""
|
||||
Invalid cryptocurrencies status:
|
||||
Invalid cryptocurrencies status:
|
||||
| Primary -> $primaryStatus
|
||||
| Secondary -> $secondaryStatus
|
||||
""".trimIndent(),
|
||||
|
|
@ -616,7 +619,8 @@ internal class SwapAmountModel @Inject constructor(
|
|||
analyticsEventHandler.send(
|
||||
SendWithSwapAnalyticEvents.SendWithSwapError(
|
||||
errorScreen = SendWithSwapAnalyticEvents.ErrorScreen.Amount,
|
||||
message = "Invalid cryptocurrencies status: primary=$primaryStatus, secondary=$secondaryStatus",
|
||||
message = "${SendWithSwapAnalyticsErrorMessages.INVALID_CRYPTOCURRENCIES_STATUS}: " +
|
||||
"primary=$primaryStatus, secondary=$secondaryStatus",
|
||||
),
|
||||
)
|
||||
showErrorAlert(errorMessage = null)
|
||||
|
|
@ -625,9 +629,9 @@ internal class SwapAmountModel @Inject constructor(
|
|||
}
|
||||
|
||||
private fun sendAmountScreenOpenedIfNeeded(secondaryStatus: CryptoCurrencyStatus) {
|
||||
val currencyId = secondaryStatus.currency.id
|
||||
if (lastAmountScreenOpenedCurrencyId == currencyId) return
|
||||
lastAmountScreenOpenedCurrencyId = currencyId
|
||||
if (params !is SwapAmountComponentParams.AmountParams) return
|
||||
if (isAmountScreenOpenedSent) return
|
||||
isAmountScreenOpenedSent = true
|
||||
|
||||
val content = uiState.value as? SwapAmountUM.Content ?: return
|
||||
|
||||
|
|
@ -778,6 +782,10 @@ internal class SwapAmountModel @Inject constructor(
|
|||
secondaryFiatRateUSD = secondaryFiatRateUSD,
|
||||
),
|
||||
)
|
||||
if (params is SwapAmountComponentParams.AmountParams) {
|
||||
val selectedQuote = (uiState.value as? SwapAmountUM.Content)?.selectedQuote
|
||||
amountAnalyticsSender.sendErrorIfNeeded(quotes, selectedQuote)
|
||||
}
|
||||
feeSelectorReloadTrigger.triggerUpdate()
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@ import com.tangem.features.swap.v2.impl.notifications.SwapNotificationsComponent
|
|||
import com.tangem.features.swap.v2.impl.notifications.SwapNotificationsUpdateListener
|
||||
import com.tangem.features.swap.v2.impl.notifications.entity.SwapNotificationUM
|
||||
import com.tangem.features.swap.v2.impl.sendviaswap.analytics.SendWithSwapAnalyticEvents
|
||||
import com.tangem.features.swap.v2.impl.sendviaswap.analytics.SendWithSwapAnalyticsErrorMessages
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
|
|
@ -44,6 +45,7 @@ internal class SwapNotificationsModel @Inject constructor(
|
|||
private val params: SwapNotificationsComponent.Params = paramsContainer.require()
|
||||
|
||||
private var notificationData = params.swapNotificationData
|
||||
private var lastSentErrorMessages: Set<String> = emptySet()
|
||||
|
||||
val uiState: StateFlow<ImmutableList<NotificationUM>>
|
||||
field = MutableStateFlow<ImmutableList<NotificationUM>>(persistentListOf())
|
||||
|
|
@ -110,6 +112,8 @@ internal class SwapNotificationsModel @Inject constructor(
|
|||
)
|
||||
}
|
||||
}
|
||||
|
||||
sendErrorAnalyticsIfNeeded(notifications)
|
||||
}
|
||||
|
||||
private suspend fun MutableList<NotificationUM>.addDestinationTagRequiredNotification() {
|
||||
|
|
@ -189,4 +193,34 @@ internal class SwapNotificationsModel @Inject constructor(
|
|||
|
||||
add(notification)
|
||||
}
|
||||
|
||||
private fun sendErrorAnalyticsIfNeeded(notifications: List<NotificationUM>) {
|
||||
val currentErrors = notifications.mapNotNull { notification ->
|
||||
when (notification) {
|
||||
is SwapNotificationUM.Error.InsufficientFunds ->
|
||||
SendWithSwapAnalyticsErrorMessages.INSUFFICIENT_BALANCE
|
||||
is SwapNotificationUM.Error.MinimalAmountError ->
|
||||
SendWithSwapAnalyticsErrorMessages.MIN_AMOUNT
|
||||
is SwapNotificationUM.Error.MaximumAmountError ->
|
||||
SendWithSwapAnalyticsErrorMessages.MAX_AMOUNT
|
||||
is SwapNotificationUM.Warning.ExpressGeneralError ->
|
||||
"${SendWithSwapAnalyticsErrorMessages.EXPRESS_QUOTE}: code=${notification.expressError.code}"
|
||||
is NotificationUM.Error.DestinationTagRequired ->
|
||||
SendWithSwapAnalyticsErrorMessages.DESTINATION_TAG_REQUIRED
|
||||
else -> null
|
||||
}
|
||||
}.toSet()
|
||||
|
||||
val newErrors = currentErrors - lastSentErrorMessages
|
||||
lastSentErrorMessages = currentErrors
|
||||
|
||||
newErrors.forEach { errorMessage ->
|
||||
analyticsEventHandler.send(
|
||||
SendWithSwapAnalyticEvents.SendWithSwapError(
|
||||
errorScreen = SendWithSwapAnalyticEvents.ErrorScreen.Confirm,
|
||||
message = errorMessage,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
package com.tangem.features.swap.v2.impl.sendviaswap.analytics
|
||||
|
||||
internal object SendWithSwapAnalyticsErrorMessages {
|
||||
const val INSUFFICIENT_BALANCE = "Error - Insufficient balance"
|
||||
const val MIN_AMOUNT = "Error - Min amount"
|
||||
const val MAX_AMOUNT = "Error - Max amount"
|
||||
const val EXPRESS_QUOTE_NO_PROVIDERS = "Error - Express quote no providers found"
|
||||
const val EXPRESS_QUOTE = "Error - Express quote"
|
||||
const val DESTINATION_TAG_REQUIRED = "Error - Destination tag required"
|
||||
const val INVALID_CRYPTOCURRENCIES_STATUS = "Error - Invalid cryptocurrencies status"
|
||||
}
|
||||
|
|
@ -0,0 +1,148 @@
|
|||
package com.tangem.features.swap.v2.impl.amount.analytics
|
||||
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import com.tangem.core.analytics.api.AnalyticsEventHandler
|
||||
import com.tangem.core.analytics.models.AnalyticsEvent
|
||||
import com.tangem.domain.express.models.ExpressError
|
||||
import com.tangem.domain.express.models.ExpressProvider
|
||||
import com.tangem.domain.express.models.ExpressProviderType
|
||||
import com.tangem.features.swap.v2.impl.common.entity.SwapQuoteUM
|
||||
import com.tangem.features.swap.v2.impl.sendviaswap.analytics.SendWithSwapAnalyticEvents
|
||||
import com.tangem.features.swap.v2.impl.sendviaswap.analytics.SendWithSwapAnalyticsErrorMessages
|
||||
import io.mockk.every
|
||||
import io.mockk.mockk
|
||||
import io.mockk.slot
|
||||
import io.mockk.verify
|
||||
import org.junit.Test
|
||||
import java.math.BigDecimal
|
||||
|
||||
class SwapAmountAnalyticsSenderTest {
|
||||
|
||||
private val analyticsEventHandler = mockk<AnalyticsEventHandler>(relaxed = true)
|
||||
private val sender = SwapAmountAnalyticsSender(analyticsEventHandler)
|
||||
|
||||
private val testProvider = ExpressProvider(
|
||||
providerId = "test",
|
||||
name = "Test Provider",
|
||||
type = ExpressProviderType.CEX,
|
||||
imageLarge = "",
|
||||
termsOfUse = null,
|
||||
privacyPolicy = null,
|
||||
slippage = null,
|
||||
)
|
||||
|
||||
@Test
|
||||
fun `GIVEN empty quotes WHEN sendErrorIfNeeded THEN send no providers error`() {
|
||||
val eventSlot = slot<AnalyticsEvent>()
|
||||
every { analyticsEventHandler.send(capture(eventSlot)) } returns Unit
|
||||
|
||||
sender.sendErrorIfNeeded(quotes = emptyList(), selectedQuote = null)
|
||||
|
||||
verify(exactly = 1) { analyticsEventHandler.send(any()) }
|
||||
val event = eventSlot.captured as SendWithSwapAnalyticEvents.SendWithSwapError
|
||||
assertThat(event.errorScreen).isEqualTo(SendWithSwapAnalyticEvents.ErrorScreen.Amount)
|
||||
assertThat(event.message).isEqualTo(SendWithSwapAnalyticsErrorMessages.EXPRESS_QUOTE_NO_PROVIDERS)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN too small error quote WHEN sendErrorIfNeeded THEN send min amount error`() {
|
||||
val errorQuote = SwapQuoteUM.Error(
|
||||
provider = testProvider,
|
||||
expressError = ExpressError.AmountError.TooSmallError(code = 1001, amount = BigDecimal("0.01")),
|
||||
)
|
||||
val eventSlot = slot<AnalyticsEvent>()
|
||||
every { analyticsEventHandler.send(capture(eventSlot)) } returns Unit
|
||||
|
||||
sender.sendErrorIfNeeded(quotes = listOf(errorQuote), selectedQuote = errorQuote)
|
||||
|
||||
verify(exactly = 1) { analyticsEventHandler.send(any()) }
|
||||
val event = eventSlot.captured as SendWithSwapAnalyticEvents.SendWithSwapError
|
||||
assertThat(event.message).isEqualTo(SendWithSwapAnalyticsErrorMessages.MIN_AMOUNT)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN too big error quote WHEN sendErrorIfNeeded THEN send max amount error`() {
|
||||
val errorQuote = SwapQuoteUM.Error(
|
||||
provider = testProvider,
|
||||
expressError = ExpressError.AmountError.TooBigError(code = 1002, amount = BigDecimal("1000")),
|
||||
)
|
||||
val eventSlot = slot<AnalyticsEvent>()
|
||||
every { analyticsEventHandler.send(capture(eventSlot)) } returns Unit
|
||||
|
||||
sender.sendErrorIfNeeded(quotes = listOf(errorQuote), selectedQuote = errorQuote)
|
||||
|
||||
verify(exactly = 1) { analyticsEventHandler.send(any()) }
|
||||
val event = eventSlot.captured as SendWithSwapAnalyticEvents.SendWithSwapError
|
||||
assertThat(event.message).isEqualTo(SendWithSwapAnalyticsErrorMessages.MAX_AMOUNT)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN unknown express error WHEN sendErrorIfNeeded THEN send express quote error with code`() {
|
||||
val errorQuote = SwapQuoteUM.Error(
|
||||
provider = testProvider,
|
||||
expressError = ExpressError.InternalError(code = 500),
|
||||
)
|
||||
val eventSlot = slot<AnalyticsEvent>()
|
||||
every { analyticsEventHandler.send(capture(eventSlot)) } returns Unit
|
||||
|
||||
sender.sendErrorIfNeeded(quotes = listOf(errorQuote), selectedQuote = errorQuote)
|
||||
|
||||
verify(exactly = 1) { analyticsEventHandler.send(any()) }
|
||||
val event = eventSlot.captured as SendWithSwapAnalyticEvents.SendWithSwapError
|
||||
assertThat(event.message).isEqualTo("${SendWithSwapAnalyticsErrorMessages.EXPRESS_QUOTE}: code=500")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN content quote WHEN sendErrorIfNeeded THEN do not send analytics`() {
|
||||
val contentQuote = mockk<SwapQuoteUM.Content>()
|
||||
|
||||
sender.sendErrorIfNeeded(quotes = listOf(contentQuote), selectedQuote = contentQuote)
|
||||
|
||||
verify(exactly = 0) { analyticsEventHandler.send(any()) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN same error twice WHEN sendErrorIfNeeded THEN send analytics only once`() {
|
||||
val errorQuote = SwapQuoteUM.Error(
|
||||
provider = testProvider,
|
||||
expressError = ExpressError.AmountError.TooSmallError(code = 1001, amount = BigDecimal("0.01")),
|
||||
)
|
||||
|
||||
sender.sendErrorIfNeeded(quotes = listOf(errorQuote), selectedQuote = errorQuote)
|
||||
sender.sendErrorIfNeeded(quotes = listOf(errorQuote), selectedQuote = errorQuote)
|
||||
|
||||
verify(exactly = 1) { analyticsEventHandler.send(any()) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN error then different error WHEN sendErrorIfNeeded THEN send analytics twice`() {
|
||||
val smallError = SwapQuoteUM.Error(
|
||||
provider = testProvider,
|
||||
expressError = ExpressError.AmountError.TooSmallError(code = 1001, amount = BigDecimal("0.01")),
|
||||
)
|
||||
val bigError = SwapQuoteUM.Error(
|
||||
provider = testProvider,
|
||||
expressError = ExpressError.AmountError.TooBigError(code = 1002, amount = BigDecimal("1000")),
|
||||
)
|
||||
|
||||
sender.sendErrorIfNeeded(quotes = listOf(smallError), selectedQuote = smallError)
|
||||
sender.sendErrorIfNeeded(quotes = listOf(bigError), selectedQuote = bigError)
|
||||
|
||||
verify(exactly = 2) { analyticsEventHandler.send(any()) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN error then success then same error WHEN sendErrorIfNeeded THEN send analytics twice`() {
|
||||
val errorQuote = SwapQuoteUM.Error(
|
||||
provider = testProvider,
|
||||
expressError = ExpressError.AmountError.TooSmallError(code = 1001, amount = BigDecimal("0.01")),
|
||||
)
|
||||
val contentQuote = mockk<SwapQuoteUM.Content>()
|
||||
|
||||
sender.sendErrorIfNeeded(quotes = listOf(errorQuote), selectedQuote = errorQuote)
|
||||
sender.sendErrorIfNeeded(quotes = listOf(contentQuote), selectedQuote = contentQuote)
|
||||
sender.sendErrorIfNeeded(quotes = listOf(errorQuote), selectedQuote = errorQuote)
|
||||
|
||||
verify(exactly = 2) { analyticsEventHandler.send(any()) }
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue