Updated on 2026-08-14

This commit is contained in:
Tangem 2026-06-05 13:23:35 +03:00
parent 1c2c5a69bd
commit 0d7824736a
3 changed files with 57 additions and 22 deletions

View file

@ -1,10 +1,12 @@
package com.tangem.feature.tokendetails.presentation.tokendetails.model
import com.tangem.common.TangemBlogUrlBuilder
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.navigation.url.UrlOpener
import com.tangem.core.res.R
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.message.SnackbarMessage
@ -55,6 +57,7 @@ internal class DynamicAddressesDelegate @AssistedInject constructor(
private val analyticsEventHandler: AnalyticsEventHandler,
@GlobalUiMessageSender private val uiMessageSender: UiMessageSender,
private val dispatchers: CoroutineDispatcherProvider,
private val urlOpener: UrlOpener,
@Assisted private val userWallet: UserWallet,
@Assisted private val cryptoCurrencyStatusProvider: Provider<CryptoCurrencyStatus?>,
@Assisted private val appCurrencyProvider: Provider<AppCurrency>,
@ -77,7 +80,7 @@ internal class DynamicAddressesDelegate @AssistedInject constructor(
// region Entry point
fun onDynamicAddressesClick() {
fun openBottomSheet() {
val currency = cryptoCurrencyStatusProvider()?.currency ?: return
analyticsEventHandler.send(TokenDetailsAnalyticsEvent.DynamicAddressesScreenOpened(currency))
coroutineScope.launch(dispatchers.main) {
@ -313,7 +316,9 @@ internal class DynamicAddressesDelegate @AssistedInject constructor(
}
private fun onReadMoreClick() {
// TODO: Replace with actual URL
coroutineScope.launch(dispatchers.main) {
urlOpener.openUrl(TangemBlogUrlBuilder.build(TangemBlogUrlBuilder.Post.WhatIsTransactionFee))
}
}
private fun onDisableClick() {

View file

@ -680,11 +680,9 @@ internal class TokenDetailsModel @Inject constructor(
openStaking()
}
override fun onDynamicAddressesClick() = dynamicAddressesDelegate.onDynamicAddressesClick()
override fun onDynamicAddressesClick() = dynamicAddressesDelegate.openBottomSheet()
override fun onDynamicAddressesFundsFoundLearnMoreClick() {
// TODO: open "Learn more" URL once the destination is decided
}
override fun onDynamicAddressesFundsFoundLearnMoreClick() = dynamicAddressesDelegate.openBottomSheet()
private fun onDynamicAddressesStateChanged() {
updateTopBarMenu()

View file

@ -4,10 +4,12 @@ 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.TangemBlogUrlBuilder
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.core.navigation.url.UrlOpener
import com.tangem.domain.appcurrency.model.AppCurrency
import com.tangem.domain.dynamicaddresses.CreateConsolidationTransactionUseCase
import com.tangem.domain.dynamicaddresses.IsDynamicAddressesConsolidationRequiredUseCase
@ -35,7 +37,9 @@ import io.mockk.coEvery
import io.mockk.coVerify
import io.mockk.every
import io.mockk.mockk
import io.mockk.mockkObject
import io.mockk.slot
import io.mockk.unmockkObject
import io.mockk.verify
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
@ -50,6 +54,7 @@ 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"
private const val TEST_BLOG_URL = "https://tangem.com/embed/blog/post/what-is-a-transaction-fee-and-why-do-we-need-it"
@OptIn(ExperimentalCoroutinesApi::class)
internal class DynamicAddressesDelegateTest {
@ -65,6 +70,7 @@ internal class DynamicAddressesDelegateTest {
private val dynamicAddressesRepository: DynamicAddressesRepository = mockk(relaxed = true)
private val getExtendedPublicKeyUseCase: GetExtendedPublicKeyForCurrencyUseCase = mockk()
private val uiMessageSender: UiMessageSender = mockk(relaxed = true)
private val urlOpener: UrlOpener = mockk(relaxed = true)
private val network: Network = mockk(relaxed = true) {
every { name } returns BLOCKCHAIN_NAME
@ -87,7 +93,7 @@ internal class DynamicAddressesDelegateTest {
private val onDynamicAddressesStateChanged: () -> Unit = mockk(relaxed = true)
@Test
fun `GIVEN currency is available WHEN onDynamicAddressesClick THEN DynamicAddressesScreenOpened event is sent`() =
fun `GIVEN currency is available WHEN openBottomSheet THEN DynamicAddressesScreenOpened event is sent`() =
runTest {
// GIVEN
every { dynamicAddressesRepository.getStatus(userWalletId, network) } returns
@ -99,7 +105,7 @@ internal class DynamicAddressesDelegateTest {
every { analyticsEventHandler.send(capture(eventSlot)) } returns Unit
// WHEN
delegate.onDynamicAddressesClick()
delegate.openBottomSheet()
// THEN
val event = eventSlot.captured as TokenDetailsAnalyticsEvent.DynamicAddressesScreenOpened
@ -110,19 +116,19 @@ internal class DynamicAddressesDelegateTest {
}
@Test
fun `GIVEN no currency WHEN onDynamicAddressesClick THEN no event is sent`() = runTest {
fun `GIVEN no currency WHEN openBottomSheet THEN no event is sent`() = runTest {
// GIVEN
val delegate = createDelegate(cryptoCurrencyStatus = null)
// WHEN
delegate.onDynamicAddressesClick()
delegate.openBottomSheet()
// THEN
verify(exactly = 0) { analyticsEventHandler.send(any()) }
}
@Test
fun `GIVEN DISABLED status AND conflicts WHEN onDynamicAddressesClick THEN Notice DynamicAddressesUnavailable is sent`() =
fun `GIVEN DISABLED status AND conflicts WHEN openBottomSheet THEN Notice DynamicAddressesUnavailable is sent`() =
runTest {
// GIVEN
every { dynamicAddressesRepository.getStatus(userWalletId, network) } returns
@ -131,7 +137,7 @@ internal class DynamicAddressesDelegateTest {
val delegate = createDelegate(cryptoCurrencyStatus = cryptoCurrencyStatus)
// WHEN
delegate.onDynamicAddressesClick()
delegate.openBottomSheet()
// THEN
verify {
@ -157,7 +163,7 @@ internal class DynamicAddressesDelegateTest {
coEvery { enableDynamicAddressesUseCase(userWalletId, network, TEST_XPUB) } returns Unit.right()
val delegate = createDelegate(cryptoCurrencyStatus = cryptoCurrencyStatus)
delegate.onDynamicAddressesClick()
delegate.openBottomSheet()
// WHEN
(delegate.bottomSheetConfig.value as DynamicAddressesBottomSheetConfig.Enable).onEnableClick()
@ -192,7 +198,7 @@ internal class DynamicAddressesDelegateTest {
IllegalStateException("xpub fail").left()
val delegate = createDelegate(cryptoCurrencyStatus = cryptoCurrencyStatus)
delegate.onDynamicAddressesClick()
delegate.openBottomSheet()
// WHEN
(delegate.bottomSheetConfig.value as DynamicAddressesBottomSheetConfig.Enable).onEnableClick()
@ -220,7 +226,7 @@ internal class DynamicAddressesDelegateTest {
TangemSdkError.UserCancelled().left()
val delegate = createDelegate(cryptoCurrencyStatus = cryptoCurrencyStatus)
delegate.onDynamicAddressesClick()
delegate.openBottomSheet()
// WHEN
(delegate.bottomSheetConfig.value as DynamicAddressesBottomSheetConfig.Enable).onEnableClick()
@ -244,7 +250,7 @@ internal class DynamicAddressesDelegateTest {
EnableDynamicAddressesError.ServiceError(RuntimeException("boom")).left()
val delegate = createDelegate(cryptoCurrencyStatus = cryptoCurrencyStatus)
delegate.onDynamicAddressesClick()
delegate.openBottomSheet()
// WHEN
(delegate.bottomSheetConfig.value as DynamicAddressesBottomSheetConfig.Enable).onEnableClick()
@ -268,7 +274,7 @@ internal class DynamicAddressesDelegateTest {
coEvery { dynamicAddressesRepository.disable(userWalletId, network) } returns Unit
val delegate = createDelegate(cryptoCurrencyStatus = cryptoCurrencyStatus)
delegate.onDynamicAddressesClick()
delegate.openBottomSheet()
// WHEN
(delegate.bottomSheetConfig.value as DynamicAddressesBottomSheetConfig.DisableWithoutConsolidation)
@ -289,6 +295,31 @@ internal class DynamicAddressesDelegateTest {
}
}
@Test
fun `GIVEN disable sheet WHEN read more clicked THEN transaction fee article is opened`() = runTest {
// GIVEN
every { dynamicAddressesRepository.getStatus(userWalletId, network) } returns
flowOf(DynamicAddressesStatus.ENABLED)
coEvery { isConsolidationRequiredUseCase(userWalletId, network) } returns false.right()
mockkObject(TangemBlogUrlBuilder)
try {
coEvery { TangemBlogUrlBuilder.build(TangemBlogUrlBuilder.Post.WhatIsTransactionFee) } returns TEST_BLOG_URL
val delegate = createDelegate(cryptoCurrencyStatus = cryptoCurrencyStatus)
delegate.openBottomSheet()
// WHEN
(delegate.bottomSheetConfig.value as DynamicAddressesBottomSheetConfig.DisableWithoutConsolidation)
.onReadMoreClick()
// THEN
verify { urlOpener.openUrl(TEST_BLOG_URL) }
} finally {
unmockkObject(TangemBlogUrlBuilder)
}
}
@Test
fun `GIVEN ENABLED status AND no consolidation WHEN menu tapped without confirmation THEN repository disable is NOT called`() =
runTest {
@ -297,7 +328,7 @@ internal class DynamicAddressesDelegateTest {
coEvery { isConsolidationRequiredUseCase(userWalletId, network) } returns false.right()
val delegate = createDelegate(cryptoCurrencyStatus = cryptoCurrencyStatus)
delegate.onDynamicAddressesClick()
delegate.openBottomSheet()
// The simple disable sheet must be shown but no backend write must happen yet.
assertThat(delegate.bottomSheetConfig.value)
@ -318,7 +349,7 @@ internal class DynamicAddressesDelegateTest {
// WHEN
val delegate = createDelegate(cryptoCurrencyStatus = cryptoCurrencyStatus)
delegate.onDynamicAddressesClick()
delegate.openBottomSheet()
// THEN
val notEnoughFee = events
@ -341,7 +372,7 @@ internal class DynamicAddressesDelegateTest {
val delegate = createDelegate(cryptoCurrencyStatus = cryptoCurrencyStatus)
// WHEN: initial load + refresh
delegate.onDynamicAddressesClick()
delegate.openBottomSheet()
(delegate.bottomSheetConfig.value as DynamicAddressesBottomSheetConfig.DisableWithConsolidation)
.onRefreshFee()
@ -365,7 +396,7 @@ internal class DynamicAddressesDelegateTest {
coEvery { dynamicAddressesRepository.disable(userWalletId, network) } returns Unit
val delegate = createDelegate(cryptoCurrencyStatus = cryptoCurrencyStatus)
delegate.onDynamicAddressesClick()
delegate.openBottomSheet()
// WHEN
(delegate.bottomSheetConfig.value as DynamicAddressesBottomSheetConfig.DisableWithConsolidation)
@ -393,7 +424,7 @@ internal class DynamicAddressesDelegateTest {
SendTransactionError.UserCancelledError.left()
val delegate = createDelegate(cryptoCurrencyStatus = cryptoCurrencyStatus)
delegate.onDynamicAddressesClick()
delegate.openBottomSheet()
// WHEN
(delegate.bottomSheetConfig.value as DynamicAddressesBottomSheetConfig.DisableWithConsolidation)
@ -444,6 +475,7 @@ internal class DynamicAddressesDelegateTest {
getExtendedPublicKeyUseCase = getExtendedPublicKeyUseCase,
analyticsEventHandler = analyticsEventHandler,
uiMessageSender = uiMessageSender,
urlOpener = urlOpener,
dispatchers = TestingCoroutineDispatcherProvider(),
userWallet = userWallet,
cryptoCurrencyStatusProvider = Provider { cryptoCurrencyStatus },