Updated on 2026-08-14
This commit is contained in:
parent
4cd0ac3674
commit
fcb3722ca0
7 changed files with 706 additions and 22 deletions
|
|
@ -22,14 +22,18 @@ sealed class TokenScreenAnalyticsEvent(
|
|||
blockchain: String,
|
||||
token: String,
|
||||
tokenBalance: TokenBalance,
|
||||
isDynamicAddress: Boolean? = null,
|
||||
) : AnalyticsEvent(
|
||||
category = "Details Screen",
|
||||
event = "Details Screen Opened",
|
||||
params = mapOf(
|
||||
BLOCKCHAIN to blockchain,
|
||||
TOKEN_PARAM to token,
|
||||
BALANCE to tokenBalance.name,
|
||||
),
|
||||
params = buildMap {
|
||||
put(BLOCKCHAIN, blockchain)
|
||||
put(TOKEN_PARAM, token)
|
||||
put(BALANCE, tokenBalance.name)
|
||||
isDynamicAddress?.let {
|
||||
put("Dynamic Address", if (it) "True" else "False")
|
||||
}
|
||||
},
|
||||
) {
|
||||
sealed class TokenBalance(val name: String) {
|
||||
data object Full : TokenBalance("Full")
|
||||
|
|
|
|||
|
|
@ -9,6 +9,31 @@ internal open class TokenDetailsAnalyticsEvent(
|
|||
params: Map<String, String> = mapOf(),
|
||||
) : AnalyticsEvent(category = "Token", event, params) {
|
||||
|
||||
class DynamicAddressesScreenOpened(currency: CryptoCurrency) : TokenDetailsAnalyticsEvent(
|
||||
event = "Dynamic Addresses Screen Opened",
|
||||
params = currency.toAnalyticsParams(),
|
||||
)
|
||||
|
||||
class ButtonEnableDynamicAddresses(currency: CryptoCurrency) : TokenDetailsAnalyticsEvent(
|
||||
event = "Button - Enable Dynamic Addresses",
|
||||
params = currency.toAnalyticsParams(),
|
||||
)
|
||||
|
||||
class DynamicAddressesEnabled(currency: CryptoCurrency) : TokenDetailsAnalyticsEvent(
|
||||
event = "Dynamic Addresses Enabled",
|
||||
params = currency.toAnalyticsParams(),
|
||||
)
|
||||
|
||||
class ButtonDisableDynamicAddresses(currency: CryptoCurrency) : TokenDetailsAnalyticsEvent(
|
||||
event = "Button - Disable Dynamic Addresses",
|
||||
params = currency.toAnalyticsParams(),
|
||||
)
|
||||
|
||||
class DynamicAddressesDisabled(currency: CryptoCurrency) : TokenDetailsAnalyticsEvent(
|
||||
event = "Dynamic Addresses Disabled",
|
||||
params = currency.toAnalyticsParams(),
|
||||
)
|
||||
|
||||
open class Notice(
|
||||
event: String,
|
||||
params: Map<String, String> = mapOf(),
|
||||
|
|
@ -19,14 +44,40 @@ internal open class TokenDetailsAnalyticsEvent(
|
|||
params = currency.toAnalyticsParams(),
|
||||
)
|
||||
|
||||
class NotEnoughFee(currency: CryptoCurrency) : Notice(
|
||||
class NotEnoughFee(currency: CryptoCurrency, source: Source) : Notice(
|
||||
event = "Not Enough Fee",
|
||||
params = currency.toAnalyticsParams(),
|
||||
)
|
||||
params = currency.toAnalyticsParams() + ("Source" to source.value),
|
||||
) {
|
||||
enum class Source(val value: String) {
|
||||
DetailedScreen("Detailed Screen"),
|
||||
DynamicAddresses("Dynamic Addresses"),
|
||||
}
|
||||
}
|
||||
|
||||
class Reveal(currency: CryptoCurrency) : Notice(
|
||||
event = "Reveal Transaction",
|
||||
params = currency.toAnalyticsParams(),
|
||||
)
|
||||
|
||||
class DynamicAddressesUnavailable(currency: CryptoCurrency) : Notice(
|
||||
event = "Dynamic Addresses Unavailable",
|
||||
params = currency.toAnalyticsParams(),
|
||||
)
|
||||
|
||||
class AdditionalAddressesFound(currency: CryptoCurrency) : Notice(
|
||||
event = "Additional Addresses Found",
|
||||
params = currency.toAnalyticsParams(),
|
||||
)
|
||||
}
|
||||
|
||||
open class Error(
|
||||
event: String,
|
||||
params: Map<String, String> = emptyMap(),
|
||||
) : TokenDetailsAnalyticsEvent(event = "Error - $event", params) {
|
||||
|
||||
class DynamicAddressesUnavailable(currency: CryptoCurrency) : Error(
|
||||
event = "Dynamic Addresses Unavailable",
|
||||
params = currency.toAnalyticsParams(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -37,6 +37,7 @@ internal class TokenDetailsNotificationsAnalyticsSender(
|
|||
is TokenDetailsNotification.NetworkFeeWithBuyButton,
|
||||
-> TokenDetailsAnalyticsEvent.Notice.NotEnoughFee(
|
||||
currency = cryptoCurrency,
|
||||
source = TokenDetailsAnalyticsEvent.Notice.NotEnoughFee.Source.DetailedScreen,
|
||||
)
|
||||
is TokenDetailsNotification.SwapPromo -> PromoAnalyticsEvent.NoticePromotionBanner(
|
||||
program = PromoAnalyticsEvent.Program.Empty, // Use it on new promo action
|
||||
|
|
@ -49,6 +50,10 @@ internal class TokenDetailsNotificationsAnalyticsSender(
|
|||
token = cryptoCurrency.symbol,
|
||||
blockchain = cryptoCurrency.network.name,
|
||||
)
|
||||
is TokenDetailsNotification.DynamicAddressesFundsFound ->
|
||||
TokenDetailsAnalyticsEvent.Notice.AdditionalAddressesFound(
|
||||
currency = cryptoCurrency,
|
||||
)
|
||||
is TokenDetailsNotification.NetworksUnreachable,
|
||||
is TokenDetailsNotification.ExistentialDeposit,
|
||||
is TokenDetailsNotification.NetworksNoAccount,
|
||||
|
|
|
|||
|
|
@ -1,6 +1,8 @@
|
|||
package com.tangem.feature.tokendetails.presentation.tokendetails.model
|
||||
|
||||
import com.tangem.common.core.TangemSdkError
|
||||
import com.tangem.core.analytics.api.AnalyticsEventHandler
|
||||
import com.tangem.core.analytics.api.ResettableOneTimeEventSender
|
||||
import com.tangem.core.decompose.di.GlobalUiMessageSender
|
||||
import com.tangem.core.decompose.ui.UiMessageSender
|
||||
import com.tangem.core.res.R
|
||||
|
|
@ -23,6 +25,7 @@ import com.tangem.domain.transaction.error.SendTransactionError
|
|||
import com.tangem.domain.transaction.usecase.GetFeeUseCase
|
||||
import com.tangem.domain.transaction.usecase.SendTransactionUseCase
|
||||
import com.tangem.domain.wallets.usecase.GetExtendedPublicKeyForCurrencyUseCase
|
||||
import com.tangem.feature.tokendetails.presentation.tokendetails.analytics.TokenDetailsAnalyticsEvent
|
||||
import com.tangem.feature.tokendetails.presentation.tokendetails.ui.components.dynamicaddresses.DynamicAddressesBottomSheetConfig
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import com.tangem.utils.coroutines.runSuspendCatching
|
||||
|
|
@ -37,7 +40,7 @@ import kotlinx.coroutines.flow.asStateFlow
|
|||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
@Suppress("LongParameterList")
|
||||
@Suppress("LongParameterList", "LargeClass")
|
||||
internal class DynamicAddressesDelegate @AssistedInject constructor(
|
||||
private val enableDynamicAddressesUseCase: EnableDynamicAddressesUseCase,
|
||||
private val disableDynamicAddressesUseCase: DisableDynamicAddressesUseCase,
|
||||
|
|
@ -47,6 +50,7 @@ internal class DynamicAddressesDelegate @AssistedInject constructor(
|
|||
private val getDerivedXpubUseCase: GetDerivedXpubUseCase,
|
||||
private val dynamicAddressesRepository: DynamicAddressesRepository,
|
||||
private val getExtendedPublicKeyUseCase: GetExtendedPublicKeyForCurrencyUseCase,
|
||||
private val analyticsEventHandler: AnalyticsEventHandler,
|
||||
@GlobalUiMessageSender private val uiMessageSender: UiMessageSender,
|
||||
private val dispatchers: CoroutineDispatcherProvider,
|
||||
@Assisted private val userWallet: UserWallet,
|
||||
|
|
@ -68,17 +72,20 @@ internal class DynamicAddressesDelegate @AssistedInject constructor(
|
|||
)
|
||||
val bottomSheetConfig: StateFlow<DynamicAddressesBottomSheetConfig> = _bottomSheetConfig.asStateFlow()
|
||||
|
||||
private val resettableOneTimeEventSender = ResettableOneTimeEventSender(analyticsEventHandler)
|
||||
|
||||
// region Entry point
|
||||
|
||||
fun onDynamicAddressesClick() {
|
||||
val network = cryptoCurrencyStatusProvider()?.currency?.network ?: return
|
||||
val currency = cryptoCurrencyStatusProvider()?.currency ?: return
|
||||
analyticsEventHandler.send(TokenDetailsAnalyticsEvent.DynamicAddressesScreenOpened(currency))
|
||||
coroutineScope.launch(dispatchers.main) {
|
||||
val status = dynamicAddressesRepository.getStatus(userWalletId, network).first()
|
||||
val status = dynamicAddressesRepository.getStatus(userWalletId, currency.network).first()
|
||||
when (status) {
|
||||
DynamicAddressesStatus.ENABLED,
|
||||
DynamicAddressesStatus.ENABLED_REQUIRES_SETUP,
|
||||
-> onDisableFlow(network)
|
||||
DynamicAddressesStatus.DISABLED -> onEnableFlow(network)
|
||||
-> onDisableFlow(currency.network)
|
||||
DynamicAddressesStatus.DISABLED -> onEnableFlow(currency.network)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -90,6 +97,9 @@ internal class DynamicAddressesDelegate @AssistedInject constructor(
|
|||
private suspend fun onEnableFlow(network: Network) {
|
||||
val hasConflicts = dynamicAddressesRepository.hasConflictingCustomTokens(userWalletId, network)
|
||||
if (hasConflicts) {
|
||||
cryptoCurrencyStatusProvider()?.currency?.let {
|
||||
analyticsEventHandler.send(TokenDetailsAnalyticsEvent.Notice.DynamicAddressesUnavailable(it))
|
||||
}
|
||||
_bottomSheetConfig.value = DynamicAddressesBottomSheetConfig.ConflictingCustomTokens(
|
||||
onDismissClick = dismissBottomSheet,
|
||||
)
|
||||
|
|
@ -106,7 +116,9 @@ internal class DynamicAddressesDelegate @AssistedInject constructor(
|
|||
}
|
||||
|
||||
private fun onEnableClick() {
|
||||
val network = cryptoCurrencyStatusProvider()?.currency?.network ?: return
|
||||
val currency = cryptoCurrencyStatusProvider()?.currency ?: return
|
||||
val network = currency.network
|
||||
analyticsEventHandler.send(TokenDetailsAnalyticsEvent.ButtonEnableDynamicAddresses(currency))
|
||||
coroutineScope.launch(dispatchers.main) {
|
||||
_bottomSheetConfig.value = DynamicAddressesBottomSheetConfig.Enable(
|
||||
isCardScanRequired = false,
|
||||
|
|
@ -120,6 +132,9 @@ internal class DynamicAddressesDelegate @AssistedInject constructor(
|
|||
dismissBottomSheet()
|
||||
} else {
|
||||
TangemLogger.e("Failed to get XPUB: ${error.message}")
|
||||
analyticsEventHandler.send(
|
||||
TokenDetailsAnalyticsEvent.Error.DynamicAddressesUnavailable(currency),
|
||||
)
|
||||
_bottomSheetConfig.value = DynamicAddressesBottomSheetConfig.ServiceUnavailable(
|
||||
onDismissClick = dismissBottomSheet,
|
||||
)
|
||||
|
|
@ -131,21 +146,24 @@ internal class DynamicAddressesDelegate @AssistedInject constructor(
|
|||
|
||||
enableDynamicAddressesUseCase(userWalletId, network, xpub).fold(
|
||||
ifLeft = { error ->
|
||||
when (error) {
|
||||
is EnableDynamicAddressesError.ConflictingCustomTokens -> {
|
||||
_bottomSheetConfig.value = DynamicAddressesBottomSheetConfig.ConflictingCustomTokens(
|
||||
analyticsEventHandler.send(
|
||||
TokenDetailsAnalyticsEvent.Error.DynamicAddressesUnavailable(currency),
|
||||
)
|
||||
_bottomSheetConfig.value = when (error) {
|
||||
is EnableDynamicAddressesError.ConflictingCustomTokens ->
|
||||
DynamicAddressesBottomSheetConfig.ConflictingCustomTokens(
|
||||
onDismissClick = dismissBottomSheet,
|
||||
)
|
||||
}
|
||||
is EnableDynamicAddressesError.ServiceError -> {
|
||||
TangemLogger.e("Failed to enable dynamic addresses: ${error.cause.message}")
|
||||
_bottomSheetConfig.value = DynamicAddressesBottomSheetConfig.ServiceUnavailable(
|
||||
DynamicAddressesBottomSheetConfig.ServiceUnavailable(
|
||||
onDismissClick = dismissBottomSheet,
|
||||
)
|
||||
}
|
||||
}
|
||||
},
|
||||
ifRight = {
|
||||
analyticsEventHandler.send(TokenDetailsAnalyticsEvent.DynamicAddressesEnabled(currency))
|
||||
dismissBottomSheet()
|
||||
onDynamicAddressesStateChanged()
|
||||
uiMessageSender.send(
|
||||
|
|
@ -190,10 +208,13 @@ internal class DynamicAddressesDelegate @AssistedInject constructor(
|
|||
}
|
||||
|
||||
private fun onSimpleDisableClick() {
|
||||
val network = cryptoCurrencyStatusProvider()?.currency?.network ?: return
|
||||
val currency = cryptoCurrencyStatusProvider()?.currency ?: return
|
||||
val network = currency.network
|
||||
analyticsEventHandler.send(TokenDetailsAnalyticsEvent.ButtonDisableDynamicAddresses(currency))
|
||||
coroutineScope.launch(dispatchers.main) {
|
||||
runSuspendCatching { dynamicAddressesRepository.disable(userWalletId, network) }
|
||||
.onSuccess {
|
||||
analyticsEventHandler.send(TokenDetailsAnalyticsEvent.DynamicAddressesDisabled(currency))
|
||||
dismissBottomSheet()
|
||||
onDynamicAddressesStateChanged()
|
||||
uiMessageSender.send(
|
||||
|
|
@ -210,6 +231,7 @@ internal class DynamicAddressesDelegate @AssistedInject constructor(
|
|||
}
|
||||
|
||||
private fun showDisableSheetAndLoadFee() {
|
||||
resettableOneTimeEventSender.reset(NOT_ENOUGH_FEE_EVENT_KEY)
|
||||
_bottomSheetConfig.value = DynamicAddressesBottomSheetConfig.DisableWithConsolidation(
|
||||
feeState = DynamicAddressesBottomSheetConfig.DisableFeeState.Loading,
|
||||
onDisableClick = ::onDisableClick,
|
||||
|
|
@ -245,6 +267,13 @@ internal class DynamicAddressesDelegate @AssistedInject constructor(
|
|||
cryptoCurrency = currency,
|
||||
).fold(
|
||||
ifLeft = {
|
||||
resettableOneTimeEventSender.sendEventOnce(
|
||||
key = NOT_ENOUGH_FEE_EVENT_KEY,
|
||||
event = TokenDetailsAnalyticsEvent.Notice.NotEnoughFee(
|
||||
currency = currency,
|
||||
source = TokenDetailsAnalyticsEvent.Notice.NotEnoughFee.Source.DynamicAddresses,
|
||||
),
|
||||
)
|
||||
_bottomSheetConfig.value = disableWithConsolidationConfig().copy(
|
||||
feeState = DynamicAddressesBottomSheetConfig.DisableFeeState.Error,
|
||||
)
|
||||
|
|
@ -282,7 +311,9 @@ internal class DynamicAddressesDelegate @AssistedInject constructor(
|
|||
}
|
||||
|
||||
private fun onDisableClick() {
|
||||
val network = cryptoCurrencyStatusProvider()?.currency?.network ?: return
|
||||
val currency = cryptoCurrencyStatusProvider()?.currency ?: return
|
||||
val network = currency.network
|
||||
analyticsEventHandler.send(TokenDetailsAnalyticsEvent.ButtonDisableDynamicAddresses(currency))
|
||||
coroutineScope.launch(dispatchers.main) {
|
||||
_bottomSheetConfig.value = disableWithConsolidationConfig().copy(
|
||||
isSending = true,
|
||||
|
|
@ -320,6 +351,7 @@ internal class DynamicAddressesDelegate @AssistedInject constructor(
|
|||
} catch (e: Exception) {
|
||||
TangemLogger.e("Failed to disable dynamic addresses after consolidation: ${e.message}")
|
||||
}
|
||||
analyticsEventHandler.send(TokenDetailsAnalyticsEvent.DynamicAddressesDisabled(currency))
|
||||
dismissBottomSheet()
|
||||
onDynamicAddressesStateChanged()
|
||||
uiMessageSender.send(
|
||||
|
|
@ -359,4 +391,8 @@ internal class DynamicAddressesDelegate @AssistedInject constructor(
|
|||
@Assisted("onDynamicAddressesStateChanged") onDynamicAddressesStateChanged: () -> Unit,
|
||||
): DynamicAddressesDelegate
|
||||
}
|
||||
|
||||
private companion object {
|
||||
const val NOT_ENOUGH_FEE_EVENT_KEY = "DynamicAddressesNotEnoughFee"
|
||||
}
|
||||
}
|
||||
|
|
@ -13,6 +13,7 @@ import com.tangem.domain.dynamicaddresses.DynamicAddressesDerivationChecker
|
|||
import com.tangem.domain.dynamicaddresses.DynamicAddressesFeatureToggles
|
||||
import com.tangem.domain.dynamicaddresses.DynamicAddressesSupportedBlockchains
|
||||
import com.tangem.domain.dynamicaddresses.IsXpubSupportedUseCase
|
||||
import com.tangem.domain.dynamicaddresses.repository.DynamicAddressesRepository
|
||||
import com.tangem.common.TangemBlogUrlBuilder
|
||||
import com.tangem.common.routing.AppRoute
|
||||
import com.tangem.common.routing.AppRouter
|
||||
|
|
@ -176,6 +177,7 @@ internal class TokenDetailsModel @Inject constructor(
|
|||
private val signCloreMessageUseCase: SignCloreMessageUseCase,
|
||||
private val isXpubSupportedUseCase: IsXpubSupportedUseCase,
|
||||
private val dynamicAddressesFeatureToggles: DynamicAddressesFeatureToggles,
|
||||
private val dynamicAddressesRepository: DynamicAddressesRepository,
|
||||
private val dynamicAddressesDelegateFactory: DynamicAddressesDelegate.Factory,
|
||||
private val dialogFactory: TokenDetailsDialogFactory,
|
||||
private val userWalletsListRepository: UserWalletsListRepository,
|
||||
|
|
@ -1258,7 +1260,7 @@ internal class TokenDetailsModel @Inject constructor(
|
|||
return TokenDetailsBottomSheetConfig.Receive(receiveConfig)
|
||||
}
|
||||
|
||||
private fun sendOneTimeBalanceLoadedAnalyticsEvent(cryptoCurrencyStatus: CryptoCurrencyStatus?) {
|
||||
private suspend fun sendOneTimeBalanceLoadedAnalyticsEvent(cryptoCurrencyStatus: CryptoCurrencyStatus?) {
|
||||
if (isBalanceLoadedEventSent || cryptoCurrencyStatus == null) return
|
||||
|
||||
val tokenBalance = when (val value = cryptoCurrencyStatus.value) {
|
||||
|
|
@ -1290,11 +1292,19 @@ internal class TokenDetailsModel @Inject constructor(
|
|||
blockchain = cryptoCurrency.network.name,
|
||||
token = cryptoCurrency.symbol,
|
||||
tokenBalance = tokenBalance,
|
||||
isDynamicAddress = getIsDynamicAddressParam(),
|
||||
),
|
||||
)
|
||||
isBalanceLoadedEventSent = true
|
||||
}
|
||||
|
||||
private suspend fun getIsDynamicAddressParam(): Boolean? {
|
||||
if (!DynamicAddressesSupportedBlockchains.isSupportedByNetworkId(cryptoCurrency.network.rawId)) return null
|
||||
return dynamicAddressesRepository
|
||||
.isDynamicAddressesEnabledForNetwork(userWalletId, cryptoCurrency.network.id)
|
||||
.first()
|
||||
}
|
||||
|
||||
private suspend fun needShowYieldSupplyWarning(): Boolean {
|
||||
return needShowYieldSupplyDepositedWarningUseCase(cryptoCurrencyStatus)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,137 @@
|
|||
package com.tangem.feature.tokendetails.presentation.tokendetails.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.models.currency.CryptoCurrency
|
||||
import com.tangem.domain.models.network.Network
|
||||
import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenDetailsState
|
||||
import com.tangem.feature.tokendetails.presentation.tokendetails.state.components.TokenDetailsNotification
|
||||
import io.mockk.every
|
||||
import io.mockk.mockk
|
||||
import io.mockk.slot
|
||||
import io.mockk.verify
|
||||
import kotlinx.collections.immutable.toPersistentList
|
||||
import org.junit.jupiter.api.Test
|
||||
|
||||
internal class TokenDetailsNotificationsAnalyticsSenderTest {
|
||||
|
||||
private val analyticsEventHandler: AnalyticsEventHandler = mockk(relaxed = true)
|
||||
|
||||
private val network: Network = mockk(relaxed = true) {
|
||||
every { name } returns "Ethereum"
|
||||
}
|
||||
private val cryptoCurrency: CryptoCurrency = mockk(relaxed = true) {
|
||||
every { symbol } returns "ETH"
|
||||
every { this@mockk.network } returns this@TokenDetailsNotificationsAnalyticsSenderTest.network
|
||||
}
|
||||
|
||||
private val sender = TokenDetailsNotificationsAnalyticsSender(
|
||||
cryptoCurrency = cryptoCurrency,
|
||||
analyticsEventHandler = analyticsEventHandler,
|
||||
)
|
||||
|
||||
@Test
|
||||
fun `GIVEN NetworkFee notification WHEN send THEN NotEnoughFee event with DetailedScreen source is sent`() {
|
||||
// GIVEN
|
||||
val notification = mockk<TokenDetailsNotification.NetworkFee>()
|
||||
val displayedState = createState(notifications = emptyList(), isRefreshing = false)
|
||||
val eventSlot = slot<AnalyticsEvent>()
|
||||
every { analyticsEventHandler.send(capture(eventSlot)) } returns Unit
|
||||
|
||||
// WHEN
|
||||
sender.send(displayedUiState = displayedState, newNotifications = listOf(notification))
|
||||
|
||||
// THEN
|
||||
val event = eventSlot.captured as TokenDetailsAnalyticsEvent.Notice.NotEnoughFee
|
||||
assertThat(event.category).isEqualTo("Token")
|
||||
assertThat(event.event).isEqualTo("Notice - Not Enough Fee")
|
||||
assertThat(event.params).containsEntry("Token", "ETH")
|
||||
assertThat(event.params).containsEntry("Blockchain", "Ethereum")
|
||||
assertThat(event.params).containsEntry("Source", "Detailed Screen")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN NetworkFeeWithBuyButton notification WHEN send THEN NotEnoughFee event with DetailedScreen source is sent`() {
|
||||
// GIVEN
|
||||
val notification = mockk<TokenDetailsNotification.NetworkFeeWithBuyButton>()
|
||||
val displayedState = createState(notifications = emptyList(), isRefreshing = false)
|
||||
val eventSlot = slot<AnalyticsEvent>()
|
||||
every { analyticsEventHandler.send(capture(eventSlot)) } returns Unit
|
||||
|
||||
// WHEN
|
||||
sender.send(displayedUiState = displayedState, newNotifications = listOf(notification))
|
||||
|
||||
// THEN
|
||||
val event = eventSlot.captured as TokenDetailsAnalyticsEvent.Notice.NotEnoughFee
|
||||
assertThat(event.params).containsEntry("Source", "Detailed Screen")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN DynamicAddressesFundsFound notification WHEN send THEN AdditionalAddressesFound event is sent`() {
|
||||
// GIVEN
|
||||
val notification = TokenDetailsNotification.DynamicAddressesFundsFound(onLearnMoreClick = {})
|
||||
val displayedState = createState(notifications = emptyList(), isRefreshing = false)
|
||||
val eventSlot = slot<AnalyticsEvent>()
|
||||
every { analyticsEventHandler.send(capture(eventSlot)) } returns Unit
|
||||
|
||||
// WHEN
|
||||
sender.send(displayedUiState = displayedState, newNotifications = listOf(notification))
|
||||
|
||||
// THEN
|
||||
verify(exactly = 1) { analyticsEventHandler.send(any()) }
|
||||
val event = eventSlot.captured as TokenDetailsAnalyticsEvent.Notice.AdditionalAddressesFound
|
||||
assertThat(event.category).isEqualTo("Token")
|
||||
assertThat(event.event).isEqualTo("Notice - Additional Addresses Found")
|
||||
assertThat(event.params).containsEntry("Token", "ETH")
|
||||
assertThat(event.params).containsEntry("Blockchain", "Ethereum")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN empty new notifications WHEN send THEN no event is sent`() {
|
||||
// GIVEN
|
||||
val displayedState = createState(notifications = emptyList(), isRefreshing = false)
|
||||
|
||||
// WHEN
|
||||
sender.send(displayedUiState = displayedState, newNotifications = emptyList())
|
||||
|
||||
// THEN
|
||||
verify(exactly = 0) { analyticsEventHandler.send(any()) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN pullToRefresh is refreshing WHEN send THEN no event is sent`() {
|
||||
// GIVEN
|
||||
val notification = TokenDetailsNotification.DynamicAddressesFundsFound(onLearnMoreClick = {})
|
||||
val displayedState = createState(notifications = emptyList(), isRefreshing = true)
|
||||
|
||||
// WHEN
|
||||
sender.send(displayedUiState = displayedState, newNotifications = listOf(notification))
|
||||
|
||||
// THEN
|
||||
verify(exactly = 0) { analyticsEventHandler.send(any()) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN notification without matching event WHEN send THEN no event is sent`() {
|
||||
// GIVEN: NetworksUnreachable is an unmapped notification (returns null in getEvent)
|
||||
val notification = TokenDetailsNotification.NetworksUnreachable
|
||||
val displayedState = createState(notifications = emptyList(), isRefreshing = false)
|
||||
|
||||
// WHEN
|
||||
sender.send(displayedUiState = displayedState, newNotifications = listOf(notification))
|
||||
|
||||
// THEN
|
||||
verify(exactly = 0) { analyticsEventHandler.send(any()) }
|
||||
}
|
||||
|
||||
private fun createState(
|
||||
notifications: List<TokenDetailsNotification>,
|
||||
isRefreshing: Boolean,
|
||||
): TokenDetailsState {
|
||||
return mockk(relaxed = true) {
|
||||
every { this@mockk.notifications } returns notifications.toPersistentList()
|
||||
every { pullToRefreshConfig.isRefreshing } returns isRefreshing
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,441 @@
|
|||
package com.tangem.feature.tokendetails.presentation.tokendetails.model
|
||||
|
||||
import arrow.core.left
|
||||
import arrow.core.right
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import com.tangem.blockchain.common.TransactionData
|
||||
import com.tangem.common.core.TangemSdkError
|
||||
import com.tangem.core.analytics.api.AnalyticsEventHandler
|
||||
import com.tangem.core.analytics.models.AnalyticsEvent
|
||||
import com.tangem.core.decompose.ui.UiMessageSender
|
||||
import com.tangem.domain.appcurrency.model.AppCurrency
|
||||
import com.tangem.domain.dynamicaddresses.CreateConsolidationTransactionUseCase
|
||||
import com.tangem.domain.dynamicaddresses.DisableDynamicAddressesUseCase
|
||||
import com.tangem.domain.dynamicaddresses.EnableDynamicAddressesError
|
||||
import com.tangem.domain.dynamicaddresses.EnableDynamicAddressesUseCase
|
||||
import com.tangem.domain.dynamicaddresses.GetDerivedXpubUseCase
|
||||
import com.tangem.domain.dynamicaddresses.model.DynamicAddressesStatus
|
||||
import com.tangem.domain.dynamicaddresses.repository.DynamicAddressesRepository
|
||||
import com.tangem.domain.models.currency.CryptoCurrency
|
||||
import com.tangem.domain.models.currency.CryptoCurrencyStatus
|
||||
import com.tangem.domain.models.network.Network
|
||||
import com.tangem.domain.models.network.NetworkAddress
|
||||
import com.tangem.domain.models.wallet.UserWallet
|
||||
import com.tangem.domain.models.wallet.UserWalletId
|
||||
import com.tangem.domain.transaction.error.GetFeeError
|
||||
import com.tangem.domain.transaction.error.SendTransactionError
|
||||
import com.tangem.domain.transaction.usecase.GetFeeUseCase
|
||||
import com.tangem.domain.transaction.usecase.SendTransactionUseCase
|
||||
import com.tangem.domain.wallets.usecase.GetExtendedPublicKeyForCurrencyUseCase
|
||||
import com.tangem.feature.tokendetails.presentation.tokendetails.analytics.TokenDetailsAnalyticsEvent
|
||||
import com.tangem.feature.tokendetails.presentation.tokendetails.ui.components.dynamicaddresses.DynamicAddressesBottomSheetConfig
|
||||
import com.tangem.utils.Provider
|
||||
import com.tangem.utils.coroutines.TestingCoroutineDispatcherProvider
|
||||
import io.mockk.coEvery
|
||||
import io.mockk.coVerify
|
||||
import io.mockk.every
|
||||
import io.mockk.mockk
|
||||
import io.mockk.slot
|
||||
import io.mockk.verify
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.SupervisorJob
|
||||
import kotlinx.coroutines.flow.flowOf
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.jupiter.api.Test
|
||||
import java.math.BigDecimal
|
||||
|
||||
private const val TEST_XPUB = "xpub-test-value"
|
||||
private const val TOKEN_SYMBOL = "ETH"
|
||||
private const val BLOCKCHAIN_NAME = "Ethereum"
|
||||
private const val TEST_ADDRESS = "0xTestAddress"
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
internal class DynamicAddressesDelegateTest {
|
||||
|
||||
private val analyticsEventHandler: AnalyticsEventHandler = mockk(relaxed = true)
|
||||
|
||||
private val enableDynamicAddressesUseCase: EnableDynamicAddressesUseCase = mockk()
|
||||
private val disableDynamicAddressesUseCase: DisableDynamicAddressesUseCase = mockk()
|
||||
private val createConsolidationTransactionUseCase: CreateConsolidationTransactionUseCase = mockk()
|
||||
private val getFeeUseCase: GetFeeUseCase = mockk(relaxed = true)
|
||||
private val sendTransactionUseCase: SendTransactionUseCase = mockk(relaxed = true)
|
||||
private val getDerivedXpubUseCase: GetDerivedXpubUseCase = mockk()
|
||||
private val dynamicAddressesRepository: DynamicAddressesRepository = mockk(relaxed = true)
|
||||
private val getExtendedPublicKeyUseCase: GetExtendedPublicKeyForCurrencyUseCase = mockk()
|
||||
private val uiMessageSender: UiMessageSender = mockk(relaxed = true)
|
||||
|
||||
private val network: Network = mockk(relaxed = true) {
|
||||
every { name } returns BLOCKCHAIN_NAME
|
||||
}
|
||||
private val cryptoCurrency: CryptoCurrency = mockk(relaxed = true) {
|
||||
every { symbol } returns TOKEN_SYMBOL
|
||||
every { this@mockk.network } returns this@DynamicAddressesDelegateTest.network
|
||||
}
|
||||
private val cryptoCurrencyStatus: CryptoCurrencyStatus = mockk(relaxed = true) {
|
||||
every { currency } returns cryptoCurrency
|
||||
}
|
||||
|
||||
private val userWalletId = UserWalletId("1234567890ABCDEF")
|
||||
private val userWallet: UserWallet = mockk(relaxed = true) {
|
||||
every { walletId } returns userWalletId
|
||||
}
|
||||
|
||||
private val showBottomSheet: () -> Unit = mockk(relaxed = true)
|
||||
private val dismissBottomSheet: () -> Unit = mockk(relaxed = true)
|
||||
private val onDynamicAddressesStateChanged: () -> Unit = mockk(relaxed = true)
|
||||
|
||||
@Test
|
||||
fun `GIVEN currency is available WHEN onDynamicAddressesClick THEN DynamicAddressesScreenOpened event is sent`() =
|
||||
runTest {
|
||||
// GIVEN
|
||||
every { dynamicAddressesRepository.getStatus(userWalletId, network) } returns
|
||||
flowOf(DynamicAddressesStatus.DISABLED)
|
||||
coEvery { dynamicAddressesRepository.hasConflictingCustomTokens(userWalletId, network) } returns false
|
||||
coEvery { getDerivedXpubUseCase(userWalletId, network) } returns TEST_XPUB
|
||||
val delegate = createDelegate(cryptoCurrencyStatus = cryptoCurrencyStatus)
|
||||
val eventSlot = slot<AnalyticsEvent>()
|
||||
every { analyticsEventHandler.send(capture(eventSlot)) } returns Unit
|
||||
|
||||
// WHEN
|
||||
delegate.onDynamicAddressesClick()
|
||||
|
||||
// THEN
|
||||
val event = eventSlot.captured as TokenDetailsAnalyticsEvent.DynamicAddressesScreenOpened
|
||||
assertThat(event.category).isEqualTo("Token")
|
||||
assertThat(event.event).isEqualTo("Dynamic Addresses Screen Opened")
|
||||
assertThat(event.params).containsEntry("Token", TOKEN_SYMBOL)
|
||||
assertThat(event.params).containsEntry("Blockchain", BLOCKCHAIN_NAME)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN no currency WHEN onDynamicAddressesClick THEN no event is sent`() = runTest {
|
||||
// GIVEN
|
||||
val delegate = createDelegate(cryptoCurrencyStatus = null)
|
||||
|
||||
// WHEN
|
||||
delegate.onDynamicAddressesClick()
|
||||
|
||||
// THEN
|
||||
verify(exactly = 0) { analyticsEventHandler.send(any()) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN DISABLED status AND conflicts WHEN onDynamicAddressesClick THEN Notice DynamicAddressesUnavailable is sent`() =
|
||||
runTest {
|
||||
// GIVEN
|
||||
every { dynamicAddressesRepository.getStatus(userWalletId, network) } returns
|
||||
flowOf(DynamicAddressesStatus.DISABLED)
|
||||
coEvery { dynamicAddressesRepository.hasConflictingCustomTokens(userWalletId, network) } returns true
|
||||
val delegate = createDelegate(cryptoCurrencyStatus = cryptoCurrencyStatus)
|
||||
|
||||
// WHEN
|
||||
delegate.onDynamicAddressesClick()
|
||||
|
||||
// THEN
|
||||
verify {
|
||||
analyticsEventHandler.send(
|
||||
match<TokenDetailsAnalyticsEvent.Notice.DynamicAddressesUnavailable> {
|
||||
it.event == "Notice - Dynamic Addresses Unavailable" &&
|
||||
it.params["Token"] == TOKEN_SYMBOL &&
|
||||
it.params["Blockchain"] == BLOCKCHAIN_NAME
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN DISABLED status WHEN enable button clicked AND enable succeeds THEN ButtonEnable and DynamicAddressesEnabled are sent`() =
|
||||
runTest {
|
||||
// GIVEN
|
||||
every { dynamicAddressesRepository.getStatus(userWalletId, network) } returns
|
||||
flowOf(DynamicAddressesStatus.DISABLED)
|
||||
coEvery { dynamicAddressesRepository.hasConflictingCustomTokens(userWalletId, network) } returns false
|
||||
coEvery { getDerivedXpubUseCase(userWalletId, network) } returns TEST_XPUB
|
||||
coEvery { getExtendedPublicKeyUseCase(userWalletId, network) } returns TEST_XPUB.right()
|
||||
coEvery { enableDynamicAddressesUseCase(userWalletId, network, TEST_XPUB) } returns Unit.right()
|
||||
|
||||
val delegate = createDelegate(cryptoCurrencyStatus = cryptoCurrencyStatus)
|
||||
delegate.onDynamicAddressesClick()
|
||||
|
||||
// WHEN
|
||||
(delegate.bottomSheetConfig.value as DynamicAddressesBottomSheetConfig.Enable).onEnableClick()
|
||||
|
||||
// THEN
|
||||
verify {
|
||||
analyticsEventHandler.send(
|
||||
match<TokenDetailsAnalyticsEvent.ButtonEnableDynamicAddresses> {
|
||||
it.event == "Button - Enable Dynamic Addresses" &&
|
||||
it.params["Token"] == TOKEN_SYMBOL
|
||||
},
|
||||
)
|
||||
analyticsEventHandler.send(
|
||||
match<TokenDetailsAnalyticsEvent.DynamicAddressesEnabled> {
|
||||
it.event == "Dynamic Addresses Enabled" &&
|
||||
it.params["Token"] == TOKEN_SYMBOL
|
||||
},
|
||||
)
|
||||
}
|
||||
coVerify { enableDynamicAddressesUseCase(userWalletId, network, TEST_XPUB) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN DISABLED status WHEN enable button clicked AND xpub retrieval fails THEN Error DynamicAddressesUnavailable is sent`() =
|
||||
runTest {
|
||||
// GIVEN
|
||||
every { dynamicAddressesRepository.getStatus(userWalletId, network) } returns
|
||||
flowOf(DynamicAddressesStatus.DISABLED)
|
||||
coEvery { dynamicAddressesRepository.hasConflictingCustomTokens(userWalletId, network) } returns false
|
||||
coEvery { getDerivedXpubUseCase(userWalletId, network) } returns TEST_XPUB
|
||||
coEvery { getExtendedPublicKeyUseCase(userWalletId, network) } returns
|
||||
IllegalStateException("xpub fail").left()
|
||||
|
||||
val delegate = createDelegate(cryptoCurrencyStatus = cryptoCurrencyStatus)
|
||||
delegate.onDynamicAddressesClick()
|
||||
|
||||
// WHEN
|
||||
(delegate.bottomSheetConfig.value as DynamicAddressesBottomSheetConfig.Enable).onEnableClick()
|
||||
|
||||
// THEN
|
||||
verify {
|
||||
analyticsEventHandler.send(
|
||||
match<TokenDetailsAnalyticsEvent.Error.DynamicAddressesUnavailable> {
|
||||
it.event == "Error - Dynamic Addresses Unavailable" &&
|
||||
it.params["Token"] == TOKEN_SYMBOL
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN DISABLED status WHEN enable button clicked AND user cancels xpub derivation THEN Error event is NOT sent`() =
|
||||
runTest {
|
||||
// GIVEN
|
||||
every { dynamicAddressesRepository.getStatus(userWalletId, network) } returns
|
||||
flowOf(DynamicAddressesStatus.DISABLED)
|
||||
coEvery { dynamicAddressesRepository.hasConflictingCustomTokens(userWalletId, network) } returns false
|
||||
coEvery { getDerivedXpubUseCase(userWalletId, network) } returns TEST_XPUB
|
||||
coEvery { getExtendedPublicKeyUseCase(userWalletId, network) } returns
|
||||
TangemSdkError.UserCancelled().left()
|
||||
|
||||
val delegate = createDelegate(cryptoCurrencyStatus = cryptoCurrencyStatus)
|
||||
delegate.onDynamicAddressesClick()
|
||||
|
||||
// WHEN
|
||||
(delegate.bottomSheetConfig.value as DynamicAddressesBottomSheetConfig.Enable).onEnableClick()
|
||||
|
||||
// THEN
|
||||
verify(exactly = 0) {
|
||||
analyticsEventHandler.send(ofType<TokenDetailsAnalyticsEvent.Error.DynamicAddressesUnavailable>())
|
||||
analyticsEventHandler.send(ofType<TokenDetailsAnalyticsEvent.DynamicAddressesEnabled>())
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN DISABLED status WHEN enable useCase fails THEN Error DynamicAddressesUnavailable is sent`() = runTest {
|
||||
// GIVEN
|
||||
every { dynamicAddressesRepository.getStatus(userWalletId, network) } returns
|
||||
flowOf(DynamicAddressesStatus.DISABLED)
|
||||
coEvery { dynamicAddressesRepository.hasConflictingCustomTokens(userWalletId, network) } returns false
|
||||
coEvery { getDerivedXpubUseCase(userWalletId, network) } returns TEST_XPUB
|
||||
coEvery { getExtendedPublicKeyUseCase(userWalletId, network) } returns TEST_XPUB.right()
|
||||
coEvery { enableDynamicAddressesUseCase(userWalletId, network, TEST_XPUB) } returns
|
||||
EnableDynamicAddressesError.ServiceError(RuntimeException("boom")).left()
|
||||
|
||||
val delegate = createDelegate(cryptoCurrencyStatus = cryptoCurrencyStatus)
|
||||
delegate.onDynamicAddressesClick()
|
||||
|
||||
// WHEN
|
||||
(delegate.bottomSheetConfig.value as DynamicAddressesBottomSheetConfig.Enable).onEnableClick()
|
||||
|
||||
// THEN
|
||||
verify {
|
||||
analyticsEventHandler.send(ofType<TokenDetailsAnalyticsEvent.Error.DynamicAddressesUnavailable>())
|
||||
}
|
||||
verify(exactly = 0) {
|
||||
analyticsEventHandler.send(ofType<TokenDetailsAnalyticsEvent.DynamicAddressesEnabled>())
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN ENABLED status AND no consolidation WHEN simple disable clicked THEN ButtonDisable and DynamicAddressesDisabled are sent`() =
|
||||
runTest {
|
||||
// GIVEN
|
||||
every { dynamicAddressesRepository.getStatus(userWalletId, network) } returns
|
||||
flowOf(DynamicAddressesStatus.ENABLED)
|
||||
coEvery { disableDynamicAddressesUseCase(userWalletId, network) } returns false.right()
|
||||
coEvery { dynamicAddressesRepository.disable(userWalletId, network) } returns Unit
|
||||
|
||||
val delegate = createDelegate(cryptoCurrencyStatus = cryptoCurrencyStatus)
|
||||
delegate.onDynamicAddressesClick()
|
||||
|
||||
// WHEN
|
||||
(delegate.bottomSheetConfig.value as DynamicAddressesBottomSheetConfig.DisableWithoutConsolidation)
|
||||
.onDisableClick()
|
||||
|
||||
// THEN
|
||||
verify {
|
||||
analyticsEventHandler.send(
|
||||
match<TokenDetailsAnalyticsEvent.ButtonDisableDynamicAddresses> {
|
||||
it.event == "Button - Disable Dynamic Addresses"
|
||||
},
|
||||
)
|
||||
analyticsEventHandler.send(
|
||||
match<TokenDetailsAnalyticsEvent.DynamicAddressesDisabled> {
|
||||
it.event == "Dynamic Addresses Disabled"
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN consolidation required AND fee fails WHEN load fee THEN NotEnoughFee with DynamicAddresses source is sent`() =
|
||||
runTest {
|
||||
// GIVEN: consolidation required, status provides balance and address, fee load fails
|
||||
setupConsolidationFlow()
|
||||
coEvery {
|
||||
getFeeUseCase(any<BigDecimal>(), any<String>(), userWallet, cryptoCurrency)
|
||||
} returns mockk<GetFeeError>(relaxed = true).left()
|
||||
val events = mutableListOf<AnalyticsEvent>()
|
||||
every { analyticsEventHandler.send(capture(events)) } returns Unit
|
||||
|
||||
// WHEN
|
||||
val delegate = createDelegate(cryptoCurrencyStatus = cryptoCurrencyStatus)
|
||||
delegate.onDynamicAddressesClick()
|
||||
|
||||
// THEN
|
||||
val notEnoughFee = events
|
||||
.filterIsInstance<TokenDetailsAnalyticsEvent.Notice.NotEnoughFee>()
|
||||
.single()
|
||||
assertThat(notEnoughFee.event).isEqualTo("Notice - Not Enough Fee")
|
||||
assertThat(notEnoughFee.params).containsEntry("Source", "Dynamic Addresses")
|
||||
assertThat(notEnoughFee.params).containsEntry("Token", TOKEN_SYMBOL)
|
||||
assertThat(notEnoughFee.params).containsEntry("Blockchain", BLOCKCHAIN_NAME)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN consolidation required AND fee fails WHEN load fee retried THEN NotEnoughFee is sent only once`() =
|
||||
runTest {
|
||||
// GIVEN
|
||||
setupConsolidationFlow()
|
||||
coEvery {
|
||||
getFeeUseCase(any<BigDecimal>(), any<String>(), userWallet, cryptoCurrency)
|
||||
} returns mockk<GetFeeError>(relaxed = true).left()
|
||||
val delegate = createDelegate(cryptoCurrencyStatus = cryptoCurrencyStatus)
|
||||
|
||||
// WHEN: initial load + refresh
|
||||
delegate.onDynamicAddressesClick()
|
||||
(delegate.bottomSheetConfig.value as DynamicAddressesBottomSheetConfig.DisableWithConsolidation)
|
||||
.onRefreshFee()
|
||||
|
||||
// THEN: one-time event sender collapses repeated errors
|
||||
verify(exactly = 1) {
|
||||
analyticsEventHandler.send(ofType<TokenDetailsAnalyticsEvent.Notice.NotEnoughFee>())
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN consolidation flow WHEN disable clicked AND tx succeeds THEN ButtonDisable and DynamicAddressesDisabled are sent`() =
|
||||
runTest {
|
||||
// GIVEN
|
||||
setupConsolidationFlow()
|
||||
coEvery {
|
||||
getFeeUseCase(any<BigDecimal>(), any<String>(), userWallet, cryptoCurrency)
|
||||
} returns mockk<GetFeeError>(relaxed = true).left()
|
||||
val txData = mockk<TransactionData>(relaxed = true)
|
||||
coEvery { createConsolidationTransactionUseCase(userWalletId, network) } returns txData.right()
|
||||
coEvery { sendTransactionUseCase(txData, userWallet, network) } returns "tx-hash".right()
|
||||
coEvery { dynamicAddressesRepository.disable(userWalletId, network) } returns Unit
|
||||
|
||||
val delegate = createDelegate(cryptoCurrencyStatus = cryptoCurrencyStatus)
|
||||
delegate.onDynamicAddressesClick()
|
||||
|
||||
// WHEN
|
||||
(delegate.bottomSheetConfig.value as DynamicAddressesBottomSheetConfig.DisableWithConsolidation)
|
||||
.onDisableClick()
|
||||
|
||||
// THEN
|
||||
verify {
|
||||
analyticsEventHandler.send(ofType<TokenDetailsAnalyticsEvent.ButtonDisableDynamicAddresses>())
|
||||
analyticsEventHandler.send(ofType<TokenDetailsAnalyticsEvent.DynamicAddressesDisabled>())
|
||||
}
|
||||
coVerify { sendTransactionUseCase(txData, userWallet, network) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN consolidation flow WHEN disable clicked AND tx cancelled by user THEN DynamicAddressesDisabled is NOT sent`() =
|
||||
runTest {
|
||||
// GIVEN
|
||||
setupConsolidationFlow()
|
||||
coEvery {
|
||||
getFeeUseCase(any<BigDecimal>(), any<String>(), userWallet, cryptoCurrency)
|
||||
} returns mockk<GetFeeError>(relaxed = true).left()
|
||||
val txData = mockk<TransactionData>(relaxed = true)
|
||||
coEvery { createConsolidationTransactionUseCase(userWalletId, network) } returns txData.right()
|
||||
coEvery { sendTransactionUseCase(txData, userWallet, network) } returns
|
||||
SendTransactionError.UserCancelledError.left()
|
||||
|
||||
val delegate = createDelegate(cryptoCurrencyStatus = cryptoCurrencyStatus)
|
||||
delegate.onDynamicAddressesClick()
|
||||
|
||||
// WHEN
|
||||
(delegate.bottomSheetConfig.value as DynamicAddressesBottomSheetConfig.DisableWithConsolidation)
|
||||
.onDisableClick()
|
||||
|
||||
// THEN: ButtonDisable is sent on click, but success event is not
|
||||
verify { analyticsEventHandler.send(ofType<TokenDetailsAnalyticsEvent.ButtonDisableDynamicAddresses>()) }
|
||||
verify(exactly = 0) {
|
||||
analyticsEventHandler.send(ofType<TokenDetailsAnalyticsEvent.DynamicAddressesDisabled>())
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN Notice category event WHEN sent THEN id starts with Token category`() {
|
||||
// GIVEN
|
||||
val notice = TokenDetailsAnalyticsEvent.Notice.DynamicAddressesUnavailable(cryptoCurrency)
|
||||
val error = TokenDetailsAnalyticsEvent.Error.DynamicAddressesUnavailable(cryptoCurrency)
|
||||
|
||||
// THEN
|
||||
assertThat(notice.category).isEqualTo("Token")
|
||||
assertThat(notice.event).isEqualTo("Notice - Dynamic Addresses Unavailable")
|
||||
assertThat(error.category).isEqualTo("Token")
|
||||
assertThat(error.event).isEqualTo("Error - Dynamic Addresses Unavailable")
|
||||
}
|
||||
|
||||
private fun setupConsolidationFlow() {
|
||||
every { dynamicAddressesRepository.getStatus(userWalletId, network) } returns
|
||||
flowOf(DynamicAddressesStatus.ENABLED)
|
||||
coEvery { disableDynamicAddressesUseCase(userWalletId, network) } returns true.right()
|
||||
val address = NetworkAddress.Address(value = TEST_ADDRESS, type = NetworkAddress.Address.Type.Primary)
|
||||
every { cryptoCurrencyStatus.value } returns mockk(relaxed = true) {
|
||||
every { amount } returns BigDecimal.ONE
|
||||
every { fiatRate } returns BigDecimal.ONE
|
||||
every { networkAddress } returns NetworkAddress.Single(defaultAddress = address)
|
||||
}
|
||||
}
|
||||
|
||||
private fun createDelegate(cryptoCurrencyStatus: CryptoCurrencyStatus?): DynamicAddressesDelegate {
|
||||
val scope = CoroutineScope(Dispatchers.Unconfined + SupervisorJob())
|
||||
return DynamicAddressesDelegate(
|
||||
enableDynamicAddressesUseCase = enableDynamicAddressesUseCase,
|
||||
disableDynamicAddressesUseCase = disableDynamicAddressesUseCase,
|
||||
createConsolidationTransactionUseCase = createConsolidationTransactionUseCase,
|
||||
getFeeUseCase = getFeeUseCase,
|
||||
sendTransactionUseCase = sendTransactionUseCase,
|
||||
getDerivedXpubUseCase = getDerivedXpubUseCase,
|
||||
dynamicAddressesRepository = dynamicAddressesRepository,
|
||||
getExtendedPublicKeyUseCase = getExtendedPublicKeyUseCase,
|
||||
analyticsEventHandler = analyticsEventHandler,
|
||||
uiMessageSender = uiMessageSender,
|
||||
dispatchers = TestingCoroutineDispatcherProvider(),
|
||||
userWallet = userWallet,
|
||||
cryptoCurrencyStatusProvider = Provider { cryptoCurrencyStatus },
|
||||
appCurrencyProvider = Provider { mockk<AppCurrency>(relaxed = true) },
|
||||
coroutineScope = scope,
|
||||
showBottomSheet = showBottomSheet,
|
||||
dismissBottomSheet = dismissBottomSheet,
|
||||
onDynamicAddressesStateChanged = onDynamicAddressesStateChanged,
|
||||
)
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue