Updated on 2026-08-14
This commit is contained in:
parent
361bd8168b
commit
3da4771d66
19 changed files with 605 additions and 14 deletions
|
|
@ -4,7 +4,9 @@ import com.tangem.blockchainsdk.utils.isNeedToCreateAccountWithoutReserve
|
|||
import com.tangem.domain.account.status.supplier.SingleAccountStatusListSupplier
|
||||
import com.tangem.domain.account.status.utils.CryptoCurrencyStatusOperations.getCoinStatus
|
||||
import com.tangem.domain.account.status.utils.CryptoCurrencyStatusOperations.getCryptoCurrencyStatus
|
||||
import com.tangem.domain.dynamicaddresses.repository.DynamicAddressesRepository
|
||||
import com.tangem.domain.models.StatusSource
|
||||
import com.tangem.domain.tokens.model.warnings.DynamicAddressesWarnings
|
||||
import com.tangem.domain.models.currency.CryptoCurrency
|
||||
import com.tangem.domain.models.currency.CryptoCurrencyStatus
|
||||
import com.tangem.domain.models.network.Network
|
||||
|
|
@ -34,6 +36,7 @@ internal class GetCurrencyWarningsUseCase @Inject constructor(
|
|||
private val currencyChecksRepository: CurrencyChecksRepository,
|
||||
private val multiWalletCryptoCurrenciesSupplier: MultiWalletCryptoCurrenciesSupplier,
|
||||
private val singleAccountStatusListSupplier: SingleAccountStatusListSupplier,
|
||||
private val dynamicAddressesRepository: DynamicAddressesRepository,
|
||||
) {
|
||||
|
||||
suspend operator fun invoke(
|
||||
|
|
@ -52,7 +55,12 @@ internal class GetCurrencyWarningsUseCase @Inject constructor(
|
|||
flow2 = flowOf(currencyChecksRepository.getRentInfoWarning(userWalletId, currencyStatus)),
|
||||
flow3 = flowOf(currencyChecksRepository.getExistentialDeposit(userWalletId, currency.network)),
|
||||
flow4 = flowOf(currencyChecksRepository.getFeeResourceAmount(userWalletId, currency.network)),
|
||||
) { coinRelatedWarnings, maybeRentWarning, maybeEdWarning, maybeFeeResource ->
|
||||
flow5 = if (currency is CryptoCurrency.Coin) {
|
||||
dynamicAddressesRepository.hasFundsOnAdditionalAddresses(userWalletId, currency.network)
|
||||
} else {
|
||||
flowOf(false)
|
||||
},
|
||||
) { coinRelatedWarnings, maybeRentWarning, maybeEdWarning, maybeFeeResource, hasExtraFunds ->
|
||||
setOfNotNull(
|
||||
maybeRentWarning,
|
||||
maybeEdWarning?.let { getExistentialDepositWarning(currency, it) },
|
||||
|
|
@ -64,6 +72,7 @@ internal class GetCurrencyWarningsUseCase @Inject constructor(
|
|||
getAssetRequirementsWarning(userWalletId = userWalletId, currency = currency),
|
||||
getMigrationFromMaticToPolWarning(currency),
|
||||
getCloreMigrationWarning(currency),
|
||||
DynamicAddressesWarnings.FundsFound.takeIf { hasExtraFunds },
|
||||
)
|
||||
}.flowOn(dispatchers.io)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -62,6 +62,7 @@ internal class TokenDetailsNotificationsAnalyticsSender(
|
|||
is TokenDetailsNotification.MigrationClore,
|
||||
is TokenDetailsNotification.UsedOutdatedData,
|
||||
-> null
|
||||
is TokenDetailsNotification.DynamicAddressesFundsFound -> null // TODO: [REDACTED_TASK_KEY] analytics event
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -50,6 +50,8 @@ interface TokenDetailsClickIntents {
|
|||
|
||||
fun onDynamicAddressesClick()
|
||||
|
||||
fun onDynamicAddressesFundsFoundLearnMoreClick()
|
||||
|
||||
fun onCopyAddress(): TextReference?
|
||||
|
||||
fun onAssociateClick()
|
||||
|
|
@ -129,6 +131,8 @@ internal class EmptyTokenDetailsClickIntents : TokenDetailsClickIntents {
|
|||
|
||||
override fun onDynamicAddressesClick() { /* no op */ }
|
||||
|
||||
override fun onDynamicAddressesFundsFoundLearnMoreClick() { /* no op */ }
|
||||
|
||||
override fun onSellClick(unavailabilityReason: ScenarioUnavailabilityReason) { /* no op */ }
|
||||
|
||||
override fun onSwapClick(unavailabilityReason: ScenarioUnavailabilityReason) { /* no op */ }
|
||||
|
|
|
|||
|
|
@ -707,6 +707,10 @@ internal class TokenDetailsModel @Inject constructor(
|
|||
|
||||
override fun onDynamicAddressesClick() = dynamicAddressesDelegate.onDynamicAddressesClick()
|
||||
|
||||
override fun onDynamicAddressesFundsFoundLearnMoreClick() {
|
||||
// TODO: open "Learn more" URL once the destination is decided
|
||||
}
|
||||
|
||||
private fun onDynamicAddressesStateChanged() {
|
||||
updateTopBarMenu()
|
||||
modelScope.launch(dispatchers.main) {
|
||||
|
|
|
|||
|
|
@ -276,6 +276,17 @@ internal sealed class TokenDetailsNotification(val config: NotificationConfig) {
|
|||
),
|
||||
)
|
||||
|
||||
data class DynamicAddressesFundsFound(
|
||||
private val onLearnMoreClick: () -> Unit,
|
||||
) : Warning(
|
||||
title = resourceReference(id = R.string.dynamic_addresses_notification_funds_found_title),
|
||||
subtitle = resourceReference(id = R.string.dynamic_addresses_notification_funds_found_description),
|
||||
buttonsState = NotificationConfig.ButtonsState.SecondaryButtonConfig(
|
||||
text = resourceReference(id = R.string.common_learn_more),
|
||||
onClick = onLearnMoreClick,
|
||||
),
|
||||
)
|
||||
|
||||
data class YieldSupplyNotTransferedToAave(val tokenName: String, val amount: String) : Warning(
|
||||
title = resourceReference(
|
||||
id = R.string.yield_module_amount_not_transfered_to_aave_title,
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ import com.tangem.core.ui.format.bigdecimal.shorted
|
|||
import com.tangem.domain.models.currency.CryptoCurrency
|
||||
import com.tangem.domain.models.wallet.UserWalletId
|
||||
import com.tangem.domain.tokens.model.warnings.CryptoCurrencyWarning
|
||||
import com.tangem.domain.tokens.model.warnings.DynamicAddressesWarnings
|
||||
import com.tangem.domain.tokens.model.warnings.HederaWarnings
|
||||
import com.tangem.domain.tokens.model.warnings.KaspaWarnings
|
||||
import com.tangem.domain.wallets.usecase.GetUserWalletUseCase
|
||||
|
|
@ -159,6 +160,9 @@ internal class TokenDetailsNotificationConverter(
|
|||
onMigrationClick = clickIntents::onCloreMigrationClick,
|
||||
)
|
||||
is CryptoCurrencyWarning.UsedOutdatedDataWarning -> UsedOutdatedData
|
||||
is DynamicAddressesWarnings.FundsFound -> DynamicAddressesFundsFound(
|
||||
onLearnMoreClick = clickIntents::onDynamicAddressesFundsFoundLearnMoreClick,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ import com.tangem.core.ui.format.bigdecimal.crypto
|
|||
import com.tangem.core.ui.format.bigdecimal.format
|
||||
import com.tangem.domain.models.currency.CryptoCurrency
|
||||
import com.tangem.domain.tokens.model.warnings.CryptoCurrencyWarning
|
||||
import com.tangem.domain.tokens.model.warnings.DynamicAddressesWarnings
|
||||
import com.tangem.domain.tokens.model.warnings.HederaWarnings
|
||||
import com.tangem.domain.tokens.model.warnings.KaspaWarnings
|
||||
import com.tangem.feature.tokendetails.presentation.tokendetails.model.TokenDetailsClickIntents
|
||||
|
|
@ -164,6 +165,20 @@ internal class UpdateNotificationsTransformer(
|
|||
),
|
||||
),
|
||||
)
|
||||
is DynamicAddressesWarnings.FundsFound -> TangemMessageUM(
|
||||
id = "dynamic_addresses_funds_found",
|
||||
title = resourceReference(CoreResR.string.dynamic_addresses_notification_funds_found_title),
|
||||
subtitle = resourceReference(CoreResR.string.dynamic_addresses_notification_funds_found_description),
|
||||
messageEffect = TangemMessageEffect.None,
|
||||
iconUM = TangemIconUM.Icon(R.drawable.ic_attention_default_24),
|
||||
buttonsUM = persistentListOf(
|
||||
TangemMessageButtonUM(
|
||||
text = resourceReference(CoreResR.string.common_learn_more),
|
||||
type = TangemButtonType.Primary,
|
||||
onClick = clickIntents::onDynamicAddressesFundsFoundLearnMoreClick,
|
||||
),
|
||||
),
|
||||
)
|
||||
// Non-warning types — skip for redesign
|
||||
is CryptoCurrencyWarning.ExistentialDeposit,
|
||||
is CryptoCurrencyWarning.Rent,
|
||||
|
|
|
|||
|
|
@ -0,0 +1,146 @@
|
|||
package com.tangem.feature.tokendetails.domain
|
||||
|
||||
import arrow.core.none
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import com.tangem.domain.account.models.AccountStatusList
|
||||
import com.tangem.domain.account.status.supplier.SingleAccountStatusListSupplier
|
||||
import com.tangem.domain.account.status.utils.CryptoCurrencyStatusOperations
|
||||
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.wallet.UserWalletId
|
||||
import com.tangem.domain.tokens.MultiWalletCryptoCurrenciesSupplier
|
||||
import com.tangem.domain.tokens.model.warnings.DynamicAddressesWarnings
|
||||
import com.tangem.domain.tokens.repository.CurrenciesRepository
|
||||
import com.tangem.domain.tokens.repository.CurrencyChecksRepository
|
||||
import com.tangem.domain.walletmanager.WalletManagersFacade
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import io.mockk.coEvery
|
||||
import io.mockk.every
|
||||
import io.mockk.mockk
|
||||
import io.mockk.mockkObject
|
||||
import io.mockk.unmockkObject
|
||||
import io.mockk.verify
|
||||
import kotlinx.coroutines.CoroutineDispatcher
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.flow.flowOf
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.jupiter.api.AfterEach
|
||||
import org.junit.jupiter.api.BeforeEach
|
||||
import org.junit.jupiter.api.Test
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
class GetCurrencyWarningsUseCaseTest {
|
||||
|
||||
private val walletManagersFacade: WalletManagersFacade = mockk(relaxed = true)
|
||||
private val currenciesRepository: CurrenciesRepository = mockk(relaxed = true)
|
||||
private val currencyChecksRepository: CurrencyChecksRepository = mockk(relaxed = true)
|
||||
private val multiWalletCryptoCurrenciesSupplier: MultiWalletCryptoCurrenciesSupplier = mockk(relaxed = true)
|
||||
private val singleAccountStatusListSupplier: SingleAccountStatusListSupplier = mockk(relaxed = true)
|
||||
private val dynamicAddressesRepository: DynamicAddressesRepository = mockk(relaxed = true)
|
||||
private val dispatchers: CoroutineDispatcherProvider = TestDispatchers(Dispatchers.Unconfined)
|
||||
|
||||
private val userWalletId: UserWalletId = mockk(relaxed = true)
|
||||
private val network: Network = mockk(relaxed = true)
|
||||
private val derivationPath: Network.DerivationPath = mockk(relaxed = true)
|
||||
private val accountStatusList: AccountStatusList = mockk(relaxed = true)
|
||||
|
||||
private val useCase = GetCurrencyWarningsUseCase(
|
||||
walletManagersFacade = walletManagersFacade,
|
||||
currenciesRepository = currenciesRepository,
|
||||
dispatchers = dispatchers,
|
||||
currencyChecksRepository = currencyChecksRepository,
|
||||
multiWalletCryptoCurrenciesSupplier = multiWalletCryptoCurrenciesSupplier,
|
||||
singleAccountStatusListSupplier = singleAccountStatusListSupplier,
|
||||
dynamicAddressesRepository = dynamicAddressesRepository,
|
||||
)
|
||||
|
||||
@BeforeEach
|
||||
fun setUp() {
|
||||
// Bypass coin-related warnings flow: force both coin and token lookups to None so
|
||||
// the use case falls through to `SomeNetworksUnreachable` without needing real data.
|
||||
mockkObject(CryptoCurrencyStatusOperations)
|
||||
with(CryptoCurrencyStatusOperations) {
|
||||
every { accountStatusList.getCoinStatus(any<CryptoCurrency>()) } returns none()
|
||||
every { accountStatusList.getCryptoCurrencyStatus(any<CryptoCurrency>()) } returns none()
|
||||
}
|
||||
|
||||
every { singleAccountStatusListSupplier(any<UserWalletId>()) } returns flowOf(accountStatusList)
|
||||
coEvery { currencyChecksRepository.getRentInfoWarning(any(), any()) } returns null
|
||||
coEvery { currencyChecksRepository.getExistentialDeposit(any(), any()) } returns null
|
||||
coEvery { currencyChecksRepository.getFeeResourceAmount(any(), any()) } returns null
|
||||
coEvery { walletManagersFacade.getAssetRequirements(any(), any()) } returns null
|
||||
}
|
||||
|
||||
@AfterEach
|
||||
fun tearDown() {
|
||||
unmockkObject(CryptoCurrencyStatusOperations)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN token currency WHEN invoke THEN FundsFound is absent and probe flow is not queried`() = runTest {
|
||||
// GIVEN
|
||||
val token: CryptoCurrency.Token = mockk(relaxed = true) {
|
||||
every { this@mockk.network } returns this@GetCurrencyWarningsUseCaseTest.network
|
||||
}
|
||||
val currencyStatus = statusFor(token)
|
||||
|
||||
// WHEN
|
||||
val result = useCase.invoke(userWalletId, currencyStatus, derivationPath).first()
|
||||
|
||||
// THEN
|
||||
assertThat(result).doesNotContain(DynamicAddressesWarnings.FundsFound)
|
||||
verify(exactly = 0) {
|
||||
dynamicAddressesRepository.hasFundsOnAdditionalAddresses(any(), any())
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN coin currency AND probe emits true WHEN invoke THEN FundsFound is present`() = runTest {
|
||||
// GIVEN
|
||||
val coin: CryptoCurrency.Coin = mockk(relaxed = true) {
|
||||
every { this@mockk.network } returns this@GetCurrencyWarningsUseCaseTest.network
|
||||
}
|
||||
every { dynamicAddressesRepository.hasFundsOnAdditionalAddresses(userWalletId, network) } returns flowOf(true)
|
||||
val currencyStatus = statusFor(coin)
|
||||
|
||||
// WHEN
|
||||
val result = useCase.invoke(userWalletId, currencyStatus, derivationPath).first()
|
||||
|
||||
// THEN
|
||||
assertThat(result).contains(DynamicAddressesWarnings.FundsFound)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN coin currency AND probe emits false WHEN invoke THEN FundsFound is absent`() = runTest {
|
||||
// GIVEN
|
||||
val coin: CryptoCurrency.Coin = mockk(relaxed = true) {
|
||||
every { this@mockk.network } returns this@GetCurrencyWarningsUseCaseTest.network
|
||||
}
|
||||
every { dynamicAddressesRepository.hasFundsOnAdditionalAddresses(userWalletId, network) } returns flowOf(false)
|
||||
val currencyStatus = statusFor(coin)
|
||||
|
||||
// WHEN
|
||||
val result = useCase.invoke(userWalletId, currencyStatus, derivationPath).first()
|
||||
|
||||
// THEN
|
||||
assertThat(result).doesNotContain(DynamicAddressesWarnings.FundsFound)
|
||||
}
|
||||
|
||||
private fun statusFor(currency: CryptoCurrency): CryptoCurrencyStatus {
|
||||
return mockk(relaxed = true) {
|
||||
every { this@mockk.currency } returns currency
|
||||
}
|
||||
}
|
||||
|
||||
private class TestDispatchers(dispatcher: CoroutineDispatcher) : CoroutineDispatcherProvider {
|
||||
override val main: CoroutineDispatcher = dispatcher
|
||||
override val mainImmediate: CoroutineDispatcher = dispatcher
|
||||
override val io: CoroutineDispatcher = dispatcher
|
||||
override val default: CoroutineDispatcher = dispatcher
|
||||
override val single: CoroutineDispatcher = dispatcher
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,48 @@
|
|||
package com.tangem.feature.tokendetails.presentation.tokendetails.state.factory
|
||||
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import com.tangem.core.ui.components.notifications.NotificationConfig
|
||||
import com.tangem.domain.models.wallet.UserWalletId
|
||||
import com.tangem.domain.tokens.model.warnings.DynamicAddressesWarnings
|
||||
import com.tangem.domain.wallets.usecase.GetUserWalletUseCase
|
||||
import com.tangem.feature.tokendetails.presentation.tokendetails.model.TokenDetailsClickIntents
|
||||
import com.tangem.feature.tokendetails.presentation.tokendetails.state.components.TokenDetailsNotification
|
||||
import io.mockk.mockk
|
||||
import io.mockk.verify
|
||||
import org.junit.jupiter.api.Test
|
||||
|
||||
class TokenDetailsNotificationConverterTest {
|
||||
|
||||
private val clickIntents: TokenDetailsClickIntents = mockk(relaxed = true)
|
||||
private val getUserWalletUseCase: GetUserWalletUseCase = mockk(relaxed = true)
|
||||
private val userWalletId: UserWalletId = mockk(relaxed = true)
|
||||
|
||||
private val converter = TokenDetailsNotificationConverter(
|
||||
userWalletId = userWalletId,
|
||||
getUserWalletUseCase = getUserWalletUseCase,
|
||||
clickIntents = clickIntents,
|
||||
)
|
||||
|
||||
@Test
|
||||
fun `GIVEN FundsFound warning WHEN convert THEN DynamicAddressesFundsFound notification is produced`() {
|
||||
// WHEN
|
||||
val result = converter.convert(setOf(DynamicAddressesWarnings.FundsFound))
|
||||
|
||||
// THEN
|
||||
assertThat(result).hasSize(1)
|
||||
assertThat(result.first()).isInstanceOf(TokenDetailsNotification.DynamicAddressesFundsFound::class.java)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN FundsFound warning WHEN learn more button clicked THEN click intent is invoked`() {
|
||||
// GIVEN
|
||||
val notification = converter.convert(setOf(DynamicAddressesWarnings.FundsFound)).first()
|
||||
val button = notification.config.buttonsState as NotificationConfig.ButtonsState.SecondaryButtonConfig
|
||||
|
||||
// WHEN
|
||||
button.onClick()
|
||||
|
||||
// THEN
|
||||
verify(exactly = 1) { clickIntents.onDynamicAddressesFundsFoundLearnMoreClick() }
|
||||
}
|
||||
}
|
||||
|
|
@ -7,6 +7,7 @@ import com.tangem.core.ui.ds.message.TangemMessageEffect
|
|||
import com.tangem.core.ui.extensions.stringReference
|
||||
import com.tangem.domain.models.currency.CryptoCurrency
|
||||
import com.tangem.domain.tokens.model.warnings.CryptoCurrencyWarning
|
||||
import com.tangem.domain.tokens.model.warnings.DynamicAddressesWarnings
|
||||
import com.tangem.domain.tokens.model.warnings.HederaWarnings
|
||||
import com.tangem.domain.tokens.model.warnings.KaspaWarnings
|
||||
import com.tangem.feature.tokendetails.presentation.tokendetails.model.TokenDetailsClickIntents
|
||||
|
|
@ -449,6 +450,37 @@ class UpdateNotificationsTransformerTest {
|
|||
verify(exactly = 1) { clickIntents.onDismissIncompleteTransactionClick() }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN DynamicAddressesFundsFound WHEN transform THEN notification with learn more button is created`() {
|
||||
// GIVEN
|
||||
val transformer = createTransformer(
|
||||
warnings = setOf(DynamicAddressesWarnings.FundsFound),
|
||||
)
|
||||
|
||||
// WHEN
|
||||
val result = transformer.transform(initialState())
|
||||
|
||||
// THEN
|
||||
assertThat(result.notifications).hasSize(1)
|
||||
assertThat(result.notifications.first().id).isEqualTo("dynamic_addresses_funds_found")
|
||||
assertThat(result.notifications.first().buttonsUM).hasSize(1)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN DynamicAddressesFundsFound WHEN button clicked THEN onDynamicAddressesFundsFoundLearnMoreClick is called`() {
|
||||
// GIVEN
|
||||
val transformer = createTransformer(
|
||||
warnings = setOf(DynamicAddressesWarnings.FundsFound),
|
||||
)
|
||||
|
||||
// WHEN
|
||||
val result = transformer.transform(initialState())
|
||||
result.notifications.first().buttonsUM.first().onClick()
|
||||
|
||||
// THEN
|
||||
verify(exactly = 1) { clickIntents.onDynamicAddressesFundsFoundLearnMoreClick() }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN MigrationClore WHEN button clicked THEN onCloreMigrationClick is called`() {
|
||||
// GIVEN
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue