Updated on 2026-08-14

This commit is contained in:
Tangem 2026-04-20 15:44:02 +04:00
commit 5705579468
65 changed files with 759 additions and 294 deletions

@ -1 +1 @@
Subproject commit 1c95c2ee9aaa0007ea203b1bd9477e874f8211be
Subproject commit 498b0bcd0d871ed60c43b5d44f646548a4f11d37

View file

@ -6,7 +6,6 @@ import com.tangem.domain.account.status.supplier.SingleAccountStatusListSupplier
import com.tangem.domain.account.status.usecase.ArchiveCryptoPortfolioUseCase
import com.tangem.domain.account.status.usecase.RecoverCryptoPortfolioUseCase
import com.tangem.domain.account.status.utils.CryptoCurrencyBalanceFetcher
import com.tangem.domain.account.supplier.MultiAccountListSupplier
import com.tangem.domain.account.tokens.MainAccountTokensMigration
import com.tangem.domain.account.usecase.*
import com.tangem.feature.referral.data.ExternalReferralRepository
@ -91,14 +90,6 @@ internal object AccountDomainModule {
return GetUnoccupiedAccountIndexUseCase(crudRepository = accountsCRUDRepository)
}
@Provides
@Singleton
fun provideIsAccountsModeEnabledUseCase(
multiAccountListSupplier: MultiAccountListSupplier,
): IsAccountsModeEnabledUseCase {
return IsAccountsModeEnabledUseCase(multiAccountListSupplier = multiAccountListSupplier)
}
@Provides
@Singleton
fun provideApplyAccountListSortingUseCase(

View file

@ -25,7 +25,7 @@ object TangemBlogUrlBuilder {
}
data object SeedPhraseRiskySolution : Post {
override val path: String = "seed-phrase-a-risky-solution"
override val path: String = "seed-phrase-faq"
}
data object WhatWalletToChoose : Post {

View file

@ -56,6 +56,10 @@
"name": "SOLANA_SCALED_UI_AMOUNT_ENABLED",
"version": "undefined"
},
{
"name": "HEDERA_ERC20_ENABLED",
"version": "5.37"
},
{
"name": "ADD_AND_MANAGE_TOKENS_ENABLED",
"version": "undefined"

View file

@ -6,5 +6,13 @@ import com.squareup.moshi.JsonClass
@JsonClass(generateAdapter = true)
data class DismissPromoBannerRequest(
@Json(name = "walletId") val walletId: String,
@Json(name = "isDismissed") val isDismissed: Boolean,
)
@Json(name = "status") val status: BannerDisplayStatus,
) {
@JsonClass(generateAdapter = false)
enum class BannerDisplayStatus {
@Json(name = "active") ACTIVE,
@Json(name = "dismissed") DISMISSED,
}
}

View file

@ -109,7 +109,7 @@ internal class DefaultPaymentAccountStatusFetcher @Inject constructor(
private suspend fun fetchTangemPayAccountStatus(account: Account.Payment): PaymentAccountStatusValue {
val prevResult = paymentAccountStatusesStore.getSyncOrNull(account.userWalletId)
if (prevResult == null || prevResult.value is PaymentAccountStatusValue.Error) {
if (prevResult == null || prevResult.value is PaymentAccountStatusValue.Error.Unavailable) {
paymentAccountStatusesStore.store(
userWalletId = account.userWalletId,
status = AccountStatus.Payment(account = account, value = PaymentAccountStatusValue.Loading),

View file

@ -1,35 +0,0 @@
package com.tangem.domain.account.usecase
import com.tangem.domain.account.models.AccountList
import com.tangem.domain.account.supplier.MultiAccountListSupplier
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.map
/**
* Use case to determine if the accounts mode is enabled.
* Accounts mode is considered enabled if any [AccountList] produced for the user's wallets contains at least two
* active accounts.
*
* @property multiAccountListSupplier supplier that provides a list of [AccountList]s for all user wallets
*
[REDACTED_AUTHOR]
*/
class IsAccountsModeEnabledUseCase(
private val multiAccountListSupplier: MultiAccountListSupplier,
) {
operator fun invoke(): Flow<Boolean> {
return multiAccountListSupplier.invoke()
.map { accountsList -> accountsList.map(AccountList::activeAccounts).isModeEnabled() }
.distinctUntilChanged()
}
suspend fun invokeSync(): Boolean {
return multiAccountListSupplier.getSyncOrNull(Unit)
?.map(AccountList::activeAccounts)
?.isModeEnabled() == true
}
private fun List<Int>.isModeEnabled(): Boolean = any { it >= 2 }
}

View file

@ -1,159 +0,0 @@
package com.tangem.domain.account.usecase
import com.google.common.truth.Truth
import com.tangem.domain.account.models.AccountList
import com.tangem.domain.account.supplier.MultiAccountListSupplier
import io.mockk.clearMocks
import io.mockk.coEvery
import io.mockk.every
import io.mockk.mockk
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.Nested
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.TestInstance
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
class IsAccountsModeEnabledUseCaseTest {
private val multiAccountListSupplier: MultiAccountListSupplier = mockk()
private val useCase = IsAccountsModeEnabledUseCase(multiAccountListSupplier = multiAccountListSupplier)
@AfterEach
fun tearDown() {
clearMocks(multiAccountListSupplier)
}
@Nested
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
inner class Invoke {
@Test
fun `returns false when supplier emits empty list`() = runTest {
// Arrange
every { multiAccountListSupplier.invoke() } returns flowOf(emptyList())
// Act
val actual = useCase.invoke().first()
// Assert
Truth.assertThat(actual).isFalse()
}
@Test
fun `returns false when supplier emits account list with one account`() = runTest {
// Arrange
val accountList = createAccountList(activeAccounts = 1)
every { multiAccountListSupplier.invoke() } returns flowOf(listOf(accountList))
// Act
val actual = useCase.invoke().first()
// Assert
Truth.assertThat(actual).isFalse()
}
@Test
fun `returns true when supplier emits account list with two accounts`() = runTest {
// Arrange
val accountList = createAccountList(activeAccounts = 2)
every { multiAccountListSupplier.invoke() } returns flowOf(listOf(accountList))
// Act
val actual = useCase.invoke().first()
// Assert
Truth.assertThat(actual).isTrue()
}
@Test
fun `returns true when supplier emits multiple account lists, one with two accounts`() = runTest {
// Arrange
val accountList1 = createAccountList(activeAccounts = 1)
val accountList2 = createAccountList(activeAccounts = 2)
every { multiAccountListSupplier.invoke() } returns flowOf(listOf(accountList1, accountList2))
// Act
val actual = useCase.invoke().first()
// Assert
Truth.assertThat(actual).isTrue()
}
}
@Nested
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
inner class InvokeSync {
@Test
fun `returns false when getSyncOrNull returns null`() = runTest {
// Arrange
coEvery { multiAccountListSupplier.getSyncOrNull(Unit, any()) } returns null
// Act
val actual = useCase.invokeSync()
// Assert
Truth.assertThat(actual).isFalse()
}
@Test
fun `returns false when getSyncOrNull returns empty list`() = runTest {
// Arrange
coEvery { multiAccountListSupplier.getSyncOrNull(Unit, any()) } returns emptyList()
// Act
val actual = useCase.invokeSync()
// Assert
Truth.assertThat(actual).isFalse()
}
@Test
fun `returns false when getSyncOrNull returns account list with one account`() = runTest {
// Arrange
val accountList = createAccountList(activeAccounts = 1)
coEvery { multiAccountListSupplier.getSyncOrNull(Unit, any()) } returns listOf(accountList)
// Act
val actual = useCase.invokeSync()
// Assert
Truth.assertThat(actual).isFalse()
}
@Test
fun `returns true when getSyncOrNull returns account list with two accounts`() = runTest {
// Arrange
val accountList = createAccountList(activeAccounts = 2)
coEvery { multiAccountListSupplier.getSyncOrNull(Unit, any()) } returns listOf(accountList)
// Act
val actual = useCase.invokeSync()
// Assert
Truth.assertThat(actual).isTrue()
}
@Test
fun `returns true when getSyncOrNull returns multiple account lists, one with two accounts`() = runTest {
// Arrange
val accountList1 = createAccountList(activeAccounts = 1)
val accountList2 = createAccountList(activeAccounts = 2)
coEvery { multiAccountListSupplier.getSyncOrNull(Unit, any()) } returns listOf(accountList1, accountList2)
// Act
val actual = useCase.invokeSync()
// Assert
Truth.assertThat(actual).isTrue()
}
}
private fun createAccountList(activeAccounts: Int): AccountList = mockk {
every { this@mockk.activeAccounts } returns activeAccounts
}
}

View file

@ -48,6 +48,14 @@ internal object AccountStatusUseCaseModule {
)
}
@Provides
@Singleton
fun provideIsAccountsModeEnabledUseCase(
multiAccountStatusListSupplier: MultiAccountStatusListSupplier,
): IsAccountsModeEnabledUseCase {
return IsAccountsModeEnabledUseCase(multiAccountStatusListSupplier = multiAccountStatusListSupplier)
}
@Provides
@Singleton
fun provideGetCryptoCurrencyActionsUseCaseV2(

View file

@ -0,0 +1,50 @@
package com.tangem.domain.account.status.usecase
import com.tangem.domain.account.status.supplier.MultiAccountStatusListSupplier
import com.tangem.domain.models.account.AccountStatus
import com.tangem.domain.models.account.PaymentAccountStatusValue
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.map
/**
* Use case to determine if the accounts mode is enabled.
* Accounts mode is considered enabled if any [com.tangem.domain.account.models.AccountStatusList] produced for the
* user's wallets has more than one [AccountStatus.CryptoPortfolio], or has a [AccountStatus.Payment] with any
*
* @property multiAccountStatusListSupplier supplier that provides a list of
* [com.tangem.domain.account.models.AccountStatusList]s for all user wallets
*
[REDACTED_AUTHOR]
*/
class IsAccountsModeEnabledUseCase(
private val multiAccountStatusListSupplier: MultiAccountStatusListSupplier,
) {
operator fun invoke(): Flow<Boolean> {
return multiAccountStatusListSupplier.invoke()
.map { accountStatusLists -> accountStatusLists.any { it.accountStatuses.isModeEnabled() } }
.distinctUntilChanged()
}
suspend fun invokeSync(): Boolean {
return multiAccountStatusListSupplier.getSyncOrNull(Unit)
?.any { it.accountStatuses.isModeEnabled() } == true
}
private fun List<AccountStatus>.isModeEnabled(): Boolean {
var cryptoPortfolioCount = 0
for (status in this) {
when {
status is AccountStatus.CryptoPortfolio && ++cryptoPortfolioCount > 1 -> return true
status is AccountStatus.Payment && status.value.isActivePayment() -> return true
}
}
return false
}
private fun PaymentAccountStatusValue.isActivePayment(): Boolean {
return this !is PaymentAccountStatusValue.NotCreated
}
}

View file

@ -4,7 +4,7 @@ import com.tangem.domain.account.models.AccountExpandedState
import com.tangem.domain.account.models.AccountList
import com.tangem.domain.account.repository.AccountsExpandedRepository
import com.tangem.domain.account.supplier.SingleAccountListSupplier
import com.tangem.domain.account.usecase.IsAccountsModeEnabledUseCase
import com.tangem.domain.account.status.usecase.IsAccountsModeEnabledUseCase
import com.tangem.domain.models.account.AccountId
import com.tangem.domain.models.wallet.UserWallet
import com.tangem.domain.models.wallet.UserWalletId

View file

@ -0,0 +1,272 @@
package com.tangem.domain.account.status.usecase
import com.google.common.truth.Truth
import com.tangem.domain.account.models.AccountStatusList
import com.tangem.domain.account.status.supplier.MultiAccountStatusListSupplier
import com.tangem.domain.models.account.AccountStatus
import com.tangem.domain.models.account.PaymentAccountStatusValue
import io.mockk.clearMocks
import io.mockk.coEvery
import io.mockk.every
import io.mockk.mockk
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.Nested
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.TestInstance
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
class IsAccountsModeEnabledUseCaseTest {
private val multiAccountStatusListSupplier: MultiAccountStatusListSupplier = mockk()
private val useCase = IsAccountsModeEnabledUseCase(multiAccountStatusListSupplier = multiAccountStatusListSupplier)
@AfterEach
fun tearDown() {
clearMocks(multiAccountStatusListSupplier)
}
@Nested
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
inner class Invoke {
@Test
fun `returns false when supplier emits empty list`() = runTest {
every { multiAccountStatusListSupplier.invoke() } returns flowOf(emptyList())
val actual = useCase.invoke().first()
Truth.assertThat(actual).isFalse()
}
@Test
fun `returns false when single crypto portfolio account`() = runTest {
val statusList = createAccountStatusList(
statuses = listOf(mockCryptoPortfolio()),
)
every { multiAccountStatusListSupplier.invoke() } returns flowOf(listOf(statusList))
val actual = useCase.invoke().first()
Truth.assertThat(actual).isFalse()
}
@Test
fun `returns true when two crypto portfolio accounts`() = runTest {
val statusList = createAccountStatusList(
statuses = listOf(mockCryptoPortfolio(), mockCryptoPortfolio()),
)
every { multiAccountStatusListSupplier.invoke() } returns flowOf(listOf(statusList))
val actual = useCase.invoke().first()
Truth.assertThat(actual).isTrue()
}
@Test
fun `returns true when payment account is Loaded`() = runTest {
val statusList = createAccountStatusList(
statuses = listOf(mockCryptoPortfolio(), mockPayment(mockk<PaymentAccountStatusValue.Loaded>())),
)
every { multiAccountStatusListSupplier.invoke() } returns flowOf(listOf(statusList))
val actual = useCase.invoke().first()
Truth.assertThat(actual).isTrue()
}
@Test
fun `returns true when payment account is Locked`() = runTest {
val statusList = createAccountStatusList(
statuses = listOf(mockCryptoPortfolio(), mockPayment(mockk<PaymentAccountStatusValue.Locked>())),
)
every { multiAccountStatusListSupplier.invoke() } returns flowOf(listOf(statusList))
val actual = useCase.invoke().first()
Truth.assertThat(actual).isTrue()
}
@Test
fun `returns false when payment account is NotCreated`() = runTest {
val statusList = createAccountStatusList(
statuses = listOf(mockCryptoPortfolio(), mockPayment(PaymentAccountStatusValue.NotCreated)),
)
every { multiAccountStatusListSupplier.invoke() } returns flowOf(listOf(statusList))
val actual = useCase.invoke().first()
Truth.assertThat(actual).isFalse()
}
@Test
fun `returns true when payment account is UnderReview`() = runTest {
val statusList = createAccountStatusList(
statuses = listOf(mockCryptoPortfolio(), mockPayment(mockk<PaymentAccountStatusValue.UnderReview>())),
)
every { multiAccountStatusListSupplier.invoke() } returns flowOf(listOf(statusList))
val actual = useCase.invoke().first()
Truth.assertThat(actual).isTrue()
}
@Test
fun `returns true when payment account is IssuingCard`() = runTest {
val statusList = createAccountStatusList(
statuses = listOf(mockCryptoPortfolio(), mockPayment(mockk<PaymentAccountStatusValue.IssuingCard>())),
)
every { multiAccountStatusListSupplier.invoke() } returns flowOf(listOf(statusList))
val actual = useCase.invoke().first()
Truth.assertThat(actual).isTrue()
}
@Test
fun `returns true when payment account is Loading`() = runTest {
val statusList = createAccountStatusList(
statuses = listOf(mockCryptoPortfolio(), mockPayment(PaymentAccountStatusValue.Loading)),
)
every { multiAccountStatusListSupplier.invoke() } returns flowOf(listOf(statusList))
val actual = useCase.invoke().first()
Truth.assertThat(actual).isTrue()
}
@Test
fun `returns true when multiple status lists and one has two crypto portfolios`() = runTest {
val statusList1 = createAccountStatusList(statuses = listOf(mockCryptoPortfolio()))
val statusList2 = createAccountStatusList(
statuses = listOf(mockCryptoPortfolio(), mockCryptoPortfolio()),
)
every { multiAccountStatusListSupplier.invoke() } returns flowOf(listOf(statusList1, statusList2))
val actual = useCase.invoke().first()
Truth.assertThat(actual).isTrue()
}
}
@Nested
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
inner class InvokeSync {
@Test
fun `returns false when getSyncOrNull returns null`() = runTest {
coEvery { multiAccountStatusListSupplier.getSyncOrNull(Unit, any()) } returns null
val actual = useCase.invokeSync()
Truth.assertThat(actual).isFalse()
}
@Test
fun `returns false when getSyncOrNull returns empty list`() = runTest {
coEvery { multiAccountStatusListSupplier.getSyncOrNull(Unit, any()) } returns emptyList()
val actual = useCase.invokeSync()
Truth.assertThat(actual).isFalse()
}
@Test
fun `returns false when single crypto portfolio account`() = runTest {
val statusList = createAccountStatusList(
statuses = listOf(mockCryptoPortfolio()),
)
coEvery { multiAccountStatusListSupplier.getSyncOrNull(Unit, any()) } returns listOf(statusList)
val actual = useCase.invokeSync()
Truth.assertThat(actual).isFalse()
}
@Test
fun `returns true when two crypto portfolio accounts`() = runTest {
val statusList = createAccountStatusList(
statuses = listOf(mockCryptoPortfolio(), mockCryptoPortfolio()),
)
coEvery { multiAccountStatusListSupplier.getSyncOrNull(Unit, any()) } returns listOf(statusList)
val actual = useCase.invokeSync()
Truth.assertThat(actual).isTrue()
}
@Test
fun `returns true when payment account is Loaded`() = runTest {
val statusList = createAccountStatusList(
statuses = listOf(mockCryptoPortfolio(), mockPayment(mockk<PaymentAccountStatusValue.Loaded>())),
)
coEvery { multiAccountStatusListSupplier.getSyncOrNull(Unit, any()) } returns listOf(statusList)
val actual = useCase.invokeSync()
Truth.assertThat(actual).isTrue()
}
@Test
fun `returns true when payment account is Locked`() = runTest {
val statusList = createAccountStatusList(
statuses = listOf(mockCryptoPortfolio(), mockPayment(mockk<PaymentAccountStatusValue.Locked>())),
)
coEvery { multiAccountStatusListSupplier.getSyncOrNull(Unit, any()) } returns listOf(statusList)
val actual = useCase.invokeSync()
Truth.assertThat(actual).isTrue()
}
@Test
fun `returns false when payment account is NotCreated`() = runTest {
val statusList = createAccountStatusList(
statuses = listOf(mockCryptoPortfolio(), mockPayment(PaymentAccountStatusValue.NotCreated)),
)
coEvery { multiAccountStatusListSupplier.getSyncOrNull(Unit, any()) } returns listOf(statusList)
val actual = useCase.invokeSync()
Truth.assertThat(actual).isFalse()
}
@Test
fun `returns true when payment account is UnderReview`() = runTest {
val statusList = createAccountStatusList(
statuses = listOf(mockCryptoPortfolio(), mockPayment(mockk<PaymentAccountStatusValue.UnderReview>())),
)
coEvery { multiAccountStatusListSupplier.getSyncOrNull(Unit, any()) } returns listOf(statusList)
val actual = useCase.invokeSync()
Truth.assertThat(actual).isTrue()
}
@Test
fun `returns true when multiple status lists and one has loaded payment`() = runTest {
val statusList1 = createAccountStatusList(statuses = listOf(mockCryptoPortfolio()))
val statusList2 = createAccountStatusList(
statuses = listOf(mockCryptoPortfolio(), mockPayment(mockk<PaymentAccountStatusValue.Loaded>())),
)
coEvery { multiAccountStatusListSupplier.getSyncOrNull(Unit, any()) } returns listOf(statusList1, statusList2)
val actual = useCase.invokeSync()
Truth.assertThat(actual).isTrue()
}
}
private fun mockCryptoPortfolio(): AccountStatus.CryptoPortfolio = mockk()
private fun mockPayment(value: PaymentAccountStatusValue): AccountStatus.Payment = mockk {
every { this@mockk.value } returns value
}
private fun createAccountStatusList(statuses: List<AccountStatus>): AccountStatusList = mockk {
every { accountStatuses } returns statuses
}
}

View file

@ -32,9 +32,11 @@ class AssociateAssetUseCase(
val networkCoin = multiWalletCryptoCurrenciesSupplier.getSyncOrNull(
params = MultiWalletCryptoCurrenciesProducer.Params(userWallet.walletId),
)
?.firstOrNull {
?.firstOrNull { cryptoCurrency ->
val network = currency.network
it.network.id == network.id && it.network.derivationPath == network.derivationPath
cryptoCurrency is CryptoCurrency.Coin &&
cryptoCurrency.network.id == network.id &&
cryptoCurrency.network.derivationPath == network.derivationPath
}
?: error("Unable to create network coin for currencyID: ${currency.id}")

View file

@ -1,6 +1,6 @@
package com.tangem.features.account.selector
import com.tangem.domain.account.usecase.IsAccountsModeEnabledUseCase
import com.tangem.domain.account.status.usecase.IsAccountsModeEnabledUseCase
import com.tangem.domain.models.account.AccountId
import com.tangem.domain.models.account.AccountStatus
import com.tangem.domain.models.account.filterCryptoPortfolio

View file

@ -9,7 +9,7 @@ import com.tangem.core.decompose.model.ParamsContainer
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.extensions.stringReference
import com.tangem.domain.account.usecase.IsAccountsModeEnabledUseCase
import com.tangem.domain.account.status.usecase.IsAccountsModeEnabledUseCase
import com.tangem.domain.models.account.AccountId
import com.tangem.domain.models.account.AccountStatus
import com.tangem.domain.models.account.filterCryptoPortfolio

View file

@ -2,6 +2,7 @@ package com.tangem.features.feed.components.feed
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.State
import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier
@ -44,6 +45,7 @@ internal class DefaultFeedComponent(
context = child("promoBannersBlockComponent"),
params = PromoBannersBlockComponent.Params(
placeholder = PromoBannersBlockComponent.Placeholder.FEED,
isInitiallyVisibleOnScreen = false,
),
)
}
@ -70,6 +72,11 @@ internal class DefaultFeedComponent(
contentPadding: PaddingValues,
modifier: Modifier,
) {
val isExpanded = bottomSheetState.value == BottomSheetState.EXPANDED
LaunchedEffect(isExpanded) {
promoBannersBlockComponent?.setVisibleOnScreen(isExpanded)
}
LifecycleStartEffect(Unit) {
feedComponentModel.isVisibleOnScreen.value = true
onStopOrDispose {

View file

@ -12,7 +12,7 @@ import com.tangem.core.ui.extensions.stringReference
import com.tangem.domain.account.models.AccountStatusList
import com.tangem.domain.account.status.supplier.MultiAccountStatusListSupplier
import com.tangem.domain.account.status.usecase.GetCryptoCurrencyActionsUseCaseV2
import com.tangem.domain.account.usecase.IsAccountsModeEnabledUseCase
import com.tangem.domain.account.status.usecase.IsAccountsModeEnabledUseCase
import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase
import com.tangem.domain.appcurrency.model.AppCurrency
import com.tangem.domain.balancehiding.GetBalanceHidingSettingsUseCase

View file

@ -130,7 +130,10 @@ private fun FeedListContent(
feedListCallbacks = state.feedListCallbacks,
)
promoBannersBlockComponent?.Content(modifier = Modifier.padding(horizontal = 16.dp))
promoBannersBlockComponent?.Content(
modifier = Modifier.padding(top = 12.dp, start = 16.dp, end = 16.dp),
)
SpacerH(32.dp)
NewsBlock(
news = state.news,

View file

@ -78,8 +78,6 @@ internal fun MarketBlock(marketChart: MarketChartUM?, feedListCallbacks: FeedLis
modifier = Modifier.padding(horizontal = 16.dp),
marketChart = currentChart,
)
SpacerH(32.dp)
}
}
null -> Unit

View file

@ -29,7 +29,7 @@ dependencies {
implementation(projects.core.datasource)
/** Domain modules */
implementation(projects.domain.account)
implementation(projects.domain.account.status)
implementation(projects.domain.wallets)
implementation(projects.domain.appCurrency.models)
implementation(projects.domain.appCurrency)

View file

@ -7,7 +7,7 @@ import com.tangem.core.ui.components.containers.pullToRefresh.PullToRefreshConfi
import com.tangem.core.ui.components.fields.InputManager
import com.tangem.core.ui.components.fields.entity.SearchBarUM
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.domain.account.usecase.IsAccountsModeEnabledUseCase
import com.tangem.domain.account.status.usecase.IsAccountsModeEnabledUseCase
import com.tangem.domain.nft.FetchNFTCollectionAssetsUseCase
import com.tangem.domain.nft.GetNFTCollectionsUseCase
import com.tangem.domain.nft.RefreshAllNFTUseCase

View file

@ -17,7 +17,7 @@ import com.tangem.core.ui.extensions.wrappedList
import com.tangem.core.ui.message.DialogMessage
import com.tangem.domain.account.producer.SingleAccountProducer
import com.tangem.domain.account.supplier.SingleAccountSupplier
import com.tangem.domain.account.usecase.IsAccountsModeEnabledUseCase
import com.tangem.domain.account.status.usecase.IsAccountsModeEnabledUseCase
import com.tangem.domain.models.TokenReceiveConfig
import com.tangem.domain.models.account.AccountId
import com.tangem.domain.models.network.Network

View file

@ -13,7 +13,7 @@ import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.iconResId
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.extensions.stringReference
import com.tangem.domain.account.usecase.IsAccountsModeEnabledUseCase
import com.tangem.domain.account.status.usecase.IsAccountsModeEnabledUseCase
import com.tangem.domain.models.account.AccountStatus
import com.tangem.features.onramp.hottokens.portfolio.OnrampAddTokenComponent
import com.tangem.features.onramp.hottokens.portfolio.OnrampAddTokenComponent.AddHotCryptoData

View file

@ -17,7 +17,7 @@ import com.tangem.core.ui.extensions.resourceReference
import com.tangem.domain.account.status.producer.SingleAccountStatusListProducer
import com.tangem.domain.account.status.supplier.SingleAccountStatusListSupplier
import com.tangem.domain.account.status.usecase.GetAccountCurrencyStatusUseCase
import com.tangem.domain.account.usecase.IsAccountsModeEnabledUseCase
import com.tangem.domain.account.status.usecase.IsAccountsModeEnabledUseCase
import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase
import com.tangem.domain.appcurrency.model.AppCurrency
import com.tangem.domain.balancehiding.GetBalanceHidingSettingsUseCase

View file

@ -9,7 +9,7 @@ import com.tangem.core.decompose.model.ParamsContainer
import com.tangem.core.decompose.navigation.Router
import com.tangem.core.ui.components.token.state.TokenItemState
import com.tangem.domain.account.status.usecase.GetAccountCurrencyStatusUseCase
import com.tangem.domain.account.usecase.IsAccountsModeEnabledUseCase
import com.tangem.domain.account.status.usecase.IsAccountsModeEnabledUseCase
import com.tangem.domain.balancehiding.GetBalanceHidingSettingsUseCase
import com.tangem.domain.models.currency.CryptoCurrencyStatus
import com.tangem.features.onramp.component.SwapSelectTokensComponent

View file

@ -11,7 +11,7 @@ import com.tangem.core.ui.extensions.resourceReference
import com.tangem.domain.account.models.AccountStatusList
import com.tangem.domain.account.status.producer.SingleAccountStatusListProducer
import com.tangem.domain.account.status.supplier.SingleAccountStatusListSupplier
import com.tangem.domain.account.usecase.IsAccountsModeEnabledUseCase
import com.tangem.domain.account.status.usecase.IsAccountsModeEnabledUseCase
import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase
import com.tangem.domain.appcurrency.model.AppCurrency
import com.tangem.domain.balancehiding.GetBalanceHidingSettingsUseCase

View file

@ -5,8 +5,11 @@ import com.tangem.core.ui.decompose.ComposableContentComponent
interface PromoBannersBlockComponent : ComposableContentComponent {
fun setVisibleOnScreen(isVisible: Boolean)
data class Params(
val placeholder: Placeholder,
val isInitiallyVisibleOnScreen: Boolean = true,
)
enum class Placeholder {

View file

@ -20,6 +20,10 @@ internal class DefaultPromoBannersBlockComponent @AssistedInject constructor(
private val model: PromoBannersBlockModel = getOrCreateModel(params)
override fun setVisibleOnScreen(isVisible: Boolean) {
model.setVisibleOnScreen(isVisible)
}
@Composable
override fun Content(modifier: Modifier) {
val state by model.uiState.collectAsStateWithLifecycle()

View file

@ -36,6 +36,7 @@ internal class PromoBannersBlockModel @Inject constructor(
private val placeholder: String = params.placeholder.name.lowercase()
private val shownBannerIds: MutableSet<Int> = ConcurrentHashMap.newKeySet()
private var isVisibleOnScreen: Boolean = params.isInitiallyVisibleOnScreen
private var wasCarouselScrolled = false
private val savedDisplayIdByWalletId: MutableMap<String, Int> = mutableMapOf()
@ -46,6 +47,11 @@ internal class PromoBannersBlockModel @Inject constructor(
subscribeOnSelectedWallet()
}
fun setVisibleOnScreen(visible: Boolean) {
isVisibleOnScreen = visible
uiState.update { it.copy(isVisibleOnScreen = visible) }
}
private fun subscribeOnSelectedWallet() {
modelScope.launch {
userWalletsListRepository.selectedUserWallet
@ -53,7 +59,6 @@ internal class PromoBannersBlockModel @Inject constructor(
.map { it.walletId.stringValue }
.distinctUntilChanged()
.onEach {
shownBannerIds.clear()
wasCarouselScrolled = false
}
.collectLatest { walletId -> loadBanners(walletId) }
@ -85,6 +90,8 @@ internal class PromoBannersBlockModel @Inject constructor(
userWalletId = walletId,
initialPage = initialPage,
banners = bannerUMs,
isVisibleOnScreen = isVisibleOnScreen,
placeholder = params.placeholder,
onBannerShown = ::onBannerShown,
onCarouselScrolled = ::onCarouselScrolled,
onPageChanged = { displayId -> savedDisplayIdByWalletId[walletId] = displayId },
@ -116,6 +123,8 @@ internal class PromoBannersBlockModel @Inject constructor(
userWalletId = "",
initialPage = 0,
banners = persistentListOf(),
isVisibleOnScreen = isVisibleOnScreen,
placeholder = params.placeholder,
onBannerShown = {},
onCarouselScrolled = {},
onPageChanged = {},

View file

@ -1,11 +1,14 @@
package com.tangem.features.promobanners.impl.model
import com.tangem.features.promobanners.api.PromoBannersBlockComponent
import kotlinx.collections.immutable.ImmutableList
internal data class PromoBannersBlockUM(
val userWalletId: String,
val initialPage: Int,
val banners: ImmutableList<PromoBannerNotificationUM>,
val isVisibleOnScreen: Boolean,
val placeholder: PromoBannersBlockComponent.Placeholder,
val onBannerShown: (displayId: Int) -> Unit,
val onCarouselScrolled: (displayId: Int) -> Unit,
val onPageChanged: (displayId: Int) -> Unit,

View file

@ -66,7 +66,7 @@ internal class DefaultPromoBannersRepository(
withContext(dispatchers.io) {
val request = DismissPromoBannerRequest(
walletId = walletId,
isDismissed = true,
status = DismissPromoBannerRequest.BannerDisplayStatus.DISMISSED,
)
tangemTechApi.dismissPromoBannerDisplay(displayId, request).getOrThrow()
}

View file

@ -11,12 +11,14 @@ import androidx.compose.runtime.key
import androidx.compose.runtime.snapshotFlow
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.layout.SubcomposeLayout
import androidx.compose.ui.util.lerp
import com.tangem.core.ui.components.SpacerH8
import com.tangem.core.ui.components.notifications.Notification
import com.tangem.core.ui.components.pager.PagerIndicator
import com.tangem.core.ui.res.TangemTheme
import com.tangem.features.promobanners.api.PromoBannersBlockComponent.Placeholder
import com.tangem.features.promobanners.impl.model.PromoBannerNotificationUM
import com.tangem.features.promobanners.impl.model.PromoBannersBlockUM
import kotlin.math.ceil
@ -26,23 +28,25 @@ import kotlin.math.floor
internal fun PromoBannersBlock(state: PromoBannersBlockUM, modifier: Modifier = Modifier) {
if (state.banners.isEmpty()) return
val containerColor = bannerContainerColor(state.placeholder)
if (state.banners.size == 1) {
val banner = state.banners.first()
LaunchedEffect(banner.displayId) {
state.onBannerShown(banner.displayId)
LaunchedEffect(banner.displayId, state.isVisibleOnScreen) {
if (state.isVisibleOnScreen) {
state.onBannerShown(banner.displayId)
}
}
SingleBanner(
banner = banner,
containerColor = containerColor,
modifier = modifier,
)
} else {
key(state.userWalletId) {
BannersCarousel(
banners = state.banners,
initialPage = state.initialPage,
onBannerShown = state.onBannerShown,
onCarouselScroll = state.onCarouselScrolled,
onPageChange = state.onPageChanged,
state = state,
containerColor = containerColor,
modifier = modifier,
)
}
@ -50,40 +54,40 @@ internal fun PromoBannersBlock(state: PromoBannersBlockUM, modifier: Modifier =
}
@Composable
private fun SingleBanner(banner: PromoBannerNotificationUM, modifier: Modifier = Modifier) {
private fun bannerContainerColor(placeholder: Placeholder): Color = when (placeholder) {
Placeholder.MAIN -> TangemTheme.colors.background.primary
Placeholder.FEED -> TangemTheme.colors.background.action
}
@Composable
private fun SingleBanner(banner: PromoBannerNotificationUM, containerColor: Color, modifier: Modifier = Modifier) {
Notification(
config = banner.config,
modifier = modifier.fillMaxWidth(),
containerColor = TangemTheme.colors.background.primary,
containerColor = containerColor,
)
}
@Composable
private fun BannersCarousel(
banners: List<PromoBannerNotificationUM>,
initialPage: Int,
onBannerShown: (Int) -> Unit,
onCarouselScroll: (Int) -> Unit,
onPageChange: (Int) -> Unit,
modifier: Modifier = Modifier,
) {
private fun BannersCarousel(state: PromoBannersBlockUM, containerColor: Color, modifier: Modifier = Modifier) {
val pagerState = rememberPagerState(
initialPage = initialPage,
pageCount = { banners.size },
initialPage = state.initialPage,
pageCount = { state.banners.size },
)
LaunchedEffect(pagerState, banners) {
LaunchedEffect(pagerState, state.banners, state.isVisibleOnScreen) {
if (!state.isVisibleOnScreen) return@LaunchedEffect
var previousPage = pagerState.currentPage
snapshotFlow { pagerState.currentPage }
.collect { page ->
banners.getOrNull(page)?.let { banner ->
onPageChange(banner.displayId)
onBannerShown(banner.displayId)
if (page == 1) {
// one-time event when user scrolls from first to second page,
// indicating that he has interacted with the carousel
onCarouselScroll(banner.displayId)
state.banners.getOrNull(page)?.let { banner ->
state.onPageChanged(banner.displayId)
state.onBannerShown(banner.displayId)
if (previousPage == 0 && page == 1) {
state.onCarouselScrolled(banner.displayId)
}
}
previousPage = page
}
}
@ -92,8 +96,9 @@ private fun BannersCarousel(
horizontalAlignment = Alignment.CenterHorizontally,
) {
SmoothHeightPager(
banners = banners,
banners = state.banners,
pagerState = pagerState,
containerColor = containerColor,
modifier = Modifier.fillMaxWidth(),
)
@ -117,6 +122,7 @@ private fun BannersCarousel(
private fun SmoothHeightPager(
banners: List<PromoBannerNotificationUM>,
pagerState: PagerState,
containerColor: Color,
modifier: Modifier = Modifier,
) {
SubcomposeLayout(modifier = modifier) { constraints ->
@ -136,7 +142,7 @@ private fun SmoothHeightPager(
Notification(
config = banners[lowerPage].config,
modifier = Modifier.fillMaxWidth(),
containerColor = TangemTheme.colors.background.primary,
containerColor = containerColor,
)
}.first().measure(pageConstraints).height
@ -145,7 +151,7 @@ private fun SmoothHeightPager(
Notification(
config = banners[upperPage].config,
modifier = Modifier.fillMaxWidth(),
containerColor = TangemTheme.colors.background.primary,
containerColor = containerColor,
)
}.first().measure(pageConstraints).height
} else {
@ -164,7 +170,7 @@ private fun SmoothHeightPager(
Notification(
config = banner.config,
modifier = Modifier.fillMaxWidth(),
containerColor = TangemTheme.colors.background.primary,
containerColor = containerColor,
)
}
}.first().measure(

View file

@ -41,6 +41,7 @@ interface SendNotificationsComponent {
val fee: Fee?,
val feeError: GetFeeError?,
val feeCryptoCurrencyStatus: CryptoCurrencyStatus,
val isReduceAmountAvailable: Boolean = true,
)
}

View file

@ -19,7 +19,7 @@ import com.tangem.core.ui.utils.parseBigDecimal
import com.tangem.core.ui.utils.parseBigDecimalOrNull
import com.tangem.domain.account.status.usecase.GetAccountCurrencyStatusUseCase
import com.tangem.domain.account.status.usecase.GetFeePaidCryptoCurrencyStatusSyncUseCase
import com.tangem.domain.account.usecase.IsAccountsModeEnabledUseCase
import com.tangem.domain.account.status.usecase.IsAccountsModeEnabledUseCase
import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase
import com.tangem.domain.appcurrency.model.AppCurrency
import com.tangem.domain.balancehiding.GetBalanceHidingSettingsUseCase

View file

@ -16,7 +16,7 @@ import com.tangem.core.decompose.navigation.Router
import com.tangem.datasource.local.nft.converter.NFTSdkAssetConverter
import com.tangem.domain.account.status.usecase.GetAccountCurrencyStatusUseCase
import com.tangem.domain.account.status.usecase.GetFeePaidCryptoCurrencyStatusSyncUseCase
import com.tangem.domain.account.usecase.IsAccountsModeEnabledUseCase
import com.tangem.domain.account.status.usecase.IsAccountsModeEnabledUseCase
import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase
import com.tangem.domain.appcurrency.model.AppCurrency
import com.tangem.domain.feedback.GetWalletMetaInfoUseCase

View file

@ -12,7 +12,7 @@ import com.tangem.core.decompose.model.ParamsContainer
import com.tangem.core.decompose.navigation.Router
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.domain.account.status.supplier.MultiAccountStatusListSupplier
import com.tangem.domain.account.usecase.IsAccountsModeEnabledUseCase
import com.tangem.domain.account.status.usecase.IsAccountsModeEnabledUseCase
import com.tangem.domain.models.account.AccountStatus
import com.tangem.domain.models.account.PaymentAccountStatusValue
import com.tangem.domain.models.currency.CryptoCurrency
@ -80,6 +80,8 @@ internal class SendDestinationModel @Inject constructor(
private val cryptoCurrency = params.cryptoCurrency
private val userWalletId = params.userWalletId
// In "Send with swap" flow, these are addresses in the destination network (not the actual sender addresses).
// Self-send validation must be skipped for them, so use only with params.isAllowSelfSend.
private val senderAddresses = MutableStateFlow<List<CryptoCurrencyAddress>>(emptyList())
private val validationJobHolder = JobHolder()
@ -206,7 +208,7 @@ internal class SendDestinationModel @Inject constructor(
SendDestinationRecentListTransformer(
cryptoCurrency = cryptoCurrency,
senderAddress = senderAddresses.value.firstOrNull()?.address,
isSelfSendAvailable = isSelfSendAvailable,
isSelfSendAvailable = params.isAllowSelfSend || isSelfSendAvailable,
destinationWalletList = destinationWalletList,
txHistoryList = txHistoryList,
isAccountsMode = isAccountsMode,

View file

@ -108,6 +108,10 @@ internal class NotificationsModel @Inject constructor(
}
private suspend fun checkIfSubtractAvailable() {
if (!notificationData.isReduceAmountAvailable) {
isAmountSubtractAvailable = false
return
}
val feeCurrencyId = notificationData.feeCryptoCurrencyStatus.currency.id
val fee = notificationData.fee
isAmountSubtractAvailable = isAmountSubtractAvailableUseCase(

View file

@ -35,7 +35,7 @@ import com.tangem.core.ui.haptic.VibratorHapticManager
import com.tangem.core.ui.message.DialogMessage
import com.tangem.domain.account.status.usecase.GetAccountCurrencyStatusUseCase
import com.tangem.domain.account.status.usecase.GetFeePaidCryptoCurrencyStatusSyncUseCase
import com.tangem.domain.account.usecase.IsAccountsModeEnabledUseCase
import com.tangem.domain.account.status.usecase.IsAccountsModeEnabledUseCase
import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase
import com.tangem.domain.appcurrency.model.AppCurrency
import com.tangem.domain.balancehiding.GetBalanceHidingSettingsUseCase

View file

@ -14,7 +14,7 @@ import com.tangem.core.ui.haptic.VibratorHapticManager
import com.tangem.domain.account.status.model.AccountCryptoCurrencyStatus
import com.tangem.domain.account.status.usecase.GetAccountCurrencyStatusUseCase
import com.tangem.domain.account.status.usecase.GetFeePaidCryptoCurrencyStatusSyncUseCase
import com.tangem.domain.account.usecase.IsAccountsModeEnabledUseCase
import com.tangem.domain.account.status.usecase.IsAccountsModeEnabledUseCase
import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase
import com.tangem.domain.appcurrency.model.AppCurrency
import com.tangem.domain.balancehiding.GetBalanceHidingSettingsUseCase

View file

@ -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)
}

View file

@ -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}"
}
}
}

View file

@ -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
private var navigationJob: Job? = null
@ -149,10 +152,11 @@ internal class SwapAmountModel @Inject constructor(
}
fun onStart() {
val isDelayFirst = params !is SwapAmountComponentParams.AmountBlockParams
configAmountNavigation()
quoteTaskScheduler.scheduleTask(
scope = modelScope,
task = loadQuotesTask(),
task = loadQuotesTask(isDelayFirst = isDelayFirst),
)
subscribeOnAutoupdateEnabling()
}
@ -609,7 +613,7 @@ internal class SwapAmountModel @Inject constructor(
@Suppress("NullableToStringCall")
TangemLogger.e(
"""
Invalid cryptocurrencies status:
Invalid cryptocurrencies status:
| Primary -> $primaryStatus
| Secondary -> $secondaryStatus
""".trimIndent(),
@ -617,7 +621,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)
@ -626,9 +631,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
@ -779,6 +784,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()
}
}
@ -826,10 +835,10 @@ internal class SwapAmountModel @Inject constructor(
)
}
private fun loadQuotesTask(): PeriodicTask<Unit> {
private fun loadQuotesTask(isDelayFirst: Boolean = true): PeriodicTask<Unit> {
return PeriodicTask(
delay = QUOTES_UPDATE_DELAY,
isDelayFirst = true,
isDelayFirst = isDelayFirst,
task = {
runCatching { loadQuotes(isSilentReload = true) }
},

View file

@ -53,6 +53,8 @@ internal class SwapNotificationsComponent(
val fromCryptoCurrencyStatus: CryptoCurrencyStatus? = null,
val priceImpact: PriceImpact? = null,
val provider: ExpressProvider? = null,
val shouldIncludeFeeInBalanceCheck: Boolean = false,
val feeValue: BigDecimal? = null,
)
}
}

View file

@ -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
@ -26,6 +27,7 @@ import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.launch
import java.math.BigDecimal
import javax.inject.Inject
@Suppress("LongParameterList")
@ -43,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())
@ -109,6 +112,8 @@ internal class SwapNotificationsModel @Inject constructor(
)
}
}
sendErrorAnalyticsIfNeeded(notifications)
}
private suspend fun MutableList<NotificationUM>.addDestinationTagRequiredNotification() {
@ -132,7 +137,12 @@ internal class SwapNotificationsModel @Inject constructor(
private fun MutableList<NotificationUM>.addInsufficientFundsNotification() {
val enteredFromAmount = notificationData.enteredFromAmount ?: return
val balance = notificationData.fromCryptoCurrencyStatus?.value?.amount ?: return
if (enteredFromAmount > balance) {
val totalRequired = if (notificationData.shouldIncludeFeeInBalanceCheck) {
enteredFromAmount + (notificationData.feeValue ?: BigDecimal.ZERO)
} else {
enteredFromAmount
}
if (totalRequired > balance) {
add(SwapNotificationUM.Error.InsufficientFunds)
}
}
@ -183,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,
),
)
}
}
}

View file

@ -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"
}

View file

@ -5,7 +5,6 @@ import arrow.core.getOrElse
import arrow.core.left
import com.tangem.blockchain.common.transaction.Fee
import com.tangem.blockchain.common.transaction.TransactionFee
import com.tangem.common.routing.AppRouter
import com.tangem.common.ui.amountScreen.converters.AmountReduceByTransformer
import com.tangem.common.ui.amountScreen.models.AmountState
import com.tangem.common.ui.navigationButtons.NavigationButton
@ -22,6 +21,7 @@ import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.domain.account.status.usecase.GetAccountCurrencyByAddressUseCase
import com.tangem.domain.express.models.ExpressOperationType
import com.tangem.domain.express.models.ExpressRateType
import com.tangem.domain.express.models.ExpressProviderType
import com.tangem.domain.models.account.derivationIndex
import com.tangem.domain.models.currency.CryptoCurrency
@ -98,7 +98,6 @@ internal class SendWithSwapConfirmModel @Inject constructor(
private val swapAmountUpdateTrigger: SwapAmountUpdateTrigger,
private val feeSelectorReloadTrigger: FeeSelectorReloadTrigger,
private val swapAlertFactory: SwapAlertFactory,
private val appRouter: AppRouter,
private val analyticsEventHandler: AnalyticsEventHandler,
swapTransactionSenderFactory: SwapTransactionSender.Factory,
paramsContainer: ParamsContainer,
@ -339,7 +338,7 @@ internal class SendWithSwapConfirmModel @Inject constructor(
)
}
},
popBack = appRouter::pop,
popBack = {},
)
},
onSendError = { error ->
@ -362,7 +361,7 @@ internal class SendWithSwapConfirmModel @Inject constructor(
)
}
},
popBack = appRouter::pop,
popBack = {},
)
},
onSendSuccess = { txHash, timestamp, data ->
@ -430,6 +429,7 @@ internal class SendWithSwapConfirmModel @Inject constructor(
private fun updateConfirmNotifications() {
modelScope.launch {
val isFixedRate = amountUM?.swapRateType == ExpressRateType.Fixed
val feeCryptoCurrencyStatus =
feeUMV2?.feeExtraInfo?.feeCryptoCurrencyStatus ?: params.primaryFeePaidCurrencyStatusFlow.value
sendNotificationsUpdateTrigger.triggerUpdate(
@ -446,6 +446,7 @@ internal class SendWithSwapConfirmModel @Inject constructor(
fee = confirmData.fee,
feeError = confirmData.feeError,
feeCryptoCurrencyStatus = feeCryptoCurrencyStatus,
isReduceAmountAvailable = !isFixedRate,
),
)
swapNotificationsUpdateTrigger.triggerUpdate(
@ -460,6 +461,8 @@ internal class SendWithSwapConfirmModel @Inject constructor(
fromCryptoCurrencyStatus = confirmData.fromCryptoCurrencyStatus,
priceImpact = confirmData.priceImpact,
provider = confirmData.quote?.provider,
shouldIncludeFeeInBalanceCheck = isFixedRate && isAmountSubtractAvailable,
feeValue = confirmData.fee?.amount?.value,
),
)
uiState.transformerUpdate(

View file

@ -7,7 +7,7 @@ import com.tangem.core.decompose.model.Model
import com.tangem.core.decompose.model.ParamsContainer
import com.tangem.core.decompose.navigation.Router
import com.tangem.domain.account.status.usecase.GetAccountCurrencyStatusUseCase
import com.tangem.domain.account.usecase.IsAccountsModeEnabledUseCase
import com.tangem.domain.account.status.usecase.IsAccountsModeEnabledUseCase
import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase
import com.tangem.domain.appcurrency.model.AppCurrency
import com.tangem.domain.balancehiding.GetBalanceHidingSettingsUseCase

View file

@ -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()) }
}
}

View file

@ -1,6 +1,6 @@
package com.tangem.feature.swap.choosetoken.api
import com.tangem.domain.account.usecase.IsAccountsModeEnabledUseCase
import com.tangem.domain.account.status.usecase.IsAccountsModeEnabledUseCase
import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase
import com.tangem.domain.appcurrency.model.AppCurrency
import com.tangem.domain.balancehiding.GetBalanceHidingSettingsUseCase

View file

@ -36,7 +36,7 @@ import com.tangem.domain.account.status.supplier.SingleAccountStatusListSupplier
import com.tangem.domain.account.status.usecase.GetAccountCurrencyStatusUseCase
import com.tangem.domain.account.status.usecase.GetFeePaidCryptoCurrencyStatusSyncUseCase
import com.tangem.domain.account.status.utils.CryptoCurrencyStatusOperations.getCryptoCurrencyStatus
import com.tangem.domain.account.usecase.IsAccountsModeEnabledUseCase
import com.tangem.domain.account.status.usecase.IsAccountsModeEnabledUseCase
import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase
import com.tangem.domain.appcurrency.model.AppCurrency
import com.tangem.domain.balancehiding.GetBalanceHidingSettingsUseCase

View file

@ -446,6 +446,7 @@ internal class StateBuilder(
selectionType = ProviderState.SelectionType.CLICK,
onProviderClick = actions.onProviderClick,
needApplyFCARestrictions = needApplyFCARestrictions,
permissionState = quoteModel.permissionState,
),
priceImpact = priceImpact,
tosState = createTosState(swapProvider),
@ -1186,6 +1187,7 @@ internal class StateBuilder(
isNeedBestRateBadge: Boolean,
onProviderClick: (String) -> Unit,
needApplyFCARestrictions: Boolean,
permissionState: PermissionDataState,
): ProviderState {
val rate = toTokenInfo.tokenAmount.value.calculateRate(
fromTokenInfo.tokenAmount.value,
@ -1200,6 +1202,8 @@ internal class StateBuilder(
val additionalBadge = when {
needApplyFCARestrictions && isFCARestrictedProvider() -> ProviderState.AdditionalBadge.FCAWarningList
permissionState is PermissionDataState.PermissionReadyForRequest ->
ProviderState.AdditionalBadge.PermissionRequired
isRecommended -> ProviderState.AdditionalBadge.Recommended
isNeedBestRateBadge && isBestRate && !needApplyFCARestrictions -> ProviderState.AdditionalBadge.BestTrade
else -> ProviderState.AdditionalBadge.Empty

View file

@ -19,6 +19,7 @@ import com.tangem.domain.models.wallet.isLocked
import com.tangem.domain.models.wallet.isMultiCurrency
import com.tangem.domain.notifications.models.NotificationType
import com.tangem.domain.tokens.wallet.WalletBalanceFetcher
import com.tangem.domain.wallets.usecase.GetSelectedWalletSyncUseCase
import com.tangem.domain.wallets.usecase.GetUserWalletUseCase
import com.tangem.domain.wallets.usecase.SelectWalletUseCase
import com.tangem.features.pushnotifications.api.analytics.PushNotificationAnalyticEvents
@ -44,6 +45,7 @@ internal class DefaultTokenDetailsDeepLinkHandler @AssistedInject constructor(
private val walletDeepLinkActionTrigger: WalletDeepLinkActionTrigger,
private val analyticsEventHandler: AnalyticsEventHandler,
private val getUserWalletUseCase: GetUserWalletUseCase,
private val getSelectedWalletSyncUseCase: GetSelectedWalletSyncUseCase,
private val walletBalanceFetcher: WalletBalanceFetcher,
private val tangemPayFeatureToggles: TangemPayFeatureToggles,
private val singleAccountListSupplier: SingleAccountListSupplier,
@ -62,15 +64,23 @@ internal class DefaultTokenDetailsDeepLinkHandler @AssistedInject constructor(
scope.launch {
val userWalletId = walletId?.let(::UserWalletId)
val userWallet = userWalletId?.let { getUserWalletUseCase(userWalletId) }?.getOrNull()
val selectedUserWallet = getSelectedWalletSyncUseCase().getOrNull()
val userWallet = if (userWalletId != null) {
getUserWalletUseCase(userWalletId).getOrNull()
} else {
selectedUserWallet
}
// If wallet to select is null or locked, ignore deeplink
if (userWallet == null || userWallet.isLocked) {
TangemLogger.e("Error on getting user wallet")
return@launch
}
if (selectWalletUseCase(userWalletId).getOrNull() == null) {
TangemLogger.e("Error on selecting user wallet")
return@launch
if (userWalletId != null && selectedUserWallet?.walletId != userWalletId) {
val isSelectionFailed = selectWalletUseCase(userWalletId).getOrNull() == null
if (isSelectionFailed) {
TangemLogger.e("Error on selecting user wallet")
return@launch
}
}
val cryptoCurrency = findCryptoCurrency(userWallet = userWallet, networkId = networkId, tokenId = tokenId)

View file

@ -25,7 +25,6 @@ import com.tangem.core.analytics.models.event.OfframpAnalyticsEvent
import com.tangem.core.decompose.di.GlobalUiMessageSender
import com.tangem.common.ui.userwallet.converter.WalletIconUMConverter
import com.tangem.domain.account.supplier.SingleAccountListSupplier
import com.tangem.domain.account.usecase.IsAccountsModeEnabledUseCase
import com.tangem.domain.common.wallets.UserWalletsListRepository
import com.tangem.core.decompose.di.ModelScoped
import com.tangem.core.decompose.model.Model
@ -44,6 +43,7 @@ import com.tangem.core.ui.haptic.TangemHapticEffect
import com.tangem.core.ui.haptic.VibratorHapticManager
import com.tangem.core.ui.message.SnackbarMessage
import com.tangem.domain.account.status.usecase.GetAccountCurrencyStatusUseCase
import com.tangem.domain.account.status.usecase.IsAccountsModeEnabledUseCase
import com.tangem.domain.account.status.usecase.IsCryptoCurrencyCouldHideUseCase
import com.tangem.domain.account.status.usecase.ManageCryptoCurrenciesUseCase
import com.tangem.domain.account.status.utils.CryptoCurrencyBalanceFetcher

View file

@ -16,11 +16,13 @@ import com.tangem.domain.account.supplier.SingleAccountListSupplier
import com.tangem.domain.common.wallets.error.SelectWalletError
import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.domain.models.network.Network
import com.tangem.domain.models.wallet.UserWallet
import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.domain.models.wallet.isLocked
import com.tangem.domain.models.wallet.isMultiCurrency
import com.tangem.domain.tokens.wallet.WalletBalanceFetcher
import com.tangem.domain.wallets.models.GetUserWalletError
import com.tangem.domain.wallets.usecase.GetSelectedWalletSyncUseCase
import com.tangem.domain.wallets.usecase.GetUserWalletUseCase
import com.tangem.domain.wallets.usecase.SelectWalletUseCase
import com.tangem.features.tangempay.TangemPayFeatureToggles
@ -41,6 +43,7 @@ class DefaultTokenDetailsDeepLinkHandlerTest {
private val appRouter: AppRouter = mockk()
private val selectWalletUseCase: SelectWalletUseCase = mockk()
private val getSelectedWalletSync: GetSelectedWalletSyncUseCase = mockk()
private val cryptoCurrencyBalanceFetcher: CryptoCurrencyBalanceFetcher = mockk()
private val tokenDetailsDeepLinkActionTrigger: TokenDetailsDeepLinkActionTrigger = mockk()
private val walletDeepLinkActionTrigger: WalletDeepLinkActionTrigger = mockk()
@ -56,6 +59,11 @@ class DefaultTokenDetailsDeepLinkHandlerTest {
mockkObject(TangemLogger)
every { analyticsEventHandler.send(any()) } just Runs
every { appRouter.push(any(), any()) } just Runs
val userWallet: UserWallet = mockk()
every { userWallet.walletId } returns mockk()
every { getSelectedWalletSync() } returns Either.Right(
value = userWallet
)
}
@Test
@ -477,6 +485,7 @@ class DefaultTokenDetailsDeepLinkHandlerTest {
walletBalanceFetcher = walletBalanceFetcher,
tangemPayFeatureToggles = tangemPayFeatureToggles,
singleAccountListSupplier = singleAccountListSupplier,
getSelectedWalletSyncUseCase = getSelectedWalletSync,
)
}
}

View file

@ -25,7 +25,7 @@ import com.tangem.core.ui.message.EventMessageAction
import com.tangem.core.ui.message.SnackbarMessage
import com.tangem.core.ui.message.bottomSheetMessage
import com.tangem.domain.account.supplier.SingleAccountListSupplier
import com.tangem.domain.account.usecase.IsAccountsModeEnabledUseCase
import com.tangem.domain.account.status.usecase.IsAccountsModeEnabledUseCase
import com.tangem.domain.card.common.util.cardTypesResolver
import com.tangem.domain.demo.IsDemoCardUseCase
import com.tangem.domain.models.account.AccountId

View file

@ -17,7 +17,7 @@ import com.tangem.domain.account.status.supplier.SingleAccountStatusListSupplier
import com.tangem.domain.account.status.usecase.ApplyTokenListSortingUseCase
import com.tangem.domain.account.status.usecase.ToggleTokenListGroupingUseCase
import com.tangem.domain.account.status.usecase.ToggleTokenListSortingUseCase
import com.tangem.domain.account.usecase.IsAccountsModeEnabledUseCase
import com.tangem.domain.account.status.usecase.IsAccountsModeEnabledUseCase
import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase
import com.tangem.domain.appcurrency.model.AppCurrency
import com.tangem.domain.balancehiding.GetBalanceHidingSettingsUseCase

View file

@ -13,7 +13,7 @@ import com.tangem.domain.account.status.supplier.SingleAccountStatusListSupplier
import com.tangem.domain.account.status.usecase.ApplyTokenListSortingUseCase
import com.tangem.domain.account.status.usecase.ToggleTokenListGroupingUseCase
import com.tangem.domain.account.status.usecase.ToggleTokenListSortingUseCase
import com.tangem.domain.account.usecase.IsAccountsModeEnabledUseCase
import com.tangem.domain.account.status.usecase.IsAccountsModeEnabledUseCase
import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase
import com.tangem.domain.appcurrency.model.AppCurrency
import com.tangem.domain.balancehiding.GetBalanceHidingSettingsUseCase

View file

@ -15,7 +15,7 @@ import com.tangem.core.decompose.ui.UiMessageSender
import com.tangem.core.ui.utils.parseBigDecimal
import com.tangem.datasource.local.appsflyer.AppsFlyerStore
import com.tangem.domain.account.supplier.SingleAccountListSupplier
import com.tangem.domain.account.usecase.IsAccountsModeEnabledUseCase
import com.tangem.domain.account.status.usecase.IsAccountsModeEnabledUseCase
import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase
import com.tangem.domain.appcurrency.model.AppCurrency
import com.tangem.domain.apptheme.GetAppThemeModeUseCase

View file

@ -3,8 +3,8 @@ package com.tangem.feature.wallet.presentation.account
import com.tangem.core.decompose.di.ModelScoped
import com.tangem.domain.account.status.supplier.SingleAccountStatusListSupplier
import com.tangem.domain.account.status.supplier.SingleAccountStatusSupplier
import com.tangem.domain.account.status.usecase.IsAccountsModeEnabledUseCase
import com.tangem.domain.account.status.utils.MainExpandedAccountsHolder
import com.tangem.domain.account.usecase.IsAccountsModeEnabledUseCase
import javax.inject.Inject
@ModelScoped

View file

@ -19,7 +19,7 @@ import com.tangem.core.ui.message.EventMessageAction
import com.tangem.core.ui.message.SnackbarMessage
import com.tangem.core.ui.res.TangemTheme
import com.tangem.domain.account.supplier.MultiAccountListSupplier
import com.tangem.domain.account.usecase.IsAccountsModeEnabledUseCase
import com.tangem.domain.account.status.usecase.IsAccountsModeEnabledUseCase
import com.tangem.domain.qrscanning.models.QrResultSource
import com.tangem.domain.qrscanning.models.SourceType
import com.tangem.domain.qrscanning.usecases.ListenToQrScanningUseCase

View file

@ -21,7 +21,7 @@ import com.tangem.core.ui.extensions.stringReference
import com.tangem.core.ui.extensions.wrappedList
import com.tangem.core.ui.message.SnackbarMessage
import com.tangem.core.ui.message.ToastMessage
import com.tangem.domain.account.usecase.IsAccountsModeEnabledUseCase
import com.tangem.domain.account.status.usecase.IsAccountsModeEnabledUseCase
import com.tangem.domain.models.account.Account
import com.tangem.domain.models.account.AccountStatus
import com.tangem.domain.models.network.Network

View file

@ -5,7 +5,7 @@ import com.tangem.common.ui.account.CryptoPortfolioIconConverter
import com.tangem.common.ui.account.toUM
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.stringReference
import com.tangem.domain.account.usecase.IsAccountsModeEnabledUseCase
import com.tangem.domain.account.status.usecase.IsAccountsModeEnabledUseCase
import com.tangem.domain.models.account.Account
import com.tangem.domain.walletconnect.model.WcSession
import dagger.assisted.Assisted

View file

@ -5,7 +5,7 @@
# https://github.com/tangem/tangem-sdk-android/
# https://github.com/tangem/vico
tangemBlockchainSdk = "develop-1491"
tangemBlockchainSdk = "develop-1496"
#tangemBlockchainSdk = "0.0.1" # Keep it! - used for local builds
tangemCardSdk = "develop-602"
#tangemCardSdk = "0.0.1" # Keep it! - used for local builds ^

View file

@ -25,6 +25,7 @@ internal class WalletManagerFactoryCreator @Inject constructor(
private val blockchainSDKLogger: BlockchainSDKLogger,
private val isSolanaTxHistoryEnabled: Boolean,
private val isSolanaScaledUiAmountEnabled: Boolean,
private val isHederaErc20Enabled: Boolean,
) {
fun create(config: BlockchainSdkConfig, blockchainProviderTypes: BlockchainProviderTypes): WalletManagerFactory {
@ -39,6 +40,7 @@ internal class WalletManagerFactoryCreator @Inject constructor(
isPendingTransactionsEnabled = true,
isSolanaTxHistoryEnabled = isSolanaTxHistoryEnabled,
isSolanaScaledUiAmountEnabled = isSolanaScaledUiAmountEnabled,
isHederaErc20Enabled = isHederaErc20Enabled,
),
blockchainDataStorage = blockchainDataStorage,
loggers = listOf(blockchainSDKLogger),

View file

@ -103,6 +103,9 @@ internal object BlockchainSDKFactoryModule {
isSolanaScaledUiAmountEnabled = featureTogglesManager.isFeatureEnabled(
FeatureToggles.SOLANA_SCALED_UI_AMOUNT_ENABLED,
),
isHederaErc20Enabled = featureTogglesManager.isFeatureEnabled(
FeatureToggles.HEDERA_ERC20_ENABLED,
),
)
}
}