Updated on 2026-08-14

This commit is contained in:
Tangem 2026-03-03 10:39:48 +04:00
parent 665d56c9e8
commit 7d461390e0
19 changed files with 387 additions and 623 deletions

View file

@ -1,6 +1,5 @@
package com.tangem.tap.di.domain
import com.tangem.domain.account.featuretoggle.AccountsFeatureToggles
import com.tangem.domain.account.supplier.SingleAccountListSupplier
import com.tangem.domain.networks.single.SingleNetworkStatusSupplier
import com.tangem.domain.nft.*
@ -25,15 +24,11 @@ internal object NFTDomainModule {
@Provides
@Singleton
fun providesGetNFTCollectionsUseCase(
currenciesRepository: CurrenciesRepository,
nftRepository: NFTRepository,
singleAccountListSupplier: SingleAccountListSupplier,
accountsFeatureToggles: AccountsFeatureToggles,
): GetNFTCollectionsUseCase = GetNFTCollectionsUseCase(
currenciesRepository = currenciesRepository,
nftRepository = nftRepository,
singleAccountListSupplier = singleAccountListSupplier,
accountsFeatureToggles = accountsFeatureToggles,
)
@Provides

View file

@ -5,17 +5,14 @@ import arrow.core.left
import arrow.core.right
import com.tangem.data.common.wallet.WalletServerBinder
import com.tangem.data.wallets.converters.UserWalletRemoteInfoConverter
import com.tangem.datasource.api.common.AuthProvider
import com.tangem.datasource.api.common.response.ApiResponse
import com.tangem.datasource.api.common.response.ApiResponseError.HttpException
import com.tangem.datasource.api.common.response.fold
import com.tangem.datasource.api.common.response.getOrThrow
import com.tangem.datasource.api.common.response.isNetworkError
import com.tangem.datasource.api.tangemTech.TangemTechApi
import com.tangem.datasource.api.tangemTech.converters.WalletIdBodyConverter
import com.tangem.datasource.api.tangemTech.models.*
import com.tangem.datasource.api.tangemTech.models.SeedPhraseNotificationDTO.Status
import com.tangem.datasource.local.appsflyer.AppsFlyerStore
import com.tangem.datasource.local.datastore.RuntimeStateStore
import com.tangem.datasource.local.preferences.AppPreferencesStore
import com.tangem.datasource.local.preferences.PreferencesKeys
@ -24,7 +21,6 @@ import com.tangem.datasource.local.preferences.utils.getObjectMap
import com.tangem.datasource.local.preferences.utils.getSyncOrDefault
import com.tangem.datasource.local.preferences.utils.getSyncOrNull
import com.tangem.datasource.local.preferences.utils.store
import com.tangem.domain.account.featuretoggle.AccountsFeatureToggles
import com.tangem.domain.common.wallets.UserWalletsListRepository
import com.tangem.domain.common.wallets.getSyncOrNull
import com.tangem.domain.common.wallets.getSyncStrict
@ -52,10 +48,7 @@ internal class DefaultWalletsRepository(
private val userWalletsListRepository: UserWalletsListRepository,
private val seedPhraseNotificationVisibilityStore: RuntimeStateStore<SeedPhraseNotificationsStatuses>,
private val dispatchers: CoroutineDispatcherProvider,
private val authProvider: AuthProvider,
private val walletServerBinder: WalletServerBinder,
private val appsFlyerStore: AppsFlyerStore,
private val accountsFeatureToggles: AccountsFeatureToggles,
private val moshi: com.squareup.moshi.Moshi,
) : WalletsRepository {
@ -368,59 +361,36 @@ internal class DefaultWalletsRepository(
override suspend fun associateWallets(applicationId: String, wallets: List<UserWallet>) =
withContext(dispatchers.io) {
if (accountsFeatureToggles.isFeatureEnabled) {
val associateApplicationIdWithWallets: suspend () -> ApiResponse<Unit> = {
tangemTechApi.associateApplicationIdWithWalletsV2(
applicationId = applicationId,
body = AssociateApplicationIdWithWalletsBody(
walletIds = wallets.map { it.walletId.stringValue }.distinct(),
),
)
}
val apiResponse = associateApplicationIdWithWallets()
if (apiResponse is ApiResponse.Success) return@withContext
if (apiResponse is ApiResponse.Error &&
apiResponse.cause.isNetworkError(HttpException.Code.BAD_REQUEST)
) {
val errorBody = (apiResponse.cause as? HttpException)?.errorBody
?: error("Bad Request must have error body")
val adapter = moshi.adapter(AssociateAppWithWalletsErrorResponse::class.java)
val errorResponse = adapter.fromJson(errorBody)
?: error("Cannot parse error body: $errorBody")
errorResponse.missingWalletIds
.map {
async { createWallet(userWalletId = UserWalletId(it)) }
}
.awaitAll()
associateApplicationIdWithWallets().getOrThrow()
}
} else {
val conversionData = appsFlyerStore.get()
val publicKeys = authProvider.getCardsPublicKeys()
val walletsBody = wallets.map { userWallet ->
WalletIdBodyConverter.convert(
userWallet = userWallet,
conversionData = conversionData,
publicKeys = if (userWallet is UserWallet.Cold) {
publicKeys.filterKeys {
userWallet.cardsInWallet.contains(it)
}
} else {
emptyMap()
},
)
}
tangemTechApi.associateApplicationIdWithWallets(
val associateApplicationIdWithWallets: suspend () -> ApiResponse<Unit> = {
tangemTechApi.associateApplicationIdWithWalletsV2(
applicationId = applicationId,
body = walletsBody,
).getOrThrow()
body = AssociateApplicationIdWithWalletsBody(
walletIds = wallets.map { it.walletId.stringValue }.distinct(),
),
)
}
val apiResponse = associateApplicationIdWithWallets()
if (apiResponse is ApiResponse.Success) return@withContext
if (apiResponse is ApiResponse.Error &&
apiResponse.cause.isNetworkError(HttpException.Code.BAD_REQUEST)
) {
val errorBody = (apiResponse.cause as? HttpException)?.errorBody
?: error("Bad Request must have error body")
val adapter = moshi.adapter(AssociateAppWithWalletsErrorResponse::class.java)
val errorResponse = adapter.fromJson(errorBody)
?: error("Cannot parse error body: $errorBody")
errorResponse.missingWalletIds
.map {
async { createWallet(userWalletId = UserWalletId(it)) }
}
.awaitAll()
associateApplicationIdWithWallets().getOrThrow()
}
}

View file

@ -9,13 +9,11 @@ import com.tangem.data.wallets.cold.DefaultColdMapDerivationsRepository
import com.tangem.data.wallets.derivations.DefaultDerivationsRepository
import com.tangem.data.wallets.hot.DefaultHotMapDerivationsRepository
import com.tangem.data.wallets.hot.DefaultHotWalletAccessCodeAttemptsRepository
import com.tangem.datasource.api.common.AuthProvider
import com.tangem.datasource.api.tangemTech.TangemTechApi
import com.tangem.datasource.di.NetworkMoshi
import com.tangem.datasource.local.appsflyer.AppsFlyerStore
import com.tangem.datasource.local.datastore.RuntimeStateStore
import com.tangem.datasource.local.preferences.AppPreferencesStore
import com.tangem.domain.account.featuretoggle.AccountsFeatureToggles
import com.tangem.domain.common.wallets.UserWalletsListRepository
import com.tangem.domain.wallets.derivations.ColdMapDerivationsRepository
import com.tangem.domain.wallets.derivations.DerivationsRepository
@ -43,10 +41,7 @@ internal object WalletsDataModule {
tangemTechApi: TangemTechApi,
userWalletsListRepository: UserWalletsListRepository,
dispatchers: CoroutineDispatcherProvider,
authProvider: AuthProvider,
walletServerBinder: WalletServerBinder,
appsFlyerStore: AppsFlyerStore,
accountsFeatureToggles: AccountsFeatureToggles,
@NetworkMoshi moshi: Moshi,
): WalletsRepository {
return DefaultWalletsRepository(
@ -55,10 +50,7 @@ internal object WalletsDataModule {
userWalletsListRepository = userWalletsListRepository,
seedPhraseNotificationVisibilityStore = RuntimeStateStore(defaultValue = emptyMap()),
dispatchers = dispatchers,
authProvider = authProvider,
walletServerBinder = walletServerBinder,
appsFlyerStore = appsFlyerStore,
accountsFeatureToggles = accountsFeatureToggles,
moshi = moshi,
)
}

View file

@ -5,149 +5,149 @@ import androidx.datastore.preferences.core.Preferences
import com.google.common.truth.Truth.assertThat
import com.squareup.moshi.Moshi
import com.tangem.data.common.wallet.WalletServerBinder
import com.tangem.datasource.api.common.AuthProvider
import com.tangem.datasource.api.common.response.ApiResponse
import com.tangem.datasource.api.common.response.ApiResponseError.HttpException
import com.tangem.datasource.api.tangemTech.TangemTechApi
import com.tangem.datasource.api.tangemTech.models.PromocodeActivationBody
import com.tangem.datasource.api.tangemTech.models.PromocodeActivationResponse
import com.tangem.datasource.api.tangemTech.models.WalletResponse
import com.tangem.datasource.local.appsflyer.AppsFlyerStore
import com.tangem.datasource.local.preferences.AppPreferencesStore
import com.tangem.datasource.local.preferences.PreferencesKeys
import com.tangem.domain.account.featuretoggle.AccountsFeatureToggles
import com.tangem.domain.models.wallet.UserWallet
import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.domain.wallets.models.errors.ActivatePromoCodeError
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import com.tangem.utils.coroutines.TestingCoroutineDispatcherProvider
import io.mockk.coEvery
import io.mockk.coVerify
import io.mockk.every
import io.mockk.mockk
import io.mockk.*
import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.test.runTest
import org.junit.Before
import org.junit.Test
import org.junit.jupiter.api.AfterEach
import org.junit.jupiter.api.Nested
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.TestInstance
/**
* Tests for [DefaultWalletsRepository]
*/
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
class DefaultWalletsRepositoryTest {
private lateinit var repository: DefaultWalletsRepository
private val preferencesDataStore = mockk<DataStore<Preferences>>(relaxed = true)
private val preferencesDataStore: DataStore<Preferences> = mockk(relaxed = true)
private val tangemTechApi: TangemTechApi = mockk()
private val walletServerBinder: WalletServerBinder = mockk()
private val appPreferenceStore = AppPreferencesStore(
moshi = Moshi.Builder().build(),
dispatchers = TestingCoroutineDispatcherProvider(),
preferencesDataStore = preferencesDataStore,
)
private lateinit var tangemTechApi: TangemTechApi
private lateinit var dispatchers: CoroutineDispatcherProvider
private lateinit var walletServerBinder: WalletServerBinder
private lateinit var appsFlyerStore: AppsFlyerStore
private val repository = DefaultWalletsRepository(
appPreferencesStore = appPreferenceStore,
tangemTechApi = tangemTechApi,
userWalletsListRepository = mockk(),
seedPhraseNotificationVisibilityStore = mockk(),
dispatchers = TestingCoroutineDispatcherProvider(),
walletServerBinder = walletServerBinder,
moshi = mockk(),
)
private val testWalletId = UserWalletId("1234567890abcdef")
@Before
fun setup() {
tangemTechApi = mockk()
walletServerBinder = mockk()
appsFlyerStore = mockk()
dispatchers = TestingCoroutineDispatcherProvider()
repository = DefaultWalletsRepository(
appPreferencesStore = appPreferenceStore,
tangemTechApi = tangemTechApi,
userWalletsListRepository = mockk(),
seedPhraseNotificationVisibilityStore = mockk(),
dispatchers = dispatchers,
authProvider = mockk(),
walletServerBinder = walletServerBinder,
appsFlyerStore = appsFlyerStore,
accountsFeatureToggles = mockk(),
moshi = mockk(),
)
@AfterEach
fun tearDown() {
clearMocks(tangemTechApi, preferencesDataStore)
}
@Test
fun `GIVEN local storage has value WHEN isNotificationsEnabled THEN should return local value`() = runTest {
// GIVEN
val expectedPreferences = """{"${testWalletId.stringValue}":true}"""
val preferences = mockk<Preferences>()
coEvery { preferencesDataStore.data } returns flowOf(preferences)
coEvery { preferences[PreferencesKeys.NOTIFICATIONS_ENABLED_STATES_KEY] } returns expectedPreferences
@Nested
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
inner class IsNotificationsEnabled {
// WHEN
val result = repository.isNotificationsEnabled(testWalletId)
@Test
fun `should return local value when local storage has value`() = runTest {
// Arrange
val expectedPreferences = """{"${testWalletId.stringValue}":true}"""
val preferences = mockk<Preferences>()
coEvery { preferencesDataStore.data } returns flowOf(preferences)
coEvery { preferences[PreferencesKeys.NOTIFICATIONS_ENABLED_STATES_KEY] } returns expectedPreferences
// THEN
assertThat(result).isTrue()
// Act
val result = repository.isNotificationsEnabled(testWalletId)
// Assert
assertThat(result).isTrue()
}
@Test
fun `should return false when local storage is empty`() = runTest {
// Arrange
val preferences = mockk<Preferences>()
coEvery { preferencesDataStore.data } returns flowOf(preferences)
coEvery { preferences[PreferencesKeys.NOTIFICATIONS_ENABLED_STATES_KEY] } returns "{}"
// Act
val result = repository.isNotificationsEnabled(testWalletId)
// Assert
assertThat(result).isFalse()
}
}
@Test
fun `GIVEN local storage is empty WHEN isNotificationsEnabled THEN should return false`() = runTest {
// GIVEN
val preferences = mockk<Preferences>()
coEvery { preferencesDataStore.data } returns flowOf(preferences)
coEvery { preferences[PreferencesKeys.NOTIFICATIONS_ENABLED_STATES_KEY] } returns "{}"
@Nested
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
inner class SetNotificationsEnabled {
// WHEN
val result = repository.isNotificationsEnabled(testWalletId)
@Test
fun `should update local storage when enabled status`() = runTest {
// Arrange
val preferences = mockk<Preferences>()
coEvery { preferencesDataStore.data } returns flowOf(preferences)
coEvery { preferences[PreferencesKeys.NOTIFICATIONS_ENABLED_STATES_KEY] } returns "{}"
coEvery { preferencesDataStore.updateData(any()) } returns mockk()
// THEN
assertThat(result).isFalse()
// Act
repository.setNotificationsEnabled(testWalletId, isEnabled = true)
// Assert
coVerify(exactly = 1) { preferencesDataStore.updateData(any()) }
}
@Test
fun `should update local storage when disabled status`() = runTest {
// Arrange
val preferences = mockk<Preferences>()
coEvery { preferencesDataStore.data } returns flowOf(preferences)
coEvery { preferences[PreferencesKeys.NOTIFICATIONS_ENABLED_STATES_KEY] } returns "{}"
coEvery { preferencesDataStore.updateData(any()) } returns mockk()
// Act
repository.setNotificationsEnabled(testWalletId, isEnabled = false)
// Assert
coVerify(exactly = 1) { preferencesDataStore.updateData(any()) }
}
}
@Test
fun `GIVEN enabled status WHEN setNotificationsEnabled THEN should update local storage`() = runTest {
// GIVEN
val preferences = mockk<Preferences>()
coEvery { preferencesDataStore.data } returns flowOf(preferences)
coEvery { preferences[PreferencesKeys.NOTIFICATIONS_ENABLED_STATES_KEY] } returns "{}"
coEvery { preferencesDataStore.updateData(any()) } returns mockk()
@Nested
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
inner class GetWalletsInfo {
// WHEN
repository.setNotificationsEnabled(testWalletId, isEnabled = true)
// THEN
coVerify(exactly = 1) { preferencesDataStore.updateData(any()) }
}
@Test
fun `GIVEN disabled status WHEN setNotificationsEnabled THEN should update local storage`() = runTest {
// GIVEN
val preferences = mockk<Preferences>()
coEvery { preferencesDataStore.data } returns flowOf(preferences)
coEvery { preferences[PreferencesKeys.NOTIFICATIONS_ENABLED_STATES_KEY] } returns "{}"
coEvery { preferencesDataStore.updateData(any()) } returns mockk()
// WHEN
repository.setNotificationsEnabled(testWalletId, isEnabled = false)
// THEN
coVerify(exactly = 1) { preferencesDataStore.updateData(any()) }
}
@Test
fun `GIVEN API returns wallets WHEN getWalletsInfo THEN should return converted wallets and update cache if requested`() =
runTest {
// GIVEN
@Test
fun `should return converted wallets and update cache when updateCache is true`() = runTest {
// Arrange
val applicationId = "test_app_id"
val wallet1Id = "1234567890abcdef"
val wallet2Id = "fedcba0987654321"
val walletResponses = listOf(
WalletResponse(
id = wallet1Id,
notifyStatus = true,
),
WalletResponse(
id = wallet2Id,
notifyStatus = false,
),
WalletResponse(id = wallet1Id, notifyStatus = true),
WalletResponse(id = wallet2Id, notifyStatus = false),
)
coEvery { tangemTechApi.getWallets(applicationId) } returns ApiResponse.Success(walletResponses)
coEvery { preferencesDataStore.updateData(any()) } returns mockk()
// WHEN
// Act
val result = repository.getWalletsInfo(applicationId, updateCache = true)
// THEN
// Assert
assertThat(result).hasSize(2)
assertThat(result[0].walletId.stringValue).isEqualTo(wallet1Id)
assertThat(result[0].isNotificationsEnabled).isTrue()
@ -158,24 +158,20 @@ class DefaultWalletsRepositoryTest {
coVerify(exactly = 2) { preferencesDataStore.updateData(any()) }
}
@Test
fun `GIVEN API returns wallets WHEN getWalletsInfo with updateCache false THEN should return converted wallets without updating cache`() =
runTest {
// GIVEN
@Test
fun `should return converted wallets without updating cache when updateCache is false`() = runTest {
// Arrange
val applicationId = "test_app_id"
val wallet1Id = "1234567890abcdef"
val walletResponses = listOf(
WalletResponse(
id = wallet1Id,
notifyStatus = true,
),
WalletResponse(id = wallet1Id, notifyStatus = true),
)
coEvery { tangemTechApi.getWallets(applicationId) } returns ApiResponse.Success(walletResponses)
// WHEN
// Act
val result = repository.getWalletsInfo(applicationId, updateCache = false)
// THEN
// Assert
assertThat(result).hasSize(1)
assertThat(result[0].walletId.stringValue).isEqualTo(wallet1Id)
assertThat(result[0].isNotificationsEnabled).isTrue()
@ -183,152 +179,126 @@ class DefaultWalletsRepositoryTest {
coVerify(exactly = 1) { tangemTechApi.getWallets(applicationId) }
coVerify(exactly = 0) { preferencesDataStore.updateData(any()) }
}
}
@Test
fun `GIVEN user wallets and application ID WHEN associateWallets THEN should convert and send to API`() = runTest {
// GIVEN
val applicationId = "test_app_id"
val wallet1Id = "1234567890abcdef"
val wallet2Id = "fedcba0987654321"
val card1PublicKey = "card1_public_key"
val card2PublicKey = "card2_public_key"
@Nested
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
inner class AssociateWallets {
val userWallets = listOf(
mockk<UserWallet.Cold> {
every { cardsInWallet } returns setOf(card1PublicKey)
every { walletId } returns UserWalletId(wallet1Id)
every { name } returns "Wallet 1"
},
mockk<UserWallet.Cold> {
every { cardsInWallet } returns setOf(card2PublicKey)
every { walletId } returns UserWalletId(wallet2Id)
every { name } returns "Wallet 2"
},
)
@Test
fun `should convert and send to API V2`() = runTest {
// Arrange
val applicationId = "test_app_id"
val wallet1Id = "1234567890abcdef"
val wallet2Id = "fedcba0987654321"
val publicKeys = mapOf(
card1PublicKey to "public_key_1",
card2PublicKey to "public_key_2",
)
val authProvider = mockk<AuthProvider> {
coEvery { getCardsPublicKeys() } returns publicKeys
}
val accountsFeatureToggles = mockk<AccountsFeatureToggles> {
every { isFeatureEnabled } returns false
}
repository = DefaultWalletsRepository(
appPreferencesStore = appPreferenceStore,
tangemTechApi = tangemTechApi,
userWalletsListRepository = mockk(),
seedPhraseNotificationVisibilityStore = mockk(),
dispatchers = dispatchers,
authProvider = authProvider,
walletServerBinder = walletServerBinder,
appsFlyerStore = appsFlyerStore,
accountsFeatureToggles = accountsFeatureToggles,
moshi = mockk(),
)
coEvery { appsFlyerStore.get() } returns null
coEvery {
tangemTechApi.associateApplicationIdWithWallets(
eq(applicationId),
any(),
)
} returns ApiResponse.Success(Unit)
// WHEN
repository.associateWallets(applicationId, userWallets)
// THEN
coVerify(exactly = 1) {
tangemTechApi.associateApplicationIdWithWallets(
applicationId = eq(applicationId),
body = match { body ->
body.size == 2 &&
body.any {
it.walletId == wallet1Id &&
it.cards!!.any { card -> card.cardPublicKey == "public_key_1" } &&
it.name == "Wallet 1"
} &&
body.any {
it.walletId == wallet2Id &&
it.cards!!.any { card -> card.cardPublicKey == "public_key_2" } &&
it.name == "Wallet 2"
}
val userWallets = listOf(
mockk<UserWallet.Cold> {
every { walletId } returns UserWalletId(wallet1Id)
},
mockk<UserWallet.Cold> {
every { walletId } returns UserWalletId(wallet2Id)
},
)
coEvery {
tangemTechApi.associateApplicationIdWithWalletsV2(eq(applicationId), any())
} returns ApiResponse.Success(Unit)
// Act
repository.associateWallets(applicationId, userWallets)
// Assert
coVerify(exactly = 1) {
tangemTechApi.associateApplicationIdWithWalletsV2(
applicationId = eq(applicationId),
body = match { body ->
body.walletIds.size == 2 &&
body.walletIds.contains(wallet1Id) &&
body.walletIds.contains(wallet2Id)
},
)
}
}
}
@Test
fun `GIVEN valid data WHEN activatePromoCode THEN returns Right with status and calls API`() = runTest {
// GIVEN
val walletId = UserWalletId("1234567890abcdef")
val promoCode = "PROMO123"
val address = "bc1qexampleaddress"
coEvery { tangemTechApi.activatePromoCode(any()) } returns ApiResponse.Success(
PromocodeActivationResponse(status = "activated"),
)
@Nested
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
inner class ActivatePromoCode {
// WHEN
val result = repository.activatePromoCode(
userWalletId = walletId,
promoCode = promoCode,
bitcoinAddress = address
)
// THEN
var right: String? = null
var left: ActivatePromoCodeError? = null
result.fold({ left = it }, { right = it })
assertThat(left).isNull()
assertThat(right).isEqualTo("activated")
coVerify(exactly = 1) {
tangemTechApi.activatePromoCode(
match { it is PromocodeActivationBody && it.promoCode == promoCode && it.address == address },
@Test
fun `should return Right with status when API returns success`() = runTest {
// Arrange
val walletId = UserWalletId("1234567890abcdef")
val promoCode = "PROMO123"
val address = "bc1qexampleaddress"
coEvery { tangemTechApi.activatePromoCode(any()) } returns ApiResponse.Success(
PromocodeActivationResponse(status = "activated"),
)
}
}
@Test
fun `GIVEN NOT_FOUND error WHEN activatePromoCode THEN returns Left InvalidPromoCode`() = runTest {
// GIVEN
val walletId = UserWalletId("1234567890abcdef")
coEvery { tangemTechApi.activatePromoCode(any()) } returns
ApiResponse.Error(
// Act
val result = repository.activatePromoCode(
userWalletId = walletId,
promoCode = promoCode,
bitcoinAddress = address,
)
// Assert
var right: String? = null
var left: ActivatePromoCodeError? = null
result.fold({ left = it }, { right = it })
assertThat(left).isNull()
assertThat(right).isEqualTo("activated")
coVerify(exactly = 1) {
tangemTechApi.activatePromoCode(
match<PromocodeActivationBody> { it.promoCode == promoCode && it.address == address },
)
}
}
@Test
fun `should return Left InvalidPromoCode when API returns NOT_FOUND`() = runTest {
// Arrange
val walletId = UserWalletId("1234567890abcdef")
@Suppress("UNCHECKED_CAST")
coEvery { tangemTechApi.activatePromoCode(any()) } returns ApiResponse.Error(
HttpException(code = HttpException.Code.NOT_FOUND, message = null, errorBody = null),
) as ApiResponse<PromocodeActivationResponse>
// WHEN
val result = repository.activatePromoCode(userWalletId = walletId, promoCode = "PROMO", bitcoinAddress = "addr")
// Act
val result = repository.activatePromoCode(
userWalletId = walletId,
promoCode = "PROMO",
bitcoinAddress = "addr",
)
// THEN
var error: ActivatePromoCodeError? = null
result.fold({ error = it }, { })
assertThat(error).isEqualTo(ActivatePromoCodeError.InvalidPromoCode)
}
// Assert
var error: ActivatePromoCodeError? = null
result.fold({ error = it }, { })
assertThat(error).isEqualTo(ActivatePromoCodeError.InvalidPromoCode)
}
@Test
fun `GIVEN CONFLICT error WHEN activatePromoCode THEN returns Left PromocodeAlreadyUsed`() = runTest {
// GIVEN
val walletId = UserWalletId("1234567890abcdef")
coEvery { tangemTechApi.activatePromoCode(any()) } returns
ApiResponse.Error(
@Test
fun `should return Left PromocodeAlreadyUsed when API returns CONFLICT`() = runTest {
// Arrange
val walletId = UserWalletId("1234567890abcdef")
@Suppress("UNCHECKED_CAST")
coEvery { tangemTechApi.activatePromoCode(any()) } returns ApiResponse.Error(
HttpException(code = HttpException.Code.CONFLICT, message = null, errorBody = null),
) as ApiResponse<PromocodeActivationResponse>
// WHEN
val result = repository.activatePromoCode(userWalletId = walletId, promoCode = "PROMO", bitcoinAddress = "addr")
// Act
val result = repository.activatePromoCode(
userWalletId = walletId,
promoCode = "PROMO",
bitcoinAddress = "addr",
)
// THEN
var error: ActivatePromoCodeError? = null
result.fold({ error = it }, { })
assertThat(error).isEqualTo(ActivatePromoCodeError.PromocodeAlreadyUsed)
// Assert
var error: ActivatePromoCodeError? = null
result.fold({ error = it }, { })
assertThat(error).isEqualTo(ActivatePromoCodeError.PromocodeAlreadyUsed)
}
}
}

View file

@ -1,6 +1,5 @@
package com.tangem.domain.nft
import com.tangem.domain.account.featuretoggle.AccountsFeatureToggles
import com.tangem.domain.account.supplier.SingleAccountListSupplier
import com.tangem.domain.models.account.Account
import com.tangem.domain.models.currency.CryptoCurrency
@ -8,52 +7,42 @@ import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.domain.nft.models.NFTCollections
import com.tangem.domain.nft.models.WalletNFTCollections
import com.tangem.domain.nft.repository.NFTRepository
import com.tangem.domain.tokens.repository.CurrenciesRepository
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.*
class GetNFTCollectionsUseCase(
private val currenciesRepository: CurrenciesRepository,
private val nftRepository: NFTRepository,
private val singleAccountListSupplier: SingleAccountListSupplier,
private val accountsFeatureToggles: AccountsFeatureToggles,
) {
@Deprecated("Use invokeForAccounts instead")
@OptIn(ExperimentalCoroutinesApi::class)
operator fun invoke(userWalletId: UserWalletId): Flow<List<NFTCollections>> =
if (accountsFeatureToggles.isFeatureEnabled) {
invokeForAccounts(userWalletId).map { it.flattenCollections }
} else {
currenciesRepository
.getWalletCurrenciesUpdates(userWalletId)
.flatMapLatest {
nftCollections(userWalletId, it)
}
}
@OptIn(ExperimentalCoroutinesApi::class)
fun invokeForAccounts(userWalletId: UserWalletId): Flow<WalletNFTCollections> {
fun Account.flowOfNFTCollections(): Flow<Pair<Account, List<NFTCollections>>>? {
val currencies = (this as? Account.CryptoPortfolio)?.cryptoCurrencies.orEmpty()
if (currencies.isEmpty()) return null
return nftCollections(userWalletId = userWalletId, cryptoCurrencies = currencies.toList())
.map { nfts -> this to nfts }
}
operator fun invoke(userWalletId: UserWalletId): Flow<WalletNFTCollections> {
return singleAccountListSupplier(userWalletId)
.mapLatest { statusList -> statusList.accounts.mapNotNull { it.flowOfNFTCollections() } }
.flatMapLatest { flows -> combine(flows) { WalletNFTCollections(it.toMap()) } }
.mapLatest { statusList -> statusList.accounts.mapNotNull(::flowOfNFTCollections) }
.flatMapLatest { flows ->
combine(flows) { WalletNFTCollections(it.toMap()) }
}
}
private fun nftCollections(
private fun flowOfNFTCollections(account: Account): Flow<Pair<Account, List<NFTCollections>>>? {
val currencies = (account as? Account.CryptoPortfolio)?.cryptoCurrencies.orEmpty()
if (currencies.isEmpty()) return null
return getNftCollections(userWalletId = account.userWalletId, cryptoCurrencies = currencies.toList())
.map { nfts -> account to nfts }
}
private fun getNftCollections(
userWalletId: UserWalletId,
cryptoCurrencies: List<CryptoCurrency>,
): Flow<List<NFTCollections>> {
val networks = cryptoCurrencies
.map { cryptoCurrency -> cryptoCurrency.network }
.map(CryptoCurrency::network)
.distinct()
if (networks.isEmpty()) return flowOf(emptyList())
return nftRepository.observeCollections(userWalletId, networks)
}
}

View file

@ -5,34 +5,23 @@
<ID>BooleanPropertyNaming:NFTAssetUM.kt$NFTAssetUM$val showAllTraitsButton: Boolean</ID>
<ID>BooleanPropertyNaming:NFTAssetUM.kt$NFTAssetUM.BlockItem$val showInfoButton: Boolean</ID>
<ID>BooleanPropertyNaming:NFTAssetUM.kt$NFTAssetUM.Rarity.Content$val showDivider: Boolean</ID>
<ID>BooleanPropertyNaming:NFTCollectionsModel.kt$NFTCollectionsModel$val assetsFulfillQuery = if (query.isEmpty()) { true } else { when (val assets = it.assets) { is NFTCollection.Assets.Empty, is NFTCollection.Assets.Failed, is NFTCollection.Assets.Loading, -&gt; false is NFTCollection.Assets.Value -&gt; { assets.items.any { asset -&gt; asset.name?.lowercase()?.contains(query.lowercase()) == true } } } }</ID>
<ID>BooleanPropertyNaming:NFTCollectionsModel.kt$NFTCollectionsModel$val collectionFulfillQuery = query.isEmpty() || it.name?.lowercase()?.contains(query.lowercase()) == true</ID>
<ID>BooleanPropertyNaming:UpdateDataStateTransformer.kt$UpdateDataStateTransformer$val custom = derivationPath is Network.DerivationPath.Custom</ID>
<ID>MultilineLambdaItParameter:ChangeCollectionExpandedStateTransformer.kt$ChangeCollectionExpandedStateTransformer${ val collectionId = collection.collectionIdProvider() if (it.id == collectionId &amp;&amp; it is NFTCollectionUM) { if (!it.isExpanded) { onFirstExpanded() } it.copy(isExpanded = !it.isExpanded) } else { it } }</ID>
<ID>MultilineLambdaItParameter:NFTCollectionsContent.kt${ key(it.id) { NFTCollectionWarning( modifier = Modifier .padding(top = TangemTheme.dimens.spacing16), state = it, ) } }</ID>
<ID>MultilineLambdaItParameter:NFTCollectionsModel.kt$NFTCollectionsModel${ ChangeCollectionExpandedStateTransformer( collection = collection, collectionIdProvider = collectionIdProvider, onFirstExpanded = { onFirstExpanded(collection) }, ).transform(it) }</ID>
<ID>MultilineLambdaItParameter:NFTCollectionsModel.kt$NFTCollectionsModel${ UpdateDataStateTransformer( nftCollections = listOf(), isAccountMode = isAccountMode, walletNFTCollections = nftCollections.copy(collections = filteredNFTs), onReceiveClick = { params.onReceiveClick() }, onRetryClick = ::onRefresh, onExpandCollectionClick = ::onExpandCollectionClick, onRetryAssetsClick = ::onRetryAssetsClick, onAssetClick = { asset, collection -&gt; params.onAssetClick(asset, collection) }, initialSearchBarFactory = ::getInitialSearchBar, collectionIdProvider = collectionIdProvider, ).transform(it) }</ID>
<ID>MultilineLambdaItParameter:NFTCollectionsModel.kt$NFTCollectionsModel${ UpdateDataStateTransformer( nftCollections = nftCollections.filter(query), onReceiveClick = { params.onReceiveClick() }, onRetryClick = ::onRefresh, onExpandCollectionClick = ::onExpandCollectionClick, onRetryAssetsClick = ::onRetryAssetsClick, onAssetClick = { asset, collection -&gt; params.onAssetClick(asset, collection) }, initialSearchBarFactory = ::getInitialSearchBar, collectionIdProvider = collectionIdProvider, ).transform(it) }</ID>
<ID>MultilineLambdaItParameter:NFTCollectionsModel.kt$NFTCollectionsModel${ it.copy( content = when (val content = it.content) { is NFTCollections.Content.Collections -&gt; content.copy( collections = content.collections.orEmpty().filter { val assetsFulfillQuery = if (query.isEmpty()) { true } else { when (val assets = it.assets) { is NFTCollection.Assets.Empty, is NFTCollection.Assets.Failed, is NFTCollection.Assets.Loading, -&gt; false is NFTCollection.Assets.Value -&gt; { assets.items.any { asset -&gt; asset.name?.lowercase()?.contains(query.lowercase()) == true } } } } val collectionFulfillQuery = query.isEmpty() || it.name?.lowercase()?.contains(query.lowercase()) == true collectionFulfillQuery || assetsFulfillQuery }, ) is NFTCollections.Content.Error -&gt; it.content }, ) }</ID>
<ID>MultilineLambdaItParameter:NFTCollectionsModel.kt$NFTCollectionsModel${ val assetsFulfillQuery = if (query.isEmpty()) { true } else { when (val assets = it.assets) { is NFTCollection.Assets.Empty, is NFTCollection.Assets.Failed, is NFTCollection.Assets.Loading, -&gt; false is NFTCollection.Assets.Value -&gt; { assets.items.any { asset -&gt; asset.name?.lowercase()?.contains(query.lowercase()) == true } } } } val collectionFulfillQuery = query.isEmpty() || it.name?.lowercase()?.contains(query.lowercase()) == true collectionFulfillQuery || assetsFulfillQuery }</ID>
<ID>MultilineLambdaItParameter:NFTDetailsUMFactory.kt$NFTDetailsUMFactory${ NFTAssetUM.BlockItem( title = stringReference(it.name), value = it.value, showInfoButton = false, ) }</ID>
<ID>MultilineLambdaItParameter:NFTDetailsUMFactory.kt$NFTDetailsUMFactory${ NFTAssetUM.Media.Content( url = it, ) }</ID>
<ID>MultilineLambdaItParameter:NFTReceiveModel.kt$NFTReceiveModel${ ShowReceiveBottomSheetTransformer( network = network, networkAddress = value.address, onDismissBottomSheet = ::onReceiveBottomSheetDismiss, onCopyClick = { text -&gt; onCopyClick(text, network) }, onShareClick = { text -&gt; onShareClick(text, network) }, ).transform(it) }</ID>
<ID>MultilineLambdaItParameter:NFTReceiveModel.kt$NFTReceiveModel${ UpdateDataStateTransformer( networks = filteredNetworks, onNetworkClick = ::onNetworkClick, ).transform(it) }</ID>
<ID>MultilineLambdaItParameter:NFTReceiveModel.kt$NFTReceiveModel${ it.copy( bottomSheetConfig = it.bottomSheetConfig?.copy(isShown = false), ) }</ID>
<ID>MultilineLambdaItParameter:UpdateDataStateTransformer.kt$UpdateDataStateTransformer${ NFTCollectionUM( id = it.collectionIdProvider(), networkIconId = getActiveIconRes(it.network.rawId), name = it.name.orEmpty(), description = TextReference.PluralRes( R.plurals.nft_collections_count, it.count, wrappedList(it.count), ), logoUrl = it.logoUrl, assets = it.transformAssets(), onExpandClick = { onExpandCollectionClick(it) }, isExpanded = it.isExpanded(state), ) }</ID>
<ID>NoNameShadowing:NFTCollectionsModel.kt$NFTCollectionsModel${ val assetsFulfillQuery = if (query.isEmpty()) { true } else { when (val assets = it.assets) { is NFTCollection.Assets.Empty, is NFTCollection.Assets.Failed, is NFTCollection.Assets.Loading, -&gt; false is NFTCollection.Assets.Value -&gt; { assets.items.any { asset -&gt; asset.name?.lowercase()?.contains(query.lowercase()) == true } } } } val collectionFulfillQuery = query.isEmpty() || it.name?.lowercase()?.contains(query.lowercase()) == true collectionFulfillQuery || assetsFulfillQuery }</ID>
<ID>NullableBooleanCheck:UpdateDataStateTransformer.kt$UpdateDataStateTransformer$(state.content as? NFTCollectionsUM.Content) ?.collections ?.filterIsInstance&lt;NFTCollectionUM&gt;() ?.firstOrNull { it.id == this.collectionIdProvider() } ?.isExpanded ?: false</ID>
<ID>NullableToStringCall:NFTCollectionsContent.kt$${item2?.id}</ID>
<ID>NullableToStringCall:NFTCollectionsModel.kt$NFTCollectionsModel$${network.derivationPath.value}</ID>
<ID>PropertyUsedBeforeDeclaration:NFTDetailsModel.kt$NFTDetailsModel$_state</ID>
<ID>ReusedModifierInstance:NFTCollectionsContent.kt$Box( modifier = modifier .fillMaxSize() .padding(bottom = bottomPadding), ) { Text( modifier = Modifier .align(Alignment.Center), text = stringResourceSafe(id = R.string.nft_empty_search), style = TangemTheme.typography.body2, color = TangemTheme.colors.text.tertiary, textAlign = TextAlign.Center, ) }</ID>
<ID>ReusedModifierInstance:NFTCollectionsLoading.kt$Card( modifier = modifier .fillMaxWidth() .padding( top = TangemTheme.dimens.spacing16, ), shape = RoundedCornerShape(TangemTheme.dimens.radius16), colors = CardDefaults.cardColors( containerColor = TangemTheme.colors.background.primary, contentColor = TangemTheme.colors.text.primary1, disabledContainerColor = TangemTheme.colors.background.primary, disabledContentColor = TangemTheme.colors.text.primary1, ), ) { Column { repeat(SHIMMER_ITEMS_COUNT) { CollectionPlaceholder() } } }</ID>
<ID>ReusedModifierInstance:NFTDetailsAsset.kt$Column( modifier = modifier .verticalScroll(scrollState) .padding( start = TangemTheme.dimens.spacing16, top = TangemTheme.dimens.spacing16, end = TangemTheme.dimens.spacing16, bottom = bottomPadding, ) .fillMaxSize(), ) { NFTDetailsLogo( state = state.media, modifier = Modifier .aspectRatio(1f), ) NFTDetailsInfoGroup( modifier = Modifier .padding(top = TangemTheme.dimens.spacing12), state = state.topInfo, onReadMoreClick = onReadMoreClick, ) NFTDetailsBlocksGroup( modifier = Modifier .padding(top = TangemTheme.dimens.spacing12), items = state.traits, title = resourceReference(R.string.nft_details_traits), action = if (state.showAllTraitsButton) { { NFTBlocksGroupAction( text = resourceReference(R.string.common_see_all), startIcon = { }, onClick = onSeeAllTraitsClick, ) } } else { null }, ) NFTDetailsBlocksGroup( modifier = Modifier .padding(top = TangemTheme.dimens.spacing12), items = state.baseInfoItems, title = resourceReference(R.string.nft_details_base_information), action = { NFTBlocksGroupAction( text = resourceReference(R.string.common_explore), startIcon = { NFTBlocksGroupActionIcon(iconRes = R.drawable.ic_compass_24) }, onClick = onExploreClick, ) }, ) }</ID>
<ID>ReusedModifierInstance:NFTDetailsInfoGroup.kt$Column( modifier = modifier .padding( start = TangemTheme.dimens.spacing12, end = TangemTheme.dimens.spacing12, ), ) { TextShimmer( modifier = Modifier .width(TangemTheme.dimens.size158), style = TangemTheme.typography.head, textSizeHeight = true, ) TextShimmer( modifier = Modifier .padding(top = TangemTheme.dimens.spacing4) .width(TangemTheme.dimens.size90), style = TangemTheme.typography.caption2, textSizeHeight = true, ) }</ID>
<ID>ReusedModifierInstance:NFTDetailsInfoGroup.kt$Column( modifier = modifier .padding( start = TangemTheme.dimens.spacing12, end = TangemTheme.dimens.spacing12, ), verticalArrangement = Arrangement.SpaceAround, ) { Text( text = state.cryptoPrice.resolveReference(), style = TangemTheme.typography.head, color = TangemTheme.colors.text.primary1, ) Text( modifier = Modifier .padding(top = TangemTheme.dimens.spacing4) .flicker(state.isFlickering), text = state.fiatPrice.resolveReference(), style = TangemTheme.typography.caption2, color = TangemTheme.colors.text.tertiary, ) }</ID>
<ID>UnsafeCallOnNullableType:DefaultNFTComponent.kt$DefaultNFTComponent$portfolioFetcher!!</ID>
<ID>UseEmptyCounterpart:NFTCollectionsModel.kt$NFTCollectionsModel$listOf()</ID>
<ID>UseEmptyCounterpart:UpdateDataStateTransformer.kt$UpdateDataStateTransformer$listOf()</ID>
<ID>UseOrEmpty:UpdateDataStateTransformer.kt$UpdateDataStateTransformer$walletNFTCollections.collections.values.firstOrNull() ?: listOf()</ID>
</CurrentIssues>

View file

@ -7,7 +7,6 @@ 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.featuretoggle.AccountsFeatureToggles
import com.tangem.domain.account.usecase.IsAccountsModeEnabledUseCase
import com.tangem.domain.nft.FetchNFTCollectionAssetsUseCase
import com.tangem.domain.nft.GetNFTCollectionsUseCase
@ -33,7 +32,6 @@ internal class NFTCollectionsModel @Inject constructor(
private val getNFTCollectionsUseCase: GetNFTCollectionsUseCase,
private val fetchNFTCollectionAssetsUseCase: FetchNFTCollectionAssetsUseCase,
private val refreshAllNFTUseCase: RefreshAllNFTUseCase,
private val accountsFeatureToggles: AccountsFeatureToggles,
private val isAccountsModeEnabledUseCase: IsAccountsModeEnabledUseCase,
paramsContainer: ParamsContainer,
) : Model() {
@ -66,51 +64,21 @@ internal class NFTCollectionsModel @Inject constructor(
}
init {
if (accountsFeatureToggles.isFeatureEnabled) {
subscribeToNFTCollectionsNew()
} else {
subscribeToNFTCollections()
}
subscribeToNFTCollections()
}
private fun subscribeToNFTCollections() {
combine(
flow = getNFTCollectionsUseCase(params.userWalletId),
flow2 = searchManager.query.distinctUntilChanged(),
) { nftCollections, query ->
_state.update {
UpdateDataStateTransformer(
nftCollections = nftCollections.filter(query),
onReceiveClick = {
params.onReceiveClick()
},
onRetryClick = ::onRefresh,
onExpandCollectionClick = ::onExpandCollectionClick,
onRetryAssetsClick = ::onRetryAssetsClick,
onAssetClick = { asset, collection ->
params.onAssetClick(asset, collection)
},
initialSearchBarFactory = ::getInitialSearchBar,
collectionIdProvider = collectionIdProvider,
).transform(it)
}
}
.onStart { onRefresh() }
.launchIn(modelScope)
}
private fun subscribeToNFTCollectionsNew() {
combine(
flow = getNFTCollectionsUseCase.invokeForAccounts(params.userWalletId),
flow2 = searchManager.query.distinctUntilChanged(),
flow3 = isAccountsModeEnabledUseCase(),
) { nftCollections, query, isAccountMode ->
val filteredNFTs = nftCollections.collections
.mapValues { (_, nfts) -> nfts.filter(query) }
_state.update {
_state.update { stateUM ->
UpdateDataStateTransformer(
nftCollections = listOf(),
nftCollections = emptyList(),
isAccountMode = isAccountMode,
walletNFTCollections = nftCollections.copy(collections = filteredNFTs),
onReceiveClick = {
@ -124,22 +92,22 @@ internal class NFTCollectionsModel @Inject constructor(
},
initialSearchBarFactory = ::getInitialSearchBar,
collectionIdProvider = collectionIdProvider,
).transform(it)
).transform(stateUM)
}
}
.onStart { onRefresh() }
.launchIn(modelScope)
}
private fun List<NFTCollections>.filter(query: String): List<NFTCollections> = map {
it.copy(
content = when (val content = it.content) {
private fun List<NFTCollections>.filter(query: String): List<NFTCollections> = map { collections ->
collections.copy(
content = when (val content = collections.content) {
is NFTCollections.Content.Collections -> content.copy(
collections = content.collections.orEmpty().filter {
val assetsFulfillQuery = if (query.isEmpty()) {
collections = content.collections.orEmpty().filter { collection: NFTCollection ->
val isAssetsFulfillQuery = if (query.isEmpty()) {
true
} else {
when (val assets = it.assets) {
when (val assets = collection.assets) {
is NFTCollection.Assets.Empty,
is NFTCollection.Assets.Failed,
is NFTCollection.Assets.Loading,
@ -152,13 +120,13 @@ internal class NFTCollectionsModel @Inject constructor(
}
}
val collectionFulfillQuery =
query.isEmpty() || it.name?.lowercase()?.contains(query.lowercase()) == true
val isCollectionFulfillQuery =
query.isEmpty() || collection.name?.lowercase()?.contains(query.lowercase()) == true
collectionFulfillQuery || assetsFulfillQuery
isCollectionFulfillQuery || isAssetsFulfillQuery
},
)
is NFTCollections.Content.Error -> it.content
is NFTCollections.Content.Error -> collections.content
},
)
}
@ -198,12 +166,12 @@ internal class NFTCollectionsModel @Inject constructor(
}
private fun onExpandCollectionClick(collection: NFTCollection) {
_state.update {
_state.update { stateUM ->
ChangeCollectionExpandedStateTransformer(
collection = collection,
collectionIdProvider = collectionIdProvider,
onFirstExpanded = { onFirstExpanded(collection) },
).transform(it)
).transform(stateUM)
}
}

View file

@ -20,7 +20,6 @@ import com.tangem.core.decompose.context.childByContext
import com.tangem.core.decompose.navigation.inner.InnerRouter
import com.tangem.core.ui.decompose.ComposableBottomSheetComponent
import com.tangem.core.ui.decompose.ComposableContentComponent
import com.tangem.domain.account.featuretoggle.AccountsFeatureToggles
import com.tangem.domain.models.PortfolioId
import com.tangem.features.account.PortfolioFetcher
import com.tangem.features.account.PortfolioSelectorComponent
@ -53,7 +52,6 @@ internal class DefaultNFTComponent @AssistedInject constructor(
private val portfolioSelectorComponentFactory: PortfolioSelectorComponent.Factory,
private val portfolioSelectorController: PortfolioSelectorController,
portfolioFetcherFactory: PortfolioFetcher.Factory,
private val accountsFeatureToggles: AccountsFeatureToggles,
) : NFTComponent, AppComponentContext by appComponentContext {
private val stackNavigation = StackNavigation<NFTRoute>()
@ -66,14 +64,10 @@ internal class DefaultNFTComponent @AssistedInject constructor(
private val initialRoute: NFTRoute = NFTRoute.Collections(params.userWalletId)
private val currentRoute = MutableStateFlow(initialRoute)
private val onReceiveClickJob = JobHolder()
private val portfolioFetcher: PortfolioFetcher? = if (accountsFeatureToggles.isFeatureEnabled) {
portfolioFetcherFactory.create(
mode = PortfolioFetcher.Mode.Wallet(params.userWalletId),
scope = componentScope,
)
} else {
null
}
private val portfolioFetcher: PortfolioFetcher = portfolioFetcherFactory.create(
mode = PortfolioFetcher.Mode.Wallet(params.userWalletId),
scope = componentScope,
)
private val bottomSheetNavigation: SlotNavigation<Unit> = SlotNavigation()
private val portfolioSelectorCallback = object : PortfolioSelectorComponent.BottomSheetCallback {
override val onDismiss: () -> Unit = { bottomSheetNavigation.dismiss() }
@ -83,7 +77,7 @@ internal class DefaultNFTComponent @AssistedInject constructor(
source = bottomSheetNavigation,
serializer = Unit.serializer(),
handleBackButton = false,
childFactory = { configuration, context -> bottomSheetChild(context) },
childFactory = { _, context -> bottomSheetChild(context) },
)
private val childStack = childStack(
@ -146,17 +140,7 @@ internal class DefaultNFTComponent @AssistedInject constructor(
params = NFTCollectionsComponent.Params(
userWalletId = route.userWalletId,
onBackClick = ::onChildBack,
onReceiveClick = {
if (accountsFeatureToggles.isFeatureEnabled) {
onReceiveClick(route)
} else {
innerRouter.push(
NFTRoute.Receive(
portfolioId = PortfolioId(route.userWalletId),
),
)
}
},
onReceiveClick = { onReceiveClick(route) },
onAssetClick = { asset, collection ->
innerRouter.push(
NFTRoute.Details(
@ -170,7 +154,6 @@ internal class DefaultNFTComponent @AssistedInject constructor(
)
private fun onReceiveClick(route: NFTRoute.Collections) = componentScope.launch {
val portfolioFetcher = requireNotNull(portfolioFetcher)
portfolioSelectorController.selectAccount(null)
portfolioFetcher.updateMode(mode = PortfolioFetcher.Mode.Wallet(route.userWalletId))
val portfolioData = portfolioFetcher.data.first()
@ -245,7 +228,7 @@ internal class DefaultNFTComponent @AssistedInject constructor(
portfolioSelectorComponentFactory.create(
context = childByContext(componentContext),
params = PortfolioSelectorComponent.Params(
portfolioFetcher = portfolioFetcher!!,
portfolioFetcher = portfolioFetcher,
controller = portfolioSelectorController,
bsCallback = portfolioSelectorCallback,
),

View file

@ -4,7 +4,6 @@
<CurrentIssues>
<ID>BooleanPropertyNaming:AddStakingNotificationsTransformer.kt$AddStakingNotificationsTransformer$val showNotification = sendingAmount + feeAmount &gt; balance</ID>
<ID>BooleanPropertyNaming:AmountCurrencyChangeStateTransformer.kt$AmountCurrencyChangeStateTransformer$private val value: Boolean</ID>
<ID>BooleanPropertyNaming:StakingModel.kt$StakingModel$val noBalanceState = balanceState == null</ID>
<ID>BooleanPropertyNaming:StakingUiState.kt$StakingStates.InitialInfoState.Data$val showBanner: Boolean</ID>
<ID>BooleanPropertyNaming:StakingUiState.kt$StakingUiState$val showColdWalletInteractionIcon: Boolean</ID>
<ID>CastNullableToNonNullableType:SetApprovalBottomSheetInProgressTransformer.kt$SetApprovalBottomSheetInProgressTransformer$as</ID>
@ -14,28 +13,13 @@
<ID>MultilineLambdaItParameter:StakingFeeBlock.kt${ if (it == FeeState.Error) { Text( text = DASH_SIGN, color = TangemTheme.colors.text.primary1, style = TangemTheme.typography.body1, ) } }</ID>
<ID>MultilineLambdaItParameter:StakingFeeBlock.kt${ if (it == FeeState.Loading) { RectangleShimmer( radius = TangemTheme.dimens.radius3, modifier = Modifier.size( height = TangemTheme.dimens.size24, width = TangemTheme.dimens.size90, ), ) } }</ID>
<ID>MultilineLambdaItParameter:StakingInfoNotificationsFactory.kt$StakingInfoNotificationsFactory${ it.type == BalanceType.PREPARING || it.type == BalanceType.STAKED || it.type == BalanceType.LOCKED }</ID>
<ID>MultilineLambdaItParameter:StakingModel.kt$StakingModel${ EnterAmountBoundary( amount = it, fiatRate = status.value.fiatRate.orZero(), ) }</ID>
<ID>MultilineLambdaItParameter:StakingModel.kt$StakingModel${ Timber.e(it) false }</ID>
<ID>MultilineLambdaItParameter:StakingModel.kt$StakingModel${ isBalanceHiddenFlow.value = it.isBalanceHidden stateController.update( transformer = HideBalanceStateTransformer( isBalanceHidden = it.isBalanceHidden, cryptoCurrencyStatus = cryptoCurrencyStatus, appCurrency = appCurrency, ), ) }</ID>
<ID>MultilineLambdaItParameter:StakingModel.kt$StakingModel${ stateController.update( SetFeeToTonInitializeBottomSheetTransformer( appCurrencyProvider = Provider { appCurrency }, feeCryptoCurrencyStatus = feeCryptoCurrencyStatus, fee = it.normal, isFeeApproximate = false, ), ) }</ID>
<ID>MultilineLambdaItParameter:StakingStateController.kt$StakingStateController${ it.copy( showColdWalletInteractionIcon = userWallet is UserWallet.Cold, ) }</ID>
<ID>NullCheckOnMutableProperty:StakingModel.kt$StakingModel$if (feeCryptoCurrencyStatus != null &amp;&amp; fee != null) { getBalanceNotEnoughForFeeWarningUseCase( fee = fee, userWalletId = userWalletId, tokenStatus = cryptoCurrencyStatus, coinStatus = feeCryptoCurrencyStatus ?: cryptoCurrencyStatus, ).getOrNull() } else { null }</ID>
<ID>NullableToStringCall:DefaultStakingDeepLinkHandler.kt$DefaultStakingDeepLinkHandler$$networkId</ID>
<ID>NullableToStringCall:DefaultStakingDeepLinkHandler.kt$DefaultStakingDeepLinkHandler$$tokenId</ID>
<ID>PropertyUsedBeforeDeclaration:StakingFeeBlock.kt$FeeBlockPreviewProvider$contentState</ID>
<ID>PropertyUsedBeforeDeclaration:StakingModel.kt$StakingModel$isAmountSubtractAvailable</ID>
<ID>PropertyUsedBeforeDeclaration:StakingStateController.kt$StakingStateController$uiState</ID>
<ID>SuspendFunSwallowedCancellation:StakingModel.kt$StakingModel$runCatching</ID>
<ID>UnnecessaryEventHandlerParameter:StakingInitialInfoContent.kt$onClick: (BalanceState) -&gt; Unit</ID>
<ID>UnnecessaryLet:StakingModel.kt$StakingModel$let(::add)</ID>
<ID>UnnecessaryLet:StakingTosText.kt$let { onTextClick(PRIVACY_POLICY_URL) }</ID>
<ID>UnnecessaryLet:StakingTosText.kt$let { onTextClick(TERMS_OF_USE_URL) }</ID>
<ID>UnsafeCallOnNullableType:StakingModel.kt$StakingModel$tonAccountInitializeTransaction!!</ID>
<ID>VarCouldBeVal:StakingModel.kt$StakingModel$private var actionsJobHolder: JobHolder = JobHolder()</ID>
<ID>VarCouldBeVal:StakingModel.kt$StakingModel$private var approvalJobHolder: JobHolder = JobHolder()</ID>
<ID>VarCouldBeVal:StakingModel.kt$StakingModel$private var feeJobHolder: JobHolder = JobHolder()</ID>
<ID>VarCouldBeVal:StakingModel.kt$StakingModel$private var sendTransactionJobHolder = JobHolder()</ID>
<ID>VarCouldBeVal:StakingModel.kt$StakingModel$private var stakingStateRouter: StakingStateRouter = StakingStateRouter( appRouter = appRouter, stateController = stateController, analyticsEventsHandler = analyticsEventHandler, )</ID>
<ID>VarCouldBeVal:StakingModel.kt$StakingModel$private var stepChangesJobHolder = JobHolder()</ID>
</CurrentIssues>
</SmellBaseline>

View file

@ -7,7 +7,6 @@ import com.tangem.blockchain.common.transaction.Fee
import com.tangem.blockchain.common.transaction.TransactionFee
import com.tangem.common.getValidatorsCount
import com.tangem.common.routing.AppRouter
import com.tangem.common.ui.amountScreen.converters.AmountReduceByTransformer
import com.tangem.common.ui.amountScreen.converters.AmountReduceByTransformer.ReduceByData
import com.tangem.common.ui.amountScreen.models.AmountState
import com.tangem.common.ui.amountScreen.models.EnterAmountBoundary
@ -28,7 +27,6 @@ import com.tangem.core.ui.format.bigdecimal.format
import com.tangem.core.ui.haptic.TangemHapticEffect
import com.tangem.core.ui.haptic.VibratorHapticManager
import com.tangem.core.ui.message.DialogMessage
import com.tangem.domain.account.featuretoggle.AccountsFeatureToggles
import com.tangem.domain.account.status.usecase.GetAccountCurrencyStatusUseCase
import com.tangem.domain.account.usecase.IsAccountsModeEnabledUseCase
import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase
@ -113,7 +111,6 @@ internal class StakingModel @Inject constructor(
private val stateController: StakingStateController,
override val dispatchers: CoroutineDispatcherProvider,
private val getBalanceHidingSettingsUseCase: GetBalanceHidingSettingsUseCase,
private val getSingleCryptoCurrencyStatusUseCase: GetSingleCryptoCurrencyStatusUseCase,
private val getFeePaidCryptoCurrencyStatusSyncUseCase: GetFeePaidCryptoCurrencyStatusSyncUseCase,
private val getMinimumTransactionAmountSyncUseCase: GetMinimumTransactionAmountSyncUseCase,
private val getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase,
@ -149,7 +146,6 @@ internal class StakingModel @Inject constructor(
@DelayedWork private val coroutineScope: CoroutineScope,
private val innerRouter: InnerStakingRouter,
private val messageSender: UiMessageSender,
private val accountsFeatureToggles: AccountsFeatureToggles,
appRouter: AppRouter,
) : Model(), StakingClickIntents {
@ -158,7 +154,7 @@ internal class StakingModel @Inject constructor(
private val params = paramsContainer.require<StakingComponent.Params>()
private var stakingStateRouter: StakingStateRouter = StakingStateRouter(
private val stakingStateRouter: StakingStateRouter = StakingStateRouter(
appRouter = appRouter,
stateController = stateController,
analyticsEventsHandler = analyticsEventHandler,
@ -237,6 +233,7 @@ internal class StakingModel @Inject constructor(
)
}
@Suppress("PropertyUsedBeforeDeclaration")
private val transactionSender: StakingTransactionSender by lazy(LazyThreadSafetyMode.NONE) {
stakingOperationsFactory.createTransactionSender(
cryptoCurrencyStatus = cryptoCurrencyStatus,
@ -298,17 +295,17 @@ internal class StakingModel @Inject constructor(
override fun onNextClick(balanceState: BalanceState?) {
modelScope.launch {
val isInitialInfoStep = value.currentStep == StakingStep.InitialInfo
val noBalanceState = balanceState == null
val isBalanceAbsent = balanceState == null
val hasNoYieldBalanceData = cryptoCurrencyStatus.value.stakingBalance !is StakingBalance.Data.StakeKit
when {
isInitialInfoStep && noBalanceState && integration.areAllTargetsFull && hasNoYieldBalanceData -> {
isInitialInfoStep && isBalanceAbsent && integration.areAllTargetsFull && hasNoYieldBalanceData -> {
stakingEventFactory.createStakingValidatorsUnavailableAlert()
return@launch
}
isInitialInfoStep && noBalanceState -> {
isInitialInfoStep && isBalanceAbsent -> {
val list = buildList {
SetConfirmationStateInitTransformer(
val setConfirmationStateInitTransformer = SetConfirmationStateInitTransformer(
isEnter = true,
isExplicitExit = false,
balanceState = null,
@ -316,13 +313,17 @@ internal class StakingModel @Inject constructor(
stakingApproval = stakingApproval,
stakingAllowance = stakingAllowance,
integration = integration,
).let(::add)
)
add(setConfirmationStateInitTransformer)
if (integration.isPartialAmountDisabled) {
ValidatorSelectChangeTransformer(
val validatorSelectChangeTransformer = ValidatorSelectChangeTransformer(
selectedTarget = integration.preferredTargets.firstOrNull(),
integration = integration,
).let(::add)
SetAmountDataTransformer(
)
val setAmountDataTransformer = SetAmountDataTransformer(
clickIntents = this@StakingModel,
cryptoCurrencyStatusProvider = Provider { cryptoCurrencyStatus },
userWalletProvider = Provider { userWallet },
@ -330,15 +331,25 @@ internal class StakingModel @Inject constructor(
isBalanceHidden = isBalanceHiddenFlow.value,
isAccountsModeEnabled = isAccountsModeEnabled,
account = account,
).let(::add)
AmountMaxValueStateTransformer(
)
val amountMaxValueStateTransformer = AmountMaxValueStateTransformer(
cryptoCurrencyStatus = cryptoCurrencyStatus,
minimumTransactionAmount = minimumTransactionAmount,
actionType = uiState.value.actionType,
integration = integration,
).let(::add)
)
addAll(
listOf(
validatorSelectChangeTransformer,
setAmountDataTransformer,
amountMaxValueStateTransformer,
),
)
}
}
stateController.updateAll(*list.toTypedArray())
}
}
@ -900,7 +911,7 @@ internal class StakingModel @Inject constructor(
AmountReduceByStateTransformer(
cryptoCurrencyStatus = cryptoCurrencyStatus,
minimumTransactionAmount = minimumTransactionAmount,
value = AmountReduceByTransformer.ReduceByData(
value = ReduceByData(
reduceAmountBy = reduceAmountBy,
reduceAmountByDiff = reduceAmountByDiff,
),
@ -953,7 +964,7 @@ internal class StakingModel @Inject constructor(
task = PeriodicTask(
delay = ALLOWANCE_UPDATE_DELAY,
task = {
runCatching {
runSuspendCatching {
getAllowanceUseCase(
userWalletId = userWalletId,
cryptoCurrency = cryptoCurrencyStatus.currency,
@ -1073,17 +1084,20 @@ internal class StakingModel @Inject constructor(
network = cryptoCurrencyStatus.currency.network,
memo = null,
)
tonAccountInitializeTransaction = transaction.getOrElse {
val initialTransaction = transaction.getOrElse {
stateController.update(
SetFeeErrorToTonInitializeBottomSheetTransformer(),
)
return@launch
}
tonAccountInitializeTransaction = initialTransaction
val transactionFee = getFeeUseCase(
userWallet = userWallet,
network = cryptoCurrencyStatus.currency.network,
transactionData = tonAccountInitializeTransaction!!,
transactionData = initialTransaction,
)
transactionFee.fold(
@ -1092,12 +1106,12 @@ internal class StakingModel @Inject constructor(
SetFeeErrorToTonInitializeBottomSheetTransformer(),
)
},
ifRight = {
ifRight = { fee ->
stateController.update(
SetFeeToTonInitializeBottomSheetTransformer(
appCurrencyProvider = Provider { appCurrency },
feeCryptoCurrencyStatus = feeCryptoCurrencyStatus,
fee = it.normal,
fee = fee.normal,
isFeeApproximate = false,
),
)
@ -1190,41 +1204,20 @@ internal class StakingModel @Inject constructor(
}
private fun subscribeOnCurrencyStatusUpdates() {
if (accountsFeatureToggles.isFeatureEnabled) {
getAccountCurrencyStatusUseCase(
userWalletId = params.userWalletId,
currency = params.cryptoCurrency,
).conflate().distinctUntilChanged()
.filter {
value.currentStep == StakingStep.InitialInfo || isTopHeatupCase()
}.onEach { (maybeAccount, maybeStatus) ->
isAccountsModeEnabled = isAccountsModeEnabledUseCase.invokeSync()
account = maybeAccount
onDataLoaded(maybeStatus)
}.flowOn(dispatchers.main)
.launchIn(modelScope)
} else {
getSingleCryptoCurrencyStatusUseCase.invokeMultiWallet(
userWalletId = userWalletId,
currencyId = cryptoCurrencyId,
isSingleWalletWithTokens = false,
).conflate().distinctUntilChanged()
.filter {
value.currentStep == StakingStep.InitialInfo || isTopHeatupCase()
}
.onEach { maybeStatus ->
maybeStatus.fold(
ifRight = { onDataLoaded(it) },
ifLeft = { error ->
stakingEventFactory.createGenericErrorAlert(error.toString())
stateController.update(
SetConfirmationStateResetAssentTransformer(cryptoCurrencyStatus = cryptoCurrencyStatus),
)
},
)
}.flowOn(dispatchers.main)
.launchIn(modelScope)
}
getAccountCurrencyStatusUseCase(
userWalletId = params.userWalletId,
currency = params.cryptoCurrency,
)
.conflate()
.distinctUntilChanged()
.filter { value.currentStep == StakingStep.InitialInfo || isTopHeatupCase() }
.onEach { (maybeAccount, maybeStatus) ->
isAccountsModeEnabled = isAccountsModeEnabledUseCase.invokeSync()
account = maybeAccount
onDataLoaded(maybeStatus)
}
.flowOn(dispatchers.main)
.launchIn(modelScope)
}
private suspend fun onDataLoaded(status: CryptoCurrencyStatus) {
@ -1241,12 +1234,11 @@ internal class StakingModel @Inject constructor(
)
}
feeCryptoCurrencyStatus =
getFeePaidCryptoCurrencyStatusSyncUseCase(userWalletId, status).getOrNull()
minimumTransactionAmount =
getMinimumTransactionAmountSyncUseCase(userWalletId, status).getOrNull()?.let {
feeCryptoCurrencyStatus = getFeePaidCryptoCurrencyStatusSyncUseCase(userWalletId, status).getOrNull()
minimumTransactionAmount = getMinimumTransactionAmountSyncUseCase(userWalletId, status).getOrNull()
?.let { amount ->
EnterAmountBoundary(
amount = it,
amount = amount,
fiatRate = status.value.fiatRate.orZero(),
)
}
@ -1265,11 +1257,11 @@ internal class StakingModel @Inject constructor(
getBalanceHidingSettingsUseCase()
.conflate()
.distinctUntilChanged()
.onEach {
isBalanceHiddenFlow.value = it.isBalanceHidden
.onEach { settings ->
isBalanceHiddenFlow.value = settings.isBalanceHidden
stateController.update(
transformer = HideBalanceStateTransformer(
isBalanceHidden = it.isBalanceHidden,
isBalanceHidden = settings.isBalanceHidden,
cryptoCurrencyStatus = cryptoCurrencyStatus,
appCurrency = appCurrency,
),
@ -1403,8 +1395,8 @@ internal class StakingModel @Inject constructor(
val isAccountInitializedNewValue = checkAccountInitializedUseCase.invoke(
userWalletId = userWalletId,
network = cryptoCurrencyStatus.currency.network,
).getOrElse {
Timber.e(it)
).getOrElse { throwable ->
Timber.e(throwable)
false
}

View file

@ -13,17 +13,8 @@
<ID>BooleanPropertyNaming:TokenDetailsSwapTransactionsStateConverter.kt$TokenDetailsSwapTransactionsStateConverter$val showProviderLink = getShowProviderLink(notification, statusModel)</ID>
<ID>BooleanPropertyNaming:TokenDetailsSwapTransactionsStateConverter.kt$TokenDetailsSwapTransactionsStateConverter$val showProviderLink = getShowProviderLink(notification, transaction.status)</ID>
<ID>BooleanPropertyNaming:TokenDetailsTopAppBar.kt$var showDropdownMenu by rememberSaveable { mutableStateOf(false) }</ID>
<ID>MultilineLambdaItParameter:DefaultTokenDetailsDeepLinkHandler.kt$DefaultTokenDetailsDeepLinkHandler${ val isNetwork = it.network.backendId.equals(networkId, ignoreCase = true) val isCurrency = it.id.rawCurrencyId?.value?.equals(tokenId, ignoreCase = true) == true val isDefaultDerivation = it.network.derivationPath is Network.DerivationPath.Card val isCustomDerivation = derivationPath?.equals(it.network.derivationPath.value) == true val isCorrectDerivation = isDefaultDerivation || isCustomDerivation isNetwork &amp;&amp; isCurrency &amp;&amp; isCorrectDerivation }</ID>
<ID>MultilineLambdaItParameter:ExpressStatusFactory.kt$ExpressStatusFactory${ when (it) { is ExpressTransactionStateUM.OnrampUM -&gt; it.activeStatus.isHidden else -&gt; false } }</ID>
<ID>MultilineLambdaItParameter:OnrampStatusFactory.kt$OnrampStatusFactory${ Timber.e("Couldn't update onramp status. $it") onrampTx }</ID>
<ID>MultilineLambdaItParameter:TokenDetailsModel.kt$TokenDetailsModel${ Timber.e(it.cause?.localizedMessage.orEmpty()) "" }</ID>
<ID>MultilineLambdaItParameter:TokenDetailsModel.kt$TokenDetailsModel${ analyticsEventsHandler.send( TokenReceiveAnalyticsEvent.ButtonShareAddress(cryptoCurrency.symbol), ) shareManager.shareText(text = it) }</ID>
<ID>MultilineLambdaItParameter:TokenDetailsModel.kt$TokenDetailsModel${ analyticsEventsHandler.send(TokenReceiveAnalyticsEvent.ButtonCopyAddress(cryptoCurrency.symbol)) clipboardManager.setText(text = it, isSensitive = true) }</ID>
<ID>MultilineLambdaItParameter:TokenDetailsModel.kt$TokenDetailsModel${ analyticsExceptionHandler.sendException( event = ExceptionAnalyticsEvent( exception = it, params = mapOf( "blockchainId" to cryptoCurrency.network.id.rawId.value, "networkId" to cryptoCurrency.network.backendId, ), ), ) Timber.e( /* t = */ it, /* message = */ "Unable to get wallet manager for user wallet %s and network %s", /* ...args = */ userWalletId, cryptoCurrency.network, ) false }</ID>
<ID>MultilineLambdaItParameter:TokenDetailsModel.kt$TokenDetailsModel${ internalUiState.value = stateFactory.getStateWithErrorDialog(stringReference(it)) Timber.e(it) }</ID>
<ID>MultilineLambdaItParameter:TokenDetailsModel.kt$TokenDetailsModel${ internalUiState.value = stateFactory.getStateWithUpdatedHidden( isBalanceHidden = it.isBalanceHidden, ) }</ID>
<ID>MultilineLambdaItParameter:TokenDetailsModel.kt$TokenDetailsModel${ sendButtonsEvents(it.states) internalUiState.value = stateFactory.getManageButtonsState(actions = it.states) }</ID>
<ID>MultilineLambdaItParameter:TokenDetailsModel.kt$TokenDetailsModel${ val updatedState = stateFactory.getStateWithNotifications(it) notificationsAnalyticsSender.send(internalUiState.value, updatedState.notifications) internalUiState.value = updatedState }</ID>
<ID>MultilineLambdaItParameter:TokenDetailsScreen.kt${ Notification( modifier = itemModifier.animateItem(), config = it.config, iconTint = when (it) { is TokenDetailsNotification.Informational -&gt; TangemTheme.colors.icon.accent is TokenDetailsNotification.UsedOutdatedData -&gt; TangemTheme.colors.text.attention else -&gt; null }, ) }</ID>
<ID>MultilineLambdaItParameter:TokenDetailsTopAppBar.kt${ TangemDropdownItem( item = it, dismissParent = { showDropdownMenu = false }, ) }</ID>
<ID>MultilineLambdaItParameter:TokenStakingBlock.kt${ when (it) { is StakingBlockUM.TemporaryUnavailable -&gt; StakingTemporaryUnavailableBlock() is StakingBlockUM.Loading -&gt; StakingLoading() is StakingBlockUM.Staked -&gt; StakingBalanceBlock( state = it, isBalanceHidden = isBalanceHidden, ) is StakingBlockUM.StakeAvailable -&gt; StakingAvailableContent( state = it, ) } }</ID>
@ -31,21 +22,15 @@
<ID>NamedArguments:TokenDetailsSwapTransactionsStateConverter.kt$TokenDetailsSwapTransactionsStateConverter$createStateInfo( transaction, toCryptoCurrency, fromCryptoCurrency, toFiatAmount, fromFiatAmount, )</ID>
<ID>NestedScopeFunctions:TokenDetailsBalanceSelectStateConverter.kt$TokenDetailsBalanceSelectStateConverter$let { cryptoCurrencyStatus.value.fiatRate?.multiply(it) }</ID>
<ID>NullableBooleanCheck:TokenDetailsSwapTransactionsStateConverter.kt$TokenDetailsSwapTransactionsStateConverter$transaction.status?.hasLongTime ?: false</ID>
<ID>NullableToStringCall:DefaultTokenDetailsDeepLinkHandler.kt$DefaultTokenDetailsDeepLinkHandler$$networkId</ID>
<ID>NullableToStringCall:DefaultTokenDetailsDeepLinkHandler.kt$DefaultTokenDetailsDeepLinkHandler$$tokenId</ID>
<ID>NullableToStringCall:TokenDetailsStakingInfoConverter.kt$TokenDetailsStakingInfoConverter$$stakingCryptoAmount</ID>
<ID>NullableToStringCall:TokenDetailsStakingInfoConverter.kt$TokenDetailsStakingInfoConverter$$stakingEntryInfo</ID>
<ID>PropertyUsedBeforeDeclaration:ExpressStatusBottomSheetStateProvider.kt$ExpressStatusBottomSheetStateProvider$network</ID>
<ID>PropertyUsedBeforeDeclaration:ExpressStatusBottomSheetStateProvider.kt$ExpressStatusBottomSheetStateProvider$token</ID>
<ID>PropertyUsedBeforeDeclaration:TokenDetailsModel.kt$TokenDetailsModel$uiState</ID>
<ID>SuspendFunSwallowedCancellation:ExchangeStatusFactory.kt$ExchangeStatusFactory$runCatching</ID>
<ID>SuspendFunSwallowedCancellation:TokenDetailsModel.kt$TokenDetailsModel$runCatching</ID>
<ID>UnnecessaryLet:TokenDetailsModel.kt$TokenDetailsModel$let { internalUiState.value = stateFactory.getStateWithErrorDialog(message) }</ID>
<ID>UnnecessaryLet:TokenDetailsSkeletonStateConverter.kt$TokenDetailsSkeletonStateConverter$let(::add)</ID>
<ID>UnnecessaryLet:TokenDetailsStateFactory.kt$TokenDetailsStateFactory$let(::add)</ID>
<ID>UseEmptyCounterpart:TokenDetailsAnalyticsEvent.kt$TokenDetailsAnalyticsEvent$mapOf()</ID>
<ID>UseEmptyCounterpart:TokenDetailsAnalyticsEvent.kt$TokenDetailsAnalyticsEvent.Notice$mapOf()</ID>
<ID>UseOrEmpty:ExchangeStatusFactory.kt$ExchangeStatusFactory$savedTransactions ?.flatMap { setOf(it.fromCryptoCurrency.id, it.toCryptoCurrency.id) } ?.toSet() ?.getQuotesOrEmpty() ?: emptySet()</ID>
<ID>VarCouldBeVal:TokenDetailsModel.kt$TokenDetailsModel$private var expressTxStatusTaskScheduler = SingleTaskScheduler&lt;PersistentList&lt;ExpressTransactionStateUM&gt;&gt;()</ID>
</CurrentIssues>
</SmellBaseline>

View file

@ -9,7 +9,6 @@ import com.tangem.common.routing.deeplink.DeeplinkConst.TRANSACTION_ID_KEY
import com.tangem.common.routing.deeplink.DeeplinkConst.TYPE_KEY
import com.tangem.common.routing.deeplink.DeeplinkConst.WALLET_ID_KEY
import com.tangem.core.analytics.api.AnalyticsEventHandler
import com.tangem.domain.account.featuretoggle.AccountsFeatureToggles
import com.tangem.domain.card.common.util.cardTypesResolver
import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.domain.models.network.Network
@ -18,7 +17,10 @@ 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.notifications.models.NotificationType
import com.tangem.domain.tokens.*
import com.tangem.domain.tokens.FetchCurrencyStatusUseCase
import com.tangem.domain.tokens.GetCryptoCurrencyUseCase
import com.tangem.domain.tokens.MultiWalletCryptoCurrenciesProducer
import com.tangem.domain.tokens.MultiWalletCryptoCurrenciesSupplier
import com.tangem.domain.tokens.wallet.WalletBalanceFetcher
import com.tangem.domain.wallets.usecase.GetUserWalletUseCase
import com.tangem.domain.wallets.usecase.SelectWalletUseCase
@ -40,7 +42,6 @@ internal class DefaultTokenDetailsDeepLinkHandler @AssistedInject constructor(
@Assisted private val isFromOnNewIntent: Boolean,
private val appRouter: AppRouter,
private val selectWalletUseCase: SelectWalletUseCase,
private val getCryptoCurrenciesUseCase: GetCryptoCurrenciesUseCase,
private val getCryptoCurrencyUseCase: GetCryptoCurrencyUseCase,
private val fetchCurrencyStatusUseCase: FetchCurrencyStatusUseCase,
private val tokenDetailsDeepLinkActionTrigger: TokenDetailsDeepLinkActionTrigger,
@ -48,7 +49,6 @@ internal class DefaultTokenDetailsDeepLinkHandler @AssistedInject constructor(
private val analyticsEventHandler: AnalyticsEventHandler,
private val getUserWalletUseCase: GetUserWalletUseCase,
private val walletBalanceFetcher: WalletBalanceFetcher,
private val accountsFeatureToggles: AccountsFeatureToggles,
private val tangemPayFeatureToggles: TangemPayFeatureToggles,
private val multiWalletCryptoCurrenciesSupplier: MultiWalletCryptoCurrenciesSupplier,
) : TokenDetailsDeepLinkHandler {
@ -142,12 +142,12 @@ internal class DefaultTokenDetailsDeepLinkHandler @AssistedInject constructor(
if (userWallet.isMultiCurrency) {
val derivationPath = queryParams[DERIVATION_PATH_KEY]
getCryptoCurrencies(userWalletId = userWallet.walletId)?.firstOrNull {
val isNetwork = it.network.backendId.equals(networkId, ignoreCase = true)
val isCurrency = it.id.rawCurrencyId?.value?.equals(tokenId, ignoreCase = true) == true
getCryptoCurrencies(userWalletId = userWallet.walletId)?.firstOrNull { currency ->
val isNetwork = currency.network.backendId.equals(networkId, ignoreCase = true)
val isCurrency = currency.id.rawCurrencyId?.value?.equals(tokenId, ignoreCase = true) == true
val isDefaultDerivation = it.network.derivationPath is Network.DerivationPath.Card
val isCustomDerivation = derivationPath?.equals(it.network.derivationPath.value) == true
val isDefaultDerivation = currency.network.derivationPath is Network.DerivationPath.Card
val isCustomDerivation = derivationPath?.equals(currency.network.derivationPath.value) == true
val isCorrectDerivation = isDefaultDerivation || isCustomDerivation
isNetwork && isCurrency && isCorrectDerivation
}
@ -156,14 +156,9 @@ internal class DefaultTokenDetailsDeepLinkHandler @AssistedInject constructor(
}
private suspend fun getCryptoCurrencies(userWalletId: UserWalletId): List<CryptoCurrency>? {
return if (accountsFeatureToggles.isFeatureEnabled) {
multiWalletCryptoCurrenciesSupplier.getSyncOrNull(
params = MultiWalletCryptoCurrenciesProducer.Params(userWalletId = userWalletId),
)
?.toList()
} else {
getCryptoCurrenciesUseCase(userWalletId = userWalletId).getOrNull()
}
return multiWalletCryptoCurrenciesSupplier.getSyncOrNull(
params = MultiWalletCryptoCurrenciesProducer.Params(userWalletId = userWalletId),
)?.toList()
}
@AssistedFactory

View file

@ -2,24 +2,20 @@ package com.tangem.feature.tokendetails.presentation.tokendetails.model
import androidx.compose.runtime.Stable
import arrow.core.getOrElse
import arrow.core.right
import com.tangem.common.ui.expressStatus.ExpressStatusBottomSheetConfig
import com.tangem.common.ui.expressStatus.state.ExpressTransactionStateUM
import com.tangem.common.ui.expressStatus.state.ExpressTransactionsBlockState
import com.tangem.core.decompose.di.ModelScoped
import com.tangem.core.decompose.model.Model
import com.tangem.core.decompose.model.ParamsContainer
import com.tangem.domain.account.featuretoggle.AccountsFeatureToggles
import com.tangem.domain.account.status.usecase.GetAccountCurrencyStatusUseCase
import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase
import com.tangem.domain.appcurrency.model.AppCurrency
import com.tangem.domain.card.common.util.cardTypesResolver
import com.tangem.domain.models.account.Account
import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.domain.models.currency.CryptoCurrencyStatus
import com.tangem.domain.models.wallet.UserWallet
import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.domain.tokens.GetSingleCryptoCurrencyStatusUseCase
import com.tangem.domain.wallets.usecase.GetUserWalletUseCase
import com.tangem.feature.tokendetails.presentation.router.InnerTokenDetailsRouter
import com.tangem.feature.tokendetails.presentation.tokendetails.state.factory.ExpressStateFactory
@ -43,10 +39,8 @@ internal class ExpressTransactionsModel @Inject constructor(
paramsContainer: ParamsContainer,
expressStatusFactory: ExpressStatusFactory.Factory,
getUserWalletUseCase: GetUserWalletUseCase,
private val getSingleCryptoCurrencyStatusUseCase: GetSingleCryptoCurrencyStatusUseCase,
private val getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase,
private val router: InnerTokenDetailsRouter,
private val accountsFeatureToggles: AccountsFeatureToggles,
private val getAccountCryptoCurrencyStatusUseCase: GetAccountCurrencyStatusUseCase,
private val expressTransactionsEventListener: ExpressTransactionsEventListener,
) : Model(), ExpressTransactionsClickIntents {
@ -163,22 +157,10 @@ internal class ExpressTransactionsModel @Inject constructor(
}
private fun subscribeOnCurrencyStatusUpdates() {
if (accountsFeatureToggles.isFeatureEnabled) {
getAccountCryptoCurrencyStatusUseCase(userWalletId, cryptoCurrency)
.onEach { account = it.account }
.map { it.status.right() }
} else {
getSingleCryptoCurrencyStatusUseCase.invokeMultiWallet(
userWalletId = userWalletId,
currencyId = cryptoCurrency.id,
isSingleWalletWithTokens = userWallet is UserWallet.Cold &&
userWallet.scanResponse.cardTypesResolver.isSingleWalletWithToken(),
)
}
getAccountCryptoCurrencyStatusUseCase(userWalletId, cryptoCurrency)
.onEach { account = it.account }
.distinctUntilChanged()
.onEach { maybeCurrencyStatus ->
maybeCurrencyStatus.onRight { status -> cryptoCurrencyStatus = status }
}
.onEach { cryptoCurrencyStatus = it.status }
.flowOn(dispatchers.main)
.launchIn(modelScope)
.saveIn(marketPriceJobHolder)

View file

@ -33,7 +33,6 @@ import com.tangem.core.ui.extensions.wrappedList
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.featuretoggle.AccountsFeatureToggles
import com.tangem.domain.account.status.usecase.GetAccountCurrencyStatusUseCase
import com.tangem.domain.account.status.usecase.ManageCryptoCurrenciesUseCase
import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase
@ -105,12 +104,11 @@ import kotlinx.coroutines.launch
import timber.log.Timber
import javax.inject.Inject
@Suppress("LongParameterList", "LargeClass", "TooManyFunctions")
@Suppress("LongParameterList", "LargeClass", "TooManyFunctions", "PropertyUsedBeforeDeclaration")
@Stable
@ModelScoped
internal class TokenDetailsModel @Inject constructor(
override val dispatchers: CoroutineDispatcherProvider,
private val getSingleCryptoCurrencyStatusUseCase: GetSingleCryptoCurrencyStatusUseCase,
private val getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase,
private val fetchCurrencyStatusUseCase: FetchCurrencyStatusUseCase,
private val getExploreUrlUseCase: GetExploreUrlUseCase,
@ -148,7 +146,6 @@ internal class TokenDetailsModel @Inject constructor(
private val saveViewedYieldSupplyWarningUseCase: SaveViewedYieldSupplyWarningUseCase,
private val saveViewedTokenReceiveWarningUseCase: SaveViewedTokenReceiveWarningUseCase,
private val needShowYieldSupplyDepositedWarningUseCase: NeedShowYieldSupplyDepositedWarningUseCase,
private val accountsFeatureToggles: AccountsFeatureToggles,
private val getAccountCryptoCurrencyStatusUseCase: GetAccountCurrencyStatusUseCase,
private val manageCryptoCurrenciesUseCase: ManageCryptoCurrenciesUseCase,
private val yieldSupplyGetRewardsBalanceUseCase: YieldSupplyGetRewardsBalanceUseCase,
@ -177,7 +174,7 @@ internal class TokenDetailsModel @Inject constructor(
private var cryptoCurrencyStatus: CryptoCurrencyStatus? = null
private var account: Account.CryptoPortfolio? = null
private var isBalanceLoadedEventSent = false
private var expressTxStatusTaskScheduler = SingleTaskScheduler<PersistentList<ExpressTransactionStateUM>>()
private val expressTxStatusTaskScheduler = SingleTaskScheduler<PersistentList<ExpressTransactionStateUM>>()
/** Transaction id to check for status */
private val waitForFirstExpressStatusEmmit = MutableStateFlow(false)
@ -265,21 +262,13 @@ internal class TokenDetailsModel @Inject constructor(
private fun initButtons() {
// we need also init buttons before start all loading to avoid buttons blocking
modelScope.launch {
val currentCryptoCurrencyStatus = if (accountsFeatureToggles.isFeatureEnabled) {
getAccountCryptoCurrencyStatusUseCase.invokeSync(
userWalletId = userWalletId,
currency = cryptoCurrency,
)
.onSome { account = it.account }
.getOrNull()
?.status
} else {
getSingleCryptoCurrencyStatusUseCase.invokeMultiWalletSync(
userWalletId = userWalletId,
cryptoCurrencyId = cryptoCurrency.id,
isSingleWalletWithTokens = false,
).getOrNull()
}
val currentCryptoCurrencyStatus = getAccountCryptoCurrencyStatusUseCase.invokeSync(
userWalletId = userWalletId,
currency = cryptoCurrency,
)
.onSome { account = it.account }
.getOrNull()
?.status
currentCryptoCurrencyStatus?.let { status ->
cryptoCurrencyStatus = status
@ -296,9 +285,9 @@ internal class TokenDetailsModel @Inject constructor(
private fun handleBalanceHiding() {
getBalanceHidingSettingsUseCase()
.onEach {
.onEach { settings ->
internalUiState.value = stateFactory.getStateWithUpdatedHidden(
isBalanceHidden = it.isBalanceHidden,
isBalanceHidden = settings.isBalanceHidden,
)
}
.launchIn(modelScope)
@ -311,9 +300,9 @@ internal class TokenDetailsModel @Inject constructor(
)
.conflate()
.distinctUntilChanged()
.onEach {
sendButtonsEvents(it.states)
internalUiState.value = stateFactory.getManageButtonsState(actions = it.states)
.onEach { state ->
sendButtonsEvents(state.states)
internalUiState.value = stateFactory.getManageButtonsState(actions = state.states)
}
.flowOn(dispatchers.main)
.launchIn(modelScope)
@ -345,8 +334,8 @@ internal class TokenDetailsModel @Inject constructor(
userWallet.scanResponse.cardTypesResolver.isSingleWalletWithToken(),
)
.distinctUntilChanged()
.onEach {
val updatedState = stateFactory.getStateWithNotifications(it)
.onEach { warnings ->
val updatedState = stateFactory.getStateWithNotifications(warnings)
notificationsAnalyticsSender.send(internalUiState.value, updatedState.notifications)
internalUiState.value = updatedState
}
@ -356,18 +345,9 @@ internal class TokenDetailsModel @Inject constructor(
}
private fun subscribeOnCurrencyStatusUpdates() {
if (accountsFeatureToggles.isFeatureEnabled) {
getAccountCryptoCurrencyStatusUseCase(userWalletId, cryptoCurrency)
.onEach { account = it.account }
.map { it.status.right() }
} else {
getSingleCryptoCurrencyStatusUseCase.invokeMultiWallet(
userWalletId = userWalletId,
currencyId = cryptoCurrency.id,
isSingleWalletWithTokens = userWallet is UserWallet.Cold &&
userWallet.scanResponse.cardTypesResolver.isSingleWalletWithToken(),
)
}
getAccountCryptoCurrencyStatusUseCase(userWalletId, cryptoCurrency)
.onEach { account = it.account }
.map { it.status.right() }
.distinctUntilChanged()
.onEach { maybeCurrencyStatus ->
internalUiState.value = stateFactory.getCurrencyLoadedBalanceState(maybeCurrencyStatus)
@ -402,7 +382,7 @@ internal class TokenDetailsModel @Inject constructor(
isDelayFirst = false,
delay = EXPRESS_STATUS_UPDATE_DELAY,
task = {
runCatching {
runSuspendCatching {
expressStatusFactory.getUpdatedExpressStatuses(internalUiState.value.expressTxs)
}
},
@ -508,10 +488,10 @@ internal class TokenDetailsModel @Inject constructor(
userWalletId = userWalletId,
network = cryptoCurrency.network,
)
.mapLeft {
.mapLeft { throwable ->
analyticsExceptionHandler.sendException(
event = ExceptionAnalyticsEvent(
exception = it,
exception = throwable,
params = mapOf(
"blockchainId" to cryptoCurrency.network.id.rawId.value,
"networkId" to cryptoCurrency.network.backendId,
@ -520,7 +500,7 @@ internal class TokenDetailsModel @Inject constructor(
)
Timber.e(
/* t = */ it,
/* t = */ throwable,
/* message = */ "Unable to get wallet manager for user wallet %s and network %s",
/* ...args = */ userWalletId,
cryptoCurrency.network,
@ -674,8 +654,8 @@ internal class TokenDetailsModel @Inject constructor(
userWalletId,
cryptoCurrency.network,
).fold(
ifLeft = {
Timber.e(it.cause?.localizedMessage.orEmpty())
ifLeft = { throwable ->
Timber.e(throwable.cause?.localizedMessage.orEmpty())
""
},
ifRight = { it },
@ -976,9 +956,9 @@ internal class TokenDetailsModel @Inject constructor(
}
}
}
message?.let {
internalUiState.value = stateFactory.getStateWithErrorDialog(stringReference(it))
Timber.e(it)
if (message != null) {
internalUiState.value = stateFactory.getStateWithErrorDialog(stringReference(message))
Timber.e(message)
}
},
ifRight = {
@ -1019,7 +999,10 @@ internal class TokenDetailsModel @Inject constructor(
is SendTransactionError.UnknownError -> error.ex?.localizedMessage
}?.let { stringReference(it) }
}
message?.let { internalUiState.value = stateFactory.getStateWithErrorDialog(message) }
if (message != null) {
internalUiState.value = stateFactory.getStateWithErrorDialog(message)
}
},
ifRight = { internalUiState.value = stateFactory.getStateWithRemovedRequiredTrustlineNotification() },
)

View file

@ -1,5 +0,0 @@
<?xml version="1.0" ?>
<SmellBaseline>
<ManuallySuppressedIssues/>
<CurrentIssues/>
</SmellBaseline>

View file

@ -22,7 +22,6 @@ import com.tangem.core.ui.message.DialogMessage
import com.tangem.core.ui.message.EventMessageAction
import com.tangem.core.ui.utils.requestPermission
import com.tangem.datasource.local.accounts.AccountTokenMigrationStore
import com.tangem.domain.account.featuretoggle.AccountsFeatureToggles
import com.tangem.feature.walletsettings.component.NetworksAvailableForNotificationsComponent
import com.tangem.feature.walletsettings.component.RenameWalletComponent
import com.tangem.feature.walletsettings.component.WalletSettingsComponent
@ -49,7 +48,6 @@ internal class DefaultWalletSettingsComponent @AssistedInject constructor(
private val renameWalletComponentFactory: RenameWalletComponent.Factory,
private val networksAvailableForNotificationsComponent: NetworksAvailableForNotificationsComponent.Factory,
private val accountTokenMigrationStore: AccountTokenMigrationStore,
private val accountsFeatureToggles: AccountsFeatureToggles,
) : WalletSettingsComponent, AppComponentContext by context {
private val model: WalletSettingsModel = getOrCreateModel(params)
@ -74,11 +72,7 @@ internal class DefaultWalletSettingsComponent @AssistedInject constructor(
init {
lifecycle.subscribe(
onResume = {
if (accountsFeatureToggles.isFeatureEnabled) {
showMigrationAlertIfNeeded()
}
},
onResume = { showMigrationAlertIfNeeded() },
onPause = { accountMigrationJobHolder.cancel() },
)
}

View file

@ -12,7 +12,6 @@ import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.extensions.wrappedList
import com.tangem.core.ui.message.DialogMessage
import com.tangem.core.ui.message.EventMessageAction
import com.tangem.domain.account.featuretoggle.AccountsFeatureToggles
import com.tangem.domain.account.models.AccountList
import com.tangem.domain.account.models.AccountStatusList
import com.tangem.domain.account.status.supplier.SingleAccountStatusListSupplier
@ -46,13 +45,12 @@ internal class AccountItemsDelegate @Inject constructor(
private val getBalanceHidingSettingsUseCase: GetBalanceHidingSettingsUseCase,
private val getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase,
private val accountListSortingSaver: AccountListSortingSaver,
private val accountsFeatureToggles: AccountsFeatureToggles,
private val analyticsEventHandler: AnalyticsEventHandler,
) {
private val userWalletId = paramsContainer.require<WalletSettingsComponent.Params>().userWalletId
fun isAccountsSupported(wallet: UserWallet) = accountsFeatureToggles.isFeatureEnabled && wallet.isAccountsSupported
fun isAccountsSupported(wallet: UserWallet) = wallet.isAccountsSupported
fun loadAccount(wallet: UserWallet): Flow<List<WalletSettingsAccountsUM>> {
if (!isAccountsSupported(wallet)) return flowOf(emptyList())

View file

@ -11,7 +11,7 @@ import dagger.assisted.AssistedInject
internal class MultiWalletContentLoader @AssistedInject constructor(
@Assisted private val userWallet: UserWallet,
private val accountListSubscriberFactory: AccountListSubscriber.Factory,
private val walletNFTListSubscriberFactory: WalletNFTListSubscriberV2.Factory,
private val walletNFTListSubscriberFactory: WalletNFTListSubscriber.Factory,
private val checkWalletWithFundsSubscriberFactory: CheckWalletWithFundsSubscriber.Factory,
private val multiWalletWarningsSubscriberFactory: MultiWalletWarningsSubscriber.Factory,
private val walletNotificationsSubscriberFactory: WalletNotificationsSubscriber.Factory,

View file

@ -15,7 +15,7 @@ import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.*
internal class WalletNFTListSubscriberV2 @AssistedInject constructor(
internal class WalletNFTListSubscriber @AssistedInject constructor(
@Assisted override val userWallet: UserWallet,
override val singleAccountStatusListSupplier: SingleAccountStatusListSupplier,
private val walletsRepository: WalletsRepository,
@ -35,7 +35,7 @@ internal class WalletNFTListSubscriberV2 @AssistedInject constructor(
// if NFT is enabled for this wallet and there are currencies,
// then start observing changes from store and apply transformer if need
if (nftEnabled && currencies.isNotEmpty()) {
getNFTCollectionsUseCase.invokeForAccounts(userWallet.walletId)
getNFTCollectionsUseCase(userWallet.walletId)
.shareIn(
scope = coroutineScope,
started = SharingStarted.WhileSubscribed(),
@ -61,6 +61,6 @@ internal class WalletNFTListSubscriberV2 @AssistedInject constructor(
@AssistedFactory
interface Factory {
fun create(userWallet: UserWallet): WalletNFTListSubscriberV2
fun create(userWallet: UserWallet): WalletNFTListSubscriber
}
}