diff --git a/app/src/main/assets/tangem-app-config b/app/src/main/assets/tangem-app-config index 1c95c2ee9a..498b0bcd0d 160000 --- a/app/src/main/assets/tangem-app-config +++ b/app/src/main/assets/tangem-app-config @@ -1 +1 @@ -Subproject commit 1c95c2ee9aaa0007ea203b1bd9477e874f8211be +Subproject commit 498b0bcd0d871ed60c43b5d44f646548a4f11d37 diff --git a/app/src/main/java/com/tangem/tap/di/domain/AccountDomainModule.kt b/app/src/main/java/com/tangem/tap/di/domain/AccountDomainModule.kt index 34a46c0524..023bc7c89a 100644 --- a/app/src/main/java/com/tangem/tap/di/domain/AccountDomainModule.kt +++ b/app/src/main/java/com/tangem/tap/di/domain/AccountDomainModule.kt @@ -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( diff --git a/common/src/main/kotlin/com/tangem/common/TangemBlogUrlBuilder.kt b/common/src/main/kotlin/com/tangem/common/TangemBlogUrlBuilder.kt index 7f1d0b8d98..b62a7c78ce 100644 --- a/common/src/main/kotlin/com/tangem/common/TangemBlogUrlBuilder.kt +++ b/common/src/main/kotlin/com/tangem/common/TangemBlogUrlBuilder.kt @@ -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 { diff --git a/core/config-toggles/src/main/assets/configs/feature_toggles_config.json b/core/config-toggles/src/main/assets/configs/feature_toggles_config.json index 37b1b10533..d4422d4bf3 100644 --- a/core/config-toggles/src/main/assets/configs/feature_toggles_config.json +++ b/core/config-toggles/src/main/assets/configs/feature_toggles_config.json @@ -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" diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/models/promobanners/DismissPromoBannerRequest.kt b/core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/models/promobanners/DismissPromoBannerRequest.kt index 1bb3d5ac18..92cc200fc7 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/models/promobanners/DismissPromoBannerRequest.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/models/promobanners/DismissPromoBannerRequest.kt @@ -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, -) \ No newline at end of file + @Json(name = "status") val status: BannerDisplayStatus, +) { + + @JsonClass(generateAdapter = false) + enum class BannerDisplayStatus { + @Json(name = "active") ACTIVE, + + @Json(name = "dismissed") DISMISSED, + } +} \ No newline at end of file diff --git a/data/visa/src/main/kotlin/com/tangem/data/pay/flow/DefaultPaymentAccountStatusFetcher.kt b/data/visa/src/main/kotlin/com/tangem/data/pay/flow/DefaultPaymentAccountStatusFetcher.kt index 09f8f901ec..1cb938336b 100644 --- a/data/visa/src/main/kotlin/com/tangem/data/pay/flow/DefaultPaymentAccountStatusFetcher.kt +++ b/data/visa/src/main/kotlin/com/tangem/data/pay/flow/DefaultPaymentAccountStatusFetcher.kt @@ -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), diff --git a/domain/account/src/main/java/com/tangem/domain/account/usecase/IsAccountsModeEnabledUseCase.kt b/domain/account/src/main/java/com/tangem/domain/account/usecase/IsAccountsModeEnabledUseCase.kt deleted file mode 100644 index 735d837692..0000000000 --- a/domain/account/src/main/java/com/tangem/domain/account/usecase/IsAccountsModeEnabledUseCase.kt +++ /dev/null @@ -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 { - 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.isModeEnabled(): Boolean = any { it >= 2 } -} \ No newline at end of file diff --git a/domain/account/src/test/kotlin/com/tangem/domain/account/usecase/IsAccountsModeEnabledUseCaseTest.kt b/domain/account/src/test/kotlin/com/tangem/domain/account/usecase/IsAccountsModeEnabledUseCaseTest.kt deleted file mode 100644 index 71e7b32d89..0000000000 --- a/domain/account/src/test/kotlin/com/tangem/domain/account/usecase/IsAccountsModeEnabledUseCaseTest.kt +++ /dev/null @@ -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 - } -} \ No newline at end of file diff --git a/domain/account/status/src/main/java/com/tangem/domain/account/status/di/AccountStatusUseCaseModule.kt b/domain/account/status/src/main/java/com/tangem/domain/account/status/di/AccountStatusUseCaseModule.kt index befcbcbbb0..8318798036 100644 --- a/domain/account/status/src/main/java/com/tangem/domain/account/status/di/AccountStatusUseCaseModule.kt +++ b/domain/account/status/src/main/java/com/tangem/domain/account/status/di/AccountStatusUseCaseModule.kt @@ -48,6 +48,14 @@ internal object AccountStatusUseCaseModule { ) } + @Provides + @Singleton + fun provideIsAccountsModeEnabledUseCase( + multiAccountStatusListSupplier: MultiAccountStatusListSupplier, + ): IsAccountsModeEnabledUseCase { + return IsAccountsModeEnabledUseCase(multiAccountStatusListSupplier = multiAccountStatusListSupplier) + } + @Provides @Singleton fun provideGetCryptoCurrencyActionsUseCaseV2( diff --git a/domain/account/status/src/main/java/com/tangem/domain/account/status/usecase/IsAccountsModeEnabledUseCase.kt b/domain/account/status/src/main/java/com/tangem/domain/account/status/usecase/IsAccountsModeEnabledUseCase.kt new file mode 100644 index 0000000000..1ae6967739 --- /dev/null +++ b/domain/account/status/src/main/java/com/tangem/domain/account/status/usecase/IsAccountsModeEnabledUseCase.kt @@ -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 { + 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.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 + } +} \ No newline at end of file diff --git a/domain/account/status/src/main/java/com/tangem/domain/account/status/utils/ExpandedAccountsHolder.kt b/domain/account/status/src/main/java/com/tangem/domain/account/status/utils/ExpandedAccountsHolder.kt index f0bf98cccd..74f2ae778b 100644 --- a/domain/account/status/src/main/java/com/tangem/domain/account/status/utils/ExpandedAccountsHolder.kt +++ b/domain/account/status/src/main/java/com/tangem/domain/account/status/utils/ExpandedAccountsHolder.kt @@ -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 diff --git a/domain/account/status/src/test/kotlin/com/tangem/domain/account/status/usecase/IsAccountsModeEnabledUseCaseTest.kt b/domain/account/status/src/test/kotlin/com/tangem/domain/account/status/usecase/IsAccountsModeEnabledUseCaseTest.kt new file mode 100644 index 0000000000..49a4a0bd50 --- /dev/null +++ b/domain/account/status/src/test/kotlin/com/tangem/domain/account/status/usecase/IsAccountsModeEnabledUseCaseTest.kt @@ -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())), + ) + 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())), + ) + 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())), + ) + 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())), + ) + 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())), + ) + 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())), + ) + 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())), + ) + 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())), + ) + 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): AccountStatusList = mockk { + every { accountStatuses } returns statuses + } +} \ No newline at end of file diff --git a/domain/transaction/src/main/java/com/tangem/domain/transaction/usecase/AssociateAssetUseCase.kt b/domain/transaction/src/main/java/com/tangem/domain/transaction/usecase/AssociateAssetUseCase.kt index d20682fdc2..664a93a44f 100644 --- a/domain/transaction/src/main/java/com/tangem/domain/transaction/usecase/AssociateAssetUseCase.kt +++ b/domain/transaction/src/main/java/com/tangem/domain/transaction/usecase/AssociateAssetUseCase.kt @@ -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}") diff --git a/features/account/impl/src/main/java/com/tangem/features/account/selector/DefaultPortfolioSelectorController.kt b/features/account/impl/src/main/java/com/tangem/features/account/selector/DefaultPortfolioSelectorController.kt index c125505737..e2da9828c4 100644 --- a/features/account/impl/src/main/java/com/tangem/features/account/selector/DefaultPortfolioSelectorController.kt +++ b/features/account/impl/src/main/java/com/tangem/features/account/selector/DefaultPortfolioSelectorController.kt @@ -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 diff --git a/features/account/impl/src/main/java/com/tangem/features/account/selector/PortfolioSelectorModel.kt b/features/account/impl/src/main/java/com/tangem/features/account/selector/PortfolioSelectorModel.kt index b70b65e101..58b5fd61ca 100644 --- a/features/account/impl/src/main/java/com/tangem/features/account/selector/PortfolioSelectorModel.kt +++ b/features/account/impl/src/main/java/com/tangem/features/account/selector/PortfolioSelectorModel.kt @@ -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 diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/feed/DefaultFeedComponent.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/feed/DefaultFeedComponent.kt index 24bd9e7eb6..cb39402dc7 100644 --- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/feed/DefaultFeedComponent.kt +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/feed/DefaultFeedComponent.kt @@ -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 { diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/market/details/portfolio/impl/model/MarketsPortfolioDelegate.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/market/details/portfolio/impl/model/MarketsPortfolioDelegate.kt index 107fdc4acb..6c8cb07a7c 100644 --- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/market/details/portfolio/impl/model/MarketsPortfolioDelegate.kt +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/market/details/portfolio/impl/model/MarketsPortfolioDelegate.kt @@ -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 diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/feed/FeedList.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/feed/FeedList.kt index 1c459a5e09..e69bccddd0 100644 --- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/feed/FeedList.kt +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/feed/FeedList.kt @@ -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, diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/feed/components/MarketsBlock.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/feed/components/MarketsBlock.kt index 832e2e4ea0..daf0b624ae 100644 --- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/feed/components/MarketsBlock.kt +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/feed/components/MarketsBlock.kt @@ -78,8 +78,6 @@ internal fun MarketBlock(marketChart: MarketChartUM?, feedListCallbacks: FeedLis modifier = Modifier.padding(horizontal = 16.dp), marketChart = currentChart, ) - - SpacerH(32.dp) } } null -> Unit diff --git a/features/nft/impl/build.gradle.kts b/features/nft/impl/build.gradle.kts index 7db2bdc36d..be33a37a7f 100644 --- a/features/nft/impl/build.gradle.kts +++ b/features/nft/impl/build.gradle.kts @@ -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) diff --git a/features/nft/impl/src/main/kotlin/com/tangem/features/nft/collections/model/NFTCollectionsModel.kt b/features/nft/impl/src/main/kotlin/com/tangem/features/nft/collections/model/NFTCollectionsModel.kt index 13fc9173de..0172c69c29 100644 --- a/features/nft/impl/src/main/kotlin/com/tangem/features/nft/collections/model/NFTCollectionsModel.kt +++ b/features/nft/impl/src/main/kotlin/com/tangem/features/nft/collections/model/NFTCollectionsModel.kt @@ -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 diff --git a/features/nft/impl/src/main/kotlin/com/tangem/features/nft/receive/model/NFTReceiveModel.kt b/features/nft/impl/src/main/kotlin/com/tangem/features/nft/receive/model/NFTReceiveModel.kt index f96d50af08..6deefd8ea5 100644 --- a/features/nft/impl/src/main/kotlin/com/tangem/features/nft/receive/model/NFTReceiveModel.kt +++ b/features/nft/impl/src/main/kotlin/com/tangem/features/nft/receive/model/NFTReceiveModel.kt @@ -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 diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/hottokens/portfolio/entity/OnrampAddTokenUiBuilder.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/hottokens/portfolio/entity/OnrampAddTokenUiBuilder.kt index ac1f747c49..fe12663f1f 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/hottokens/portfolio/entity/OnrampAddTokenUiBuilder.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/hottokens/portfolio/entity/OnrampAddTokenUiBuilder.kt @@ -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 diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/swap/availablepairs/model/AvailableSwapPairsModel.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/swap/availablepairs/model/AvailableSwapPairsModel.kt index 05c0c3bddb..d87b8d3d0e 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/swap/availablepairs/model/AvailableSwapPairsModel.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/swap/availablepairs/model/AvailableSwapPairsModel.kt @@ -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 diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/swap/model/SwapSelectTokensModel.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/swap/model/SwapSelectTokensModel.kt index dcf23013a0..cf05eb89cc 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/swap/model/SwapSelectTokensModel.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/swap/model/SwapSelectTokensModel.kt @@ -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 diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/tokenlist/model/OnrampTokenListModel.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/tokenlist/model/OnrampTokenListModel.kt index 0b7676b1e6..884058c33b 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/tokenlist/model/OnrampTokenListModel.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/tokenlist/model/OnrampTokenListModel.kt @@ -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 diff --git a/features/promo-banners/api/src/main/kotlin/com/tangem/features/promobanners/api/PromoBannersBlockComponent.kt b/features/promo-banners/api/src/main/kotlin/com/tangem/features/promobanners/api/PromoBannersBlockComponent.kt index bb50b2bcd2..e97e39f047 100644 --- a/features/promo-banners/api/src/main/kotlin/com/tangem/features/promobanners/api/PromoBannersBlockComponent.kt +++ b/features/promo-banners/api/src/main/kotlin/com/tangem/features/promobanners/api/PromoBannersBlockComponent.kt @@ -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 { diff --git a/features/promo-banners/impl/src/main/kotlin/com/tangem/features/promobanners/impl/DefaultPromoBannersBlockComponent.kt b/features/promo-banners/impl/src/main/kotlin/com/tangem/features/promobanners/impl/DefaultPromoBannersBlockComponent.kt index 23ae1dce22..814031d197 100644 --- a/features/promo-banners/impl/src/main/kotlin/com/tangem/features/promobanners/impl/DefaultPromoBannersBlockComponent.kt +++ b/features/promo-banners/impl/src/main/kotlin/com/tangem/features/promobanners/impl/DefaultPromoBannersBlockComponent.kt @@ -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() diff --git a/features/promo-banners/impl/src/main/kotlin/com/tangem/features/promobanners/impl/model/PromoBannersBlockModel.kt b/features/promo-banners/impl/src/main/kotlin/com/tangem/features/promobanners/impl/model/PromoBannersBlockModel.kt index f4f2d505e5..d4a633984f 100644 --- a/features/promo-banners/impl/src/main/kotlin/com/tangem/features/promobanners/impl/model/PromoBannersBlockModel.kt +++ b/features/promo-banners/impl/src/main/kotlin/com/tangem/features/promobanners/impl/model/PromoBannersBlockModel.kt @@ -36,6 +36,7 @@ internal class PromoBannersBlockModel @Inject constructor( private val placeholder: String = params.placeholder.name.lowercase() private val shownBannerIds: MutableSet = ConcurrentHashMap.newKeySet() + private var isVisibleOnScreen: Boolean = params.isInitiallyVisibleOnScreen private var wasCarouselScrolled = false private val savedDisplayIdByWalletId: MutableMap = 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 = {}, diff --git a/features/promo-banners/impl/src/main/kotlin/com/tangem/features/promobanners/impl/model/PromoBannersBlockUM.kt b/features/promo-banners/impl/src/main/kotlin/com/tangem/features/promobanners/impl/model/PromoBannersBlockUM.kt index 0b12f8cf7c..e2adc41722 100644 --- a/features/promo-banners/impl/src/main/kotlin/com/tangem/features/promobanners/impl/model/PromoBannersBlockUM.kt +++ b/features/promo-banners/impl/src/main/kotlin/com/tangem/features/promobanners/impl/model/PromoBannersBlockUM.kt @@ -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, + val isVisibleOnScreen: Boolean, + val placeholder: PromoBannersBlockComponent.Placeholder, val onBannerShown: (displayId: Int) -> Unit, val onCarouselScrolled: (displayId: Int) -> Unit, val onPageChanged: (displayId: Int) -> Unit, diff --git a/features/promo-banners/impl/src/main/kotlin/com/tangem/features/promobanners/impl/repository/DefaultPromoBannersRepository.kt b/features/promo-banners/impl/src/main/kotlin/com/tangem/features/promobanners/impl/repository/DefaultPromoBannersRepository.kt index 9a13d9fab1..37c5cf33ef 100644 --- a/features/promo-banners/impl/src/main/kotlin/com/tangem/features/promobanners/impl/repository/DefaultPromoBannersRepository.kt +++ b/features/promo-banners/impl/src/main/kotlin/com/tangem/features/promobanners/impl/repository/DefaultPromoBannersRepository.kt @@ -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() } diff --git a/features/promo-banners/impl/src/main/kotlin/com/tangem/features/promobanners/impl/ui/PromoBannersBlock.kt b/features/promo-banners/impl/src/main/kotlin/com/tangem/features/promobanners/impl/ui/PromoBannersBlock.kt index 49cff50a0a..5cebe96145 100644 --- a/features/promo-banners/impl/src/main/kotlin/com/tangem/features/promobanners/impl/ui/PromoBannersBlock.kt +++ b/features/promo-banners/impl/src/main/kotlin/com/tangem/features/promobanners/impl/ui/PromoBannersBlock.kt @@ -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, - 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, 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( diff --git a/features/send-v2/api/src/main/java/com/tangem/features/send/v2/api/SendNotificationsComponent.kt b/features/send-v2/api/src/main/java/com/tangem/features/send/v2/api/SendNotificationsComponent.kt index b6c92acf3a..8e8192a6db 100644 --- a/features/send-v2/api/src/main/java/com/tangem/features/send/v2/api/SendNotificationsComponent.kt +++ b/features/send-v2/api/src/main/java/com/tangem/features/send/v2/api/SendNotificationsComponent.kt @@ -41,6 +41,7 @@ interface SendNotificationsComponent { val fee: Fee?, val feeError: GetFeeError?, val feeCryptoCurrencyStatus: CryptoCurrencyStatus, + val isReduceAmountAvailable: Boolean = true, ) } diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/model/SendModel.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/model/SendModel.kt index 3a0d2b40c8..a4be5674aa 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/model/SendModel.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/model/SendModel.kt @@ -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 diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/sendnft/model/NFTSendModel.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/sendnft/model/NFTSendModel.kt index 962b35e833..3d7216fc41 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/sendnft/model/NFTSendModel.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/sendnft/model/NFTSendModel.kt @@ -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 diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/destination/model/SendDestinationModel.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/destination/model/SendDestinationModel.kt index df6dc83590..2daa88480b 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/destination/model/SendDestinationModel.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/destination/model/SendDestinationModel.kt @@ -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>(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, diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/notifications/model/NotificationsModel.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/notifications/model/NotificationsModel.kt index 62dc80100b..5e0111ec67 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/notifications/model/NotificationsModel.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/notifications/model/NotificationsModel.kt @@ -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( diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/model/StakingModel.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/model/StakingModel.kt index 04f821f568..f3fbf4671e 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/model/StakingModel.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/model/StakingModel.kt @@ -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 diff --git a/features/staking/impl/src/test/kotlin/com/tangem/features/staking/impl/presentation/model/StakingModelTestBase.kt b/features/staking/impl/src/test/kotlin/com/tangem/features/staking/impl/presentation/model/StakingModelTestBase.kt index f44b1f3126..c0d02afd92 100644 --- a/features/staking/impl/src/test/kotlin/com/tangem/features/staking/impl/presentation/model/StakingModelTestBase.kt +++ b/features/staking/impl/src/test/kotlin/com/tangem/features/staking/impl/presentation/model/StakingModelTestBase.kt @@ -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 diff --git a/features/swap-v2/impl/build.gradle.kts b/features/swap-v2/impl/build.gradle.kts index 612a39dcc8..76f57cbad9 100644 --- a/features/swap-v2/impl/build.gradle.kts +++ b/features/swap-v2/impl/build.gradle.kts @@ -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) } \ No newline at end of file diff --git a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/analytics/SwapAmountAnalyticsSender.kt b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/analytics/SwapAmountAnalyticsSender.kt new file mode 100644 index 0000000000..d6b20adde2 --- /dev/null +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/analytics/SwapAmountAnalyticsSender.kt @@ -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, 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, 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}" + } + } +} \ No newline at end of file diff --git a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/SwapAmountModel.kt b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/SwapAmountModel.kt index d4a45ef016..9df178e74b 100644 --- a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/SwapAmountModel.kt +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/SwapAmountModel.kt @@ -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 = 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 { + private fun loadQuotesTask(isDelayFirst: Boolean = true): PeriodicTask { return PeriodicTask( delay = QUOTES_UPDATE_DELAY, - isDelayFirst = true, + isDelayFirst = isDelayFirst, task = { runCatching { loadQuotes(isSilentReload = true) } }, diff --git a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/notifications/SwapNotificationsComponent.kt b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/notifications/SwapNotificationsComponent.kt index 580b1c629a..f68c3e608d 100644 --- a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/notifications/SwapNotificationsComponent.kt +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/notifications/SwapNotificationsComponent.kt @@ -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, ) } } \ No newline at end of file diff --git a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/notifications/model/SwapNotificationsModel.kt b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/notifications/model/SwapNotificationsModel.kt index f69bf413d1..0031fd2efd 100644 --- a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/notifications/model/SwapNotificationsModel.kt +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/notifications/model/SwapNotificationsModel.kt @@ -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 = emptySet() val uiState: StateFlow> field = MutableStateFlow>(persistentListOf()) @@ -109,6 +112,8 @@ internal class SwapNotificationsModel @Inject constructor( ) } } + + sendErrorAnalyticsIfNeeded(notifications) } private suspend fun MutableList.addDestinationTagRequiredNotification() { @@ -132,7 +137,12 @@ internal class SwapNotificationsModel @Inject constructor( private fun MutableList.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) { + 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, + ), + ) + } + } } \ No newline at end of file diff --git a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/sendviaswap/analytics/SendWithSwapAnalyticsErrorMessages.kt b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/sendviaswap/analytics/SendWithSwapAnalyticsErrorMessages.kt new file mode 100644 index 0000000000..610a9c1159 --- /dev/null +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/sendviaswap/analytics/SendWithSwapAnalyticsErrorMessages.kt @@ -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" +} \ No newline at end of file diff --git a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/sendviaswap/confirm/model/SendWithSwapConfirmModel.kt b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/sendviaswap/confirm/model/SendWithSwapConfirmModel.kt index a73337555b..7b506a8eda 100644 --- a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/sendviaswap/confirm/model/SendWithSwapConfirmModel.kt +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/sendviaswap/confirm/model/SendWithSwapConfirmModel.kt @@ -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( diff --git a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/sendviaswap/model/SendWithSwapModel.kt b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/sendviaswap/model/SendWithSwapModel.kt index 2ebed7f554..0ec7b02ecd 100644 --- a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/sendviaswap/model/SendWithSwapModel.kt +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/sendviaswap/model/SendWithSwapModel.kt @@ -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 diff --git a/features/swap-v2/impl/src/test/java/com/tangem/features/swap/v2/impl/amount/analytics/SwapAmountAnalyticsSenderTest.kt b/features/swap-v2/impl/src/test/java/com/tangem/features/swap/v2/impl/amount/analytics/SwapAmountAnalyticsSenderTest.kt new file mode 100644 index 0000000000..26f07c6275 --- /dev/null +++ b/features/swap-v2/impl/src/test/java/com/tangem/features/swap/v2/impl/amount/analytics/SwapAmountAnalyticsSenderTest.kt @@ -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(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() + 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() + 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() + 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() + 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() + + 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() + + 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()) } + } +} \ No newline at end of file diff --git a/features/swap/impl/src/main/java/com/tangem/feature/swap/choosetoken/api/SettingContextUseCase.kt b/features/swap/impl/src/main/java/com/tangem/feature/swap/choosetoken/api/SettingContextUseCase.kt index a40a2a311c..9eee2c081b 100644 --- a/features/swap/impl/src/main/java/com/tangem/feature/swap/choosetoken/api/SettingContextUseCase.kt +++ b/features/swap/impl/src/main/java/com/tangem/feature/swap/choosetoken/api/SettingContextUseCase.kt @@ -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 diff --git a/features/swap/impl/src/main/java/com/tangem/feature/swap/model/SwapModel.kt b/features/swap/impl/src/main/java/com/tangem/feature/swap/model/SwapModel.kt index a369c9494d..074fe771f4 100644 --- a/features/swap/impl/src/main/java/com/tangem/feature/swap/model/SwapModel.kt +++ b/features/swap/impl/src/main/java/com/tangem/feature/swap/model/SwapModel.kt @@ -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 diff --git a/features/swap/impl/src/main/java/com/tangem/feature/swap/ui/StateBuilder.kt b/features/swap/impl/src/main/java/com/tangem/feature/swap/ui/StateBuilder.kt index 9fdab1593d..dfb24bc959 100644 --- a/features/swap/impl/src/main/java/com/tangem/feature/swap/ui/StateBuilder.kt +++ b/features/swap/impl/src/main/java/com/tangem/feature/swap/ui/StateBuilder.kt @@ -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 diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/deeplink/DefaultTokenDetailsDeepLinkHandler.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/deeplink/DefaultTokenDetailsDeepLinkHandler.kt index 5013ca11f5..d912665f4e 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/deeplink/DefaultTokenDetailsDeepLinkHandler.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/deeplink/DefaultTokenDetailsDeepLinkHandler.kt @@ -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) diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/model/TokenDetailsModel.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/model/TokenDetailsModel.kt index 11185ac5f1..bf086562db 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/model/TokenDetailsModel.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/model/TokenDetailsModel.kt @@ -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 diff --git a/features/tokendetails/impl/src/test/kotlin/com/tangem/feature/tokendetails/deeplink/DefaultTokenDetailsDeepLinkHandlerTest.kt b/features/tokendetails/impl/src/test/kotlin/com/tangem/feature/tokendetails/deeplink/DefaultTokenDetailsDeepLinkHandlerTest.kt index fd2e01e58b..4be98e40a0 100644 --- a/features/tokendetails/impl/src/test/kotlin/com/tangem/feature/tokendetails/deeplink/DefaultTokenDetailsDeepLinkHandlerTest.kt +++ b/features/tokendetails/impl/src/test/kotlin/com/tangem/feature/tokendetails/deeplink/DefaultTokenDetailsDeepLinkHandlerTest.kt @@ -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, ) } } \ No newline at end of file diff --git a/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/model/WalletSettingsModel.kt b/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/model/WalletSettingsModel.kt index f40b8d6490..2eb12d6343 100644 --- a/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/model/WalletSettingsModel.kt +++ b/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/model/WalletSettingsModel.kt @@ -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 diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/organizetokens/model/OrganizeTokensModel.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/organizetokens/model/OrganizeTokensModel.kt index 6d6f148c84..d2607e3957 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/organizetokens/model/OrganizeTokensModel.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/organizetokens/model/OrganizeTokensModel.kt @@ -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 diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/organizetokens/model/OrganizeTokensModelLegacy.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/organizetokens/model/OrganizeTokensModelLegacy.kt index 5a9359c88e..6e2ea1b370 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/organizetokens/model/OrganizeTokensModelLegacy.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/organizetokens/model/OrganizeTokensModelLegacy.kt @@ -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 diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/WalletModel.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/WalletModel.kt index 828ab47a4f..bb44739865 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/WalletModel.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/WalletModel.kt @@ -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 diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/account/AccountDependencies.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/account/AccountDependencies.kt index b252ecce55..bed6dd3ecd 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/account/AccountDependencies.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/account/AccountDependencies.kt @@ -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 diff --git a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/model/WcConnectionsModel.kt b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/model/WcConnectionsModel.kt index cdccfa2ec0..b4fb74e59e 100644 --- a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/model/WcConnectionsModel.kt +++ b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/model/WcConnectionsModel.kt @@ -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 diff --git a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/model/WcPairModel.kt b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/model/WcPairModel.kt index 36238a65fd..d51b17e8c5 100644 --- a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/model/WcPairModel.kt +++ b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/model/WcPairModel.kt @@ -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 diff --git a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/converter/WcPortfolioNameDelegate.kt b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/converter/WcPortfolioNameDelegate.kt index cad34dfe8e..4f12adbf92 100644 --- a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/converter/WcPortfolioNameDelegate.kt +++ b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/converter/WcPortfolioNameDelegate.kt @@ -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 diff --git a/gradle/tangem_dependencies.toml b/gradle/tangem_dependencies.toml index f1841ba8e2..91a743b450 100644 --- a/gradle/tangem_dependencies.toml +++ b/gradle/tangem_dependencies.toml @@ -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 ^ diff --git a/libs/blockchain-sdk/src/main/java/com/tangem/blockchainsdk/WalletManagerFactoryCreator.kt b/libs/blockchain-sdk/src/main/java/com/tangem/blockchainsdk/WalletManagerFactoryCreator.kt index 3de6c03bd1..2213a287ff 100644 --- a/libs/blockchain-sdk/src/main/java/com/tangem/blockchainsdk/WalletManagerFactoryCreator.kt +++ b/libs/blockchain-sdk/src/main/java/com/tangem/blockchainsdk/WalletManagerFactoryCreator.kt @@ -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), diff --git a/libs/blockchain-sdk/src/main/java/com/tangem/blockchainsdk/di/BlockchainSDKFactoryModule.kt b/libs/blockchain-sdk/src/main/java/com/tangem/blockchainsdk/di/BlockchainSDKFactoryModule.kt index 9bdbd36f05..cd52d01c17 100644 --- a/libs/blockchain-sdk/src/main/java/com/tangem/blockchainsdk/di/BlockchainSDKFactoryModule.kt +++ b/libs/blockchain-sdk/src/main/java/com/tangem/blockchainsdk/di/BlockchainSDKFactoryModule.kt @@ -103,6 +103,9 @@ internal object BlockchainSDKFactoryModule { isSolanaScaledUiAmountEnabled = featureTogglesManager.isFeatureEnabled( FeatureToggles.SOLANA_SCALED_UI_AMOUNT_ENABLED, ), + isHederaErc20Enabled = featureTogglesManager.isFeatureEnabled( + FeatureToggles.HEDERA_ERC20_ENABLED, + ), ) } } \ No newline at end of file