From a6d0b50f43da9378292e2ff9a0b0f1c631ff588c Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 5 May 2025 20:09:31 +0500 Subject: [PATCH 001/165] Updated on 2026-08-14 --- .../tap/di/domain/WalletsDomainModule.kt | 34 ++++++++- .../api/tangemTech/TangemTechApi.kt | 3 + .../DefaultNotificationsRepository.kt | 28 +++---- .../DefaultNotificationsRepositoryTest.kt | 76 +++++++------------ .../data/wallets/DefaultWalletsRepository.kt | 34 +++++++++ .../UserWalletRemoteInfoConverter.kt | 16 ++++ .../wallets/DefaultWalletsRepositoryTest.kt | 58 ++++++++++++++ .../notifications/models/ApplicationId.kt | 4 + .../notifications/GetApplicationIdUseCase.kt | 3 +- .../repository/NotificationsRepository.kt | 15 ++-- .../GetApplicationIdUseCaseTest.kt | 9 ++- .../notifications/SendPushTokenUseCaseTest.kt | 5 +- domain/wallets/build.gradle.kts | 8 ++ .../wallets/models/UserWalletRemoteInfo.kt | 7 ++ .../DefaultUserWalletsSyncDelegate.kt | 55 ++++++++++++++ .../delegate/UserWalletsSyncDelegate.kt | 14 ++++ .../wallets/repository/WalletsRepository.kt | 11 +++ .../wallets/usecase/RenameWalletUseCase.kt | 33 +++----- .../usecase/UpdateRemoteWalletsInfoUseCase.kt | 19 +++++ 19 files changed, 319 insertions(+), 113 deletions(-) create mode 100644 data/wallets/src/main/java/com/tangem/data/wallets/converters/UserWalletRemoteInfoConverter.kt create mode 100644 domain/notifications/models/src/main/java/com/tangem/domain/notifications/models/ApplicationId.kt create mode 100644 domain/wallets/models/src/main/java/com/tangem/domain/wallets/models/UserWalletRemoteInfo.kt create mode 100644 domain/wallets/src/main/java/com/tangem/domain/wallets/delegate/DefaultUserWalletsSyncDelegate.kt create mode 100644 domain/wallets/src/main/java/com/tangem/domain/wallets/delegate/UserWalletsSyncDelegate.kt create mode 100644 domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/UpdateRemoteWalletsInfoUseCase.kt diff --git a/app/src/main/java/com/tangem/tap/di/domain/WalletsDomainModule.kt b/app/src/main/java/com/tangem/tap/di/domain/WalletsDomainModule.kt index 4096521b75..b153b0e0de 100644 --- a/app/src/main/java/com/tangem/tap/di/domain/WalletsDomainModule.kt +++ b/app/src/main/java/com/tangem/tap/di/domain/WalletsDomainModule.kt @@ -6,6 +6,8 @@ import com.tangem.domain.transaction.usecase.ParseSharedAddressUseCase import com.tangem.domain.transaction.usecase.ValidateWalletAddressUseCase import com.tangem.domain.transaction.usecase.ValidateWalletMemoUseCase import com.tangem.domain.walletmanager.WalletManagersFacade +import com.tangem.domain.wallets.delegate.DefaultUserWalletsSyncDelegate +import com.tangem.domain.wallets.delegate.UserWalletsSyncDelegate import com.tangem.domain.wallets.legacy.UserWalletsListManager import com.tangem.domain.wallets.repository.WalletNamesMigrationRepository import com.tangem.domain.wallets.repository.WalletsRepository @@ -28,6 +30,17 @@ import javax.inject.Singleton @InstallIn(SingletonComponent::class) internal object WalletsDomainModule { + @Provides + fun providesUserWalletsSyncDelegate( + userWalletsListManager: UserWalletsListManager, + dispatchers: CoroutineDispatcherProvider, + ): UserWalletsSyncDelegate { + return DefaultUserWalletsSyncDelegate( + userWalletsListManager = userWalletsListManager, + dispatchers = dispatchers, + ) + } + @Provides @Singleton fun providesGetWalletsUseCase(userWalletsListManager: UserWalletsListManager): GetWalletsUseCase { @@ -102,10 +115,13 @@ internal object WalletsDomainModule { @Provides @Singleton fun providesRenameWalletUseCase( - userWalletsListManager: UserWalletsListManager, - dispatchers: CoroutineDispatcherProvider, + walletsRepository: WalletsRepository, + userWalletsSyncDelegate: UserWalletsSyncDelegate, ): RenameWalletUseCase { - return RenameWalletUseCase(userWalletsListManager = userWalletsListManager, dispatchers = dispatchers) + return RenameWalletUseCase( + walletsRepository = walletsRepository, + userWalletsSyncDelegate = userWalletsSyncDelegate, + ) } @Provides @@ -197,4 +213,16 @@ internal object WalletsDomainModule { nftFeatureToggles = nftFeatureToggles, ) } + + @Provides + @Singleton + fun providesUpdateRemoteWalletsInfoUseCase( + walletsRepository: WalletsRepository, + userWalletsSyncDelegate: UserWalletsSyncDelegate, + ): UpdateRemoteWalletsInfoUseCase { + return UpdateRemoteWalletsInfoUseCase( + walletsRepository = walletsRepository, + userWalletsSyncDelegate = userWalletsSyncDelegate, + ) + } } \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/TangemTechApi.kt b/core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/TangemTechApi.kt index 7b474dd442..d4db8f1fa1 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/TangemTechApi.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/TangemTechApi.kt @@ -172,6 +172,9 @@ interface TangemTechApi { @GET("user-wallets/wallets/{wallet_id}") suspend fun getWalletById(@Path("wallet_id") walletId: String): ApiResponse + + @GET("user-wallets/wallets/by-app/{app_id}") + suspend fun getWallets(@Path("app_id") appId: String): ApiResponse> // endregion companion object { diff --git a/data/notifications/src/main/java/com/tangem/data/notifications/DefaultNotificationsRepository.kt b/data/notifications/src/main/java/com/tangem/data/notifications/DefaultNotificationsRepository.kt index 1470589ef2..62f837994d 100644 --- a/data/notifications/src/main/java/com/tangem/data/notifications/DefaultNotificationsRepository.kt +++ b/data/notifications/src/main/java/com/tangem/data/notifications/DefaultNotificationsRepository.kt @@ -4,12 +4,12 @@ import com.tangem.data.notifications.converters.NotificationsEligibleNetworkConv import com.tangem.datasource.api.common.response.getOrThrow import com.tangem.datasource.api.tangemTech.TangemTechApi import com.tangem.datasource.api.tangemTech.models.NotificationApplicationCreateBody -import com.tangem.datasource.api.tangemTech.models.WalletBody import com.tangem.datasource.api.tangemTech.models.WalletIdBody import com.tangem.utils.info.AppInfoProvider import com.tangem.datasource.local.preferences.AppPreferencesStore import com.tangem.datasource.local.preferences.PreferencesKeys import com.tangem.datasource.local.preferences.utils.* +import com.tangem.domain.notifications.models.ApplicationId import com.tangem.domain.notifications.repository.NotificationsRepository import com.tangem.domain.notifications.models.NotificationsEligibleNetwork import com.tangem.utils.coroutines.CoroutineDispatcherProvider @@ -23,7 +23,7 @@ internal class DefaultNotificationsRepository @Inject constructor( private val dispatchers: CoroutineDispatcherProvider, ) : NotificationsRepository { - override suspend fun createApplicationId(pushToken: String?): String = withContext(dispatchers.io) { + override suspend fun createApplicationId(pushToken: String?): ApplicationId = withContext(dispatchers.io) { tangemTechApi.createApplicationId( NotificationApplicationCreateBody( platform = appInfoProvider.platform, @@ -33,15 +33,16 @@ internal class DefaultNotificationsRepository @Inject constructor( timezone = appInfoProvider.timezone, pushToken = pushToken, ), - ).getOrThrow().appId + ).getOrThrow().appId.let(::ApplicationId) } - override suspend fun saveApplicationId(appId: String) { - appPreferencesStore.store(PreferencesKeys.NOTIFICATIONS_APPLICATION_ID_KEY, appId) + override suspend fun saveApplicationId(appId: ApplicationId) { + appPreferencesStore.store(PreferencesKeys.NOTIFICATIONS_APPLICATION_ID_KEY, appId.value) } - override suspend fun getApplicationId(): String? { + override suspend fun getApplicationId(): ApplicationId? { return appPreferencesStore.getSyncOrNull(PreferencesKeys.NOTIFICATIONS_APPLICATION_ID_KEY) + ?.let(::ApplicationId) } override suspend fun incrementTronTokenFeeNotificationShowCounter() { @@ -71,21 +72,10 @@ internal class DefaultNotificationsRepository @Inject constructor( ).getOrThrow() } - override suspend fun setWalletName(walletId: String, walletName: String) = withContext(dispatchers.io) { - tangemTechApi.updateWallet( - walletId, - WalletBody(name = walletName), - ).getOrThrow() - } - - override suspend fun getWalletName(walletId: String): String? = withContext(dispatchers.io) { - tangemTechApi.getWalletById(walletId).getOrThrow().name - } - - override suspend fun sendPushToken(appId: String, pushToken: String) { + override suspend fun sendPushToken(appId: ApplicationId, pushToken: String) { withContext(dispatchers.io) { tangemTechApi.updatePushTokenForApplicationId( - appId, + appId.value, NotificationApplicationCreateBody( pushToken = pushToken, ), diff --git a/data/notifications/src/test/java/com/tangem/data/notifications/DefaultNotificationsRepositoryTest.kt b/data/notifications/src/test/java/com/tangem/data/notifications/DefaultNotificationsRepositoryTest.kt index 305732cde6..57d8d4c429 100644 --- a/data/notifications/src/test/java/com/tangem/data/notifications/DefaultNotificationsRepositoryTest.kt +++ b/data/notifications/src/test/java/com/tangem/data/notifications/DefaultNotificationsRepositoryTest.kt @@ -12,6 +12,7 @@ import com.squareup.moshi.Moshi import com.tangem.data.notifications.converters.NotificationsEligibleNetworkConverter import com.tangem.datasource.api.common.response.ApiResponse import com.tangem.datasource.api.tangemTech.models.* +import com.tangem.domain.notifications.models.ApplicationId import com.tangem.utils.coroutines.TestingCoroutineDispatcherProvider import io.mockk.coEvery import io.mockk.coVerify @@ -41,9 +42,9 @@ class DefaultNotificationsRepositoryTest { fun `GIVEN valid push token WHEN createApplicationId THEN returns application id`() = runTest { // GIVEN val pushToken = "test-push-token" - val expectedAppId = "test-app-id" + val expectedAppId = ApplicationId("test-app-id") val expectedAppIdResponse = NotificationApplicationIdResponse( - appId = expectedAppId, + appId = expectedAppId.value, ) coEvery { appInfoProvider.platform } returns "android" coEvery { appInfoProvider.device } returns "test-device" @@ -76,7 +77,7 @@ class DefaultNotificationsRepositoryTest { @Test fun `GIVEN application id WHEN saveApplicationId THEN stores it in preferences`() = runTest { // GIVEN - val appId = "test-app-id" + val appId = ApplicationId("test-app-id") val preferences = mockk(relaxed = true) coEvery { preferencesDataStore.updateData(any()) } returns preferences @@ -90,10 +91,10 @@ class DefaultNotificationsRepositoryTest { @Test fun `GIVEN stored application id WHEN getApplicationId THEN returns it`() = runTest { // GIVEN - val expectedAppId = "test-app-id" + val expectedAppId = ApplicationId("test-app-id") val preferences = mockk(relaxed = true) val key = stringPreferencesKey(PreferencesKeys.NOTIFICATIONS_APPLICATION_ID_KEY.name) - every { preferences[key] } returns expectedAppId + every { preferences[key] } returns expectedAppId.value coEvery { preferencesDataStore.data } returns flowOf(preferences) // WHEN @@ -122,56 +123,24 @@ class DefaultNotificationsRepositoryTest { coVerify { tangemTechApi.associateApplicationIdWithWallets(appId, wallets.map { WalletIdBody(it) }) } } - @Test - fun `GIVEN wallet id and name WHEN setWalletName THEN updates wallet name`() = runTest { - // GIVEN - val walletId = "test-wallet-id" - val walletName = "Test Wallet" - coEvery { - tangemTechApi.updateWallet( - walletId, - WalletBody(name = walletName), - ) - } returns ApiResponse.Success(Unit) - - // WHEN - repository.setWalletName(walletId, walletName) - - // THEN - coVerify { tangemTechApi.updateWallet(walletId, WalletBody(name = walletName)) } - } - - @Test - fun `GIVEN wallet id WHEN getWalletName THEN returns wallet name`() = runTest { - // GIVEN - val walletId = "test-wallet-id" - val expectedName = "Test Wallet" - coEvery { tangemTechApi.getWalletById(walletId) } returns ApiResponse.Success( - WalletResponse( - notifyStatus = false, - name = expectedName, - id = walletId, - ), - ) - - // WHEN - val result = repository.getWalletName(walletId) - - // THEN - assertThat(result).isEqualTo(expectedName) - } - @Test fun `GIVEN application id and push token WHEN sendPushToken THEN updates push token`() = runTest { // GIVEN - val appId = "test-app-id" + val appId = ApplicationId("test-app-id") val pushToken = "test-push-token" coEvery { tangemTechApi.updatePushTokenForApplicationId( - appId, - NotificationApplicationCreateBody(pushToken = pushToken), + appId.value, + NotificationApplicationCreateBody( + pushToken = pushToken, + platform = null, + device = null, + systemVersion = null, + language = null, + timezone = null, + ), ) - } returns ApiResponse.Success(appId) + } returns ApiResponse.Success(appId.value) // WHEN repository.sendPushToken(appId, pushToken) @@ -179,8 +148,15 @@ class DefaultNotificationsRepositoryTest { // THEN coVerify { tangemTechApi.updatePushTokenForApplicationId( - appId, - NotificationApplicationCreateBody(pushToken = pushToken), + appId.value, + NotificationApplicationCreateBody( + pushToken = pushToken, + platform = null, + device = null, + systemVersion = null, + language = null, + timezone = null, + ), ) } } diff --git a/data/wallets/src/main/java/com/tangem/data/wallets/DefaultWalletsRepository.kt b/data/wallets/src/main/java/com/tangem/data/wallets/DefaultWalletsRepository.kt index 39096c2551..25bc5ae0b6 100644 --- a/data/wallets/src/main/java/com/tangem/data/wallets/DefaultWalletsRepository.kt +++ b/data/wallets/src/main/java/com/tangem/data/wallets/DefaultWalletsRepository.kt @@ -1,5 +1,6 @@ package com.tangem.data.wallets +import com.tangem.data.wallets.converters.UserWalletRemoteInfoConverter import com.tangem.datasource.api.common.response.ApiResponseError.HttpException import com.tangem.datasource.api.common.response.getOrThrow import com.tangem.datasource.api.tangemTech.TangemTechApi @@ -18,6 +19,7 @@ import com.tangem.datasource.local.preferences.utils.store import com.tangem.datasource.local.userwallet.UserWalletsStore import com.tangem.domain.wallets.models.SeedPhraseNotificationsStatus import com.tangem.domain.wallets.models.UserWalletId +import com.tangem.domain.wallets.models.UserWalletRemoteInfo import com.tangem.domain.wallets.repository.WalletsRepository import com.tangem.utils.WEEK_MILLIS import com.tangem.utils.coroutines.CoroutineDispatcherProvider @@ -28,6 +30,7 @@ import kotlinx.coroutines.withContext typealias SeedPhraseNotificationsStatuses = Map +@Suppress("TooManyFunctions") internal class DefaultWalletsRepository( private val appPreferencesStore: AppPreferencesStore, private val tangemTechApi: TangemTechApi, @@ -254,6 +257,37 @@ internal class DefaultWalletsRepository( } } + override suspend fun setWalletName(walletId: String, walletName: String) = withContext(dispatchers.io) { + tangemTechApi.updateWallet( + walletId = walletId, + body = WalletBody(name = walletName), + ).getOrThrow() + } + + override suspend fun getWalletInfo(walletId: String): UserWalletRemoteInfo = withContext(dispatchers.io) { + UserWalletRemoteInfoConverter.convert( + value = tangemTechApi.getWalletById(walletId).getOrThrow(), + ) + } + + override suspend fun getWalletsInfo(applicationId: String, updateCache: Boolean): List = + withContext(dispatchers.io) { + tangemTechApi.getWallets(applicationId) + .getOrThrow() + .map { walletInfo -> + val userWallet = UserWalletRemoteInfoConverter.convert( + value = walletInfo, + ) + if (updateCache) { + setNotificationsEnabledLocally( + userWalletId = userWallet.walletId, + isEnabled = userWallet.isNotificationsEnabled, + ) + } + userWallet + } + } + private suspend fun loadAndSaveNotificationsEnabled(userWalletId: UserWalletId): Boolean { val walletResponse = tangemTechApi.getWalletById(walletId = userWalletId.stringValue).getOrThrow() val isEnabled = walletResponse.notifyStatus diff --git a/data/wallets/src/main/java/com/tangem/data/wallets/converters/UserWalletRemoteInfoConverter.kt b/data/wallets/src/main/java/com/tangem/data/wallets/converters/UserWalletRemoteInfoConverter.kt new file mode 100644 index 0000000000..4ac595663e --- /dev/null +++ b/data/wallets/src/main/java/com/tangem/data/wallets/converters/UserWalletRemoteInfoConverter.kt @@ -0,0 +1,16 @@ +package com.tangem.data.wallets.converters + +import com.tangem.datasource.api.tangemTech.models.WalletResponse +import com.tangem.domain.wallets.models.UserWalletId +import com.tangem.domain.wallets.models.UserWalletRemoteInfo +import com.tangem.utils.converter.Converter + +internal object UserWalletRemoteInfoConverter : Converter { + override fun convert(value: WalletResponse): UserWalletRemoteInfo { + return UserWalletRemoteInfo( + walletId = UserWalletId(value.id), + name = value.name.orEmpty(), + isNotificationsEnabled = value.notifyStatus, + ) + } +} \ No newline at end of file diff --git a/data/wallets/src/test/java/com/tangem/data/wallets/DefaultWalletsRepositoryTest.kt b/data/wallets/src/test/java/com/tangem/data/wallets/DefaultWalletsRepositoryTest.kt index 28f07454c2..f430dab62f 100644 --- a/data/wallets/src/test/java/com/tangem/data/wallets/DefaultWalletsRepositoryTest.kt +++ b/data/wallets/src/test/java/com/tangem/data/wallets/DefaultWalletsRepositoryTest.kt @@ -173,4 +173,62 @@ class DefaultWalletsRepositoryTest { } 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 + val applicationId = "test_app_id" + val wallet1Id = "1234567890abcdef" + val wallet2Id = "fedcba0987654321" + val walletResponses = listOf( + 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 + val result = repository.getWalletsInfo(applicationId, updateCache = true) + + // THEN + assertThat(result).hasSize(2) + assertThat(result[0].walletId.stringValue).isEqualTo(wallet1Id) + assertThat(result[0].isNotificationsEnabled).isTrue() + assertThat(result[1].walletId.stringValue).isEqualTo(wallet2Id) + assertThat(result[1].isNotificationsEnabled).isFalse() + + coVerify(exactly = 1) { tangemTechApi.getWallets(applicationId) } + 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 + val applicationId = "test_app_id" + val wallet1Id = "1234567890abcdef" + val walletResponses = listOf( + WalletResponse( + id = wallet1Id, + notifyStatus = true, + ), + ) + coEvery { tangemTechApi.getWallets(applicationId) } returns ApiResponse.Success(walletResponses) + + // WHEN + val result = repository.getWalletsInfo(applicationId, updateCache = false) + + // THEN + assertThat(result).hasSize(1) + assertThat(result[0].walletId.stringValue).isEqualTo(wallet1Id) + assertThat(result[0].isNotificationsEnabled).isTrue() + + coVerify(exactly = 1) { tangemTechApi.getWallets(applicationId) } + coVerify(exactly = 0) { preferencesDataStore.updateData(any()) } + } } \ No newline at end of file diff --git a/domain/notifications/models/src/main/java/com/tangem/domain/notifications/models/ApplicationId.kt b/domain/notifications/models/src/main/java/com/tangem/domain/notifications/models/ApplicationId.kt new file mode 100644 index 0000000000..62db4696b3 --- /dev/null +++ b/domain/notifications/models/src/main/java/com/tangem/domain/notifications/models/ApplicationId.kt @@ -0,0 +1,4 @@ +package com.tangem.domain.notifications.models + +@JvmInline +value class ApplicationId(val value: String) \ No newline at end of file diff --git a/domain/notifications/src/main/java/com/tangem/domain/notifications/GetApplicationIdUseCase.kt b/domain/notifications/src/main/java/com/tangem/domain/notifications/GetApplicationIdUseCase.kt index 026d3a89c6..963d3b99d7 100644 --- a/domain/notifications/src/main/java/com/tangem/domain/notifications/GetApplicationIdUseCase.kt +++ b/domain/notifications/src/main/java/com/tangem/domain/notifications/GetApplicationIdUseCase.kt @@ -1,6 +1,7 @@ package com.tangem.domain.notifications import arrow.core.Either +import com.tangem.domain.notifications.models.ApplicationId import com.tangem.domain.notifications.repository.NotificationsRepository import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.withLock @@ -10,7 +11,7 @@ class GetApplicationIdUseCase( ) { private val mutex = Mutex() - suspend operator fun invoke(): Either = Either.catch { + suspend operator fun invoke(): Either = Either.catch { val localApplicationId = notificationsRepository.getApplicationId() if (localApplicationId != null) return@catch localApplicationId diff --git a/domain/notifications/src/main/java/com/tangem/domain/notifications/repository/NotificationsRepository.kt b/domain/notifications/src/main/java/com/tangem/domain/notifications/repository/NotificationsRepository.kt index b7f53ab51e..d5e99ab745 100644 --- a/domain/notifications/src/main/java/com/tangem/domain/notifications/repository/NotificationsRepository.kt +++ b/domain/notifications/src/main/java/com/tangem/domain/notifications/repository/NotificationsRepository.kt @@ -1,15 +1,16 @@ package com.tangem.domain.notifications.repository +import com.tangem.domain.notifications.models.ApplicationId import com.tangem.domain.notifications.models.NotificationsEligibleNetwork interface NotificationsRepository { @Throws - suspend fun createApplicationId(pushToken: String? = null): String + suspend fun createApplicationId(pushToken: String? = null): ApplicationId - suspend fun saveApplicationId(appId: String) + suspend fun saveApplicationId(appId: ApplicationId) - suspend fun getApplicationId(): String? + suspend fun getApplicationId(): ApplicationId? suspend fun getTronTokenFeeNotificationShowCounter(): Int @@ -19,13 +20,7 @@ interface NotificationsRepository { suspend fun associateApplicationIdWithWallets(appId: String, wallets: List) @Throws - suspend fun setWalletName(walletId: String, walletName: String) - - @Throws - suspend fun getWalletName(walletId: String): String? - - @Throws - suspend fun sendPushToken(appId: String, pushToken: String) + suspend fun sendPushToken(appId: ApplicationId, pushToken: String) @Throws suspend fun getEligibleNetworks(): List diff --git a/domain/notifications/src/test/java/com/tangem/domain/notifications/GetApplicationIdUseCaseTest.kt b/domain/notifications/src/test/java/com/tangem/domain/notifications/GetApplicationIdUseCaseTest.kt index 350f5bad73..6b2984554b 100644 --- a/domain/notifications/src/test/java/com/tangem/domain/notifications/GetApplicationIdUseCaseTest.kt +++ b/domain/notifications/src/test/java/com/tangem/domain/notifications/GetApplicationIdUseCaseTest.kt @@ -2,6 +2,7 @@ package com.tangem.domain.notifications import arrow.core.Either import com.google.common.truth.Truth.assertThat +import com.tangem.domain.notifications.models.ApplicationId import com.tangem.domain.notifications.repository.NotificationsRepository import io.mockk.coEvery import io.mockk.coVerify @@ -20,7 +21,7 @@ class GetApplicationIdUseCaseTest { @Test fun `GIVEN local application ID exists WHEN invoke THEN return local application ID`() = runTest { // GIVEN - val expectedApplicationId = "test-app-id" + val expectedApplicationId = ApplicationId("test-app-id") coEvery { notificationsRepository.getApplicationId() } returns expectedApplicationId // WHEN @@ -39,7 +40,7 @@ class GetApplicationIdUseCaseTest { @Test fun `GIVEN local application ID does not exist WHEN invoke THEN create and save new application ID`() = runTest { // GIVEN - val newApplicationId = "new-app-id" + val newApplicationId = ApplicationId("new-app-id") coEvery { notificationsRepository.getApplicationId() } returns null coEvery { notificationsRepository.createApplicationId() } returns newApplicationId coEvery { notificationsRepository.saveApplicationId(newApplicationId) } returns Unit @@ -81,7 +82,7 @@ class GetApplicationIdUseCaseTest { fun `GIVEN no local application ID WHEN multiple concurrent invokes THEN create only one application ID`() = runTest { // GIVEN - val newApplicationId = "new-app-id" + val newApplicationId = ApplicationId("new-app-id") var isIdCreated = false coEvery { notificationsRepository.getApplicationId() } answers { @@ -115,7 +116,7 @@ class GetApplicationIdUseCaseTest { @Test fun `GIVEN no local application ID WHEN multiple concurrent invokes with delay THEN create only one application ID`() = runTest { // GIVEN - val newApplicationId = "new-app-id" + val newApplicationId = ApplicationId("new-app-id") var isIdCreated = false coEvery { notificationsRepository.getApplicationId() } answers { diff --git a/domain/notifications/src/test/java/com/tangem/domain/notifications/SendPushTokenUseCaseTest.kt b/domain/notifications/src/test/java/com/tangem/domain/notifications/SendPushTokenUseCaseTest.kt index aeb18310a1..f079a021d1 100644 --- a/domain/notifications/src/test/java/com/tangem/domain/notifications/SendPushTokenUseCaseTest.kt +++ b/domain/notifications/src/test/java/com/tangem/domain/notifications/SendPushTokenUseCaseTest.kt @@ -2,6 +2,7 @@ package com.tangem.domain.notifications import arrow.core.Either import com.google.common.truth.Truth.assertThat +import com.tangem.domain.notifications.models.ApplicationId import com.tangem.domain.notifications.repository.NotificationsRepository import com.tangem.utils.notifications.PushNotificationsTokenProvider import io.mockk.coEvery @@ -33,7 +34,7 @@ class SendPushTokenUseCaseTest { @Test fun `GIVEN valid application ID and token WHEN invoke THEN token is sent successfully`() = runTest { // GIVEN - val applicationId = "test-app-id" + val applicationId = ApplicationId("test-app-id") val token = "test-token" coEvery { getApplicationIdUseCase() } returns Either.Right(applicationId) coEvery { pushNotificationsTokenProvider.getToken() } returns token @@ -62,7 +63,7 @@ class SendPushTokenUseCaseTest { @Test fun `GIVEN repository throws error WHEN invoke THEN returns error`() = runTest { // GIVEN - val applicationId = "test-app-id" + val applicationId = ApplicationId("test-app-id") val token = "test-token" val expectedError = RuntimeException("Network error") coEvery { getApplicationIdUseCase() } returns Either.Right(applicationId) diff --git a/domain/wallets/build.gradle.kts b/domain/wallets/build.gradle.kts index 6ca312b904..331335a257 100644 --- a/domain/wallets/build.gradle.kts +++ b/domain/wallets/build.gradle.kts @@ -26,6 +26,7 @@ dependencies { implementation(projects.domain.tokens) implementation(projects.domain.tokens.models) implementation(projects.domain.wallets.models) + implementation(projects.domain.notifications.models) // endregion // region Tangem libraries @@ -37,4 +38,11 @@ dependencies { implementation(deps.hilt.android) kapt(deps.hilt.kapt) // end + + // region Tests + testImplementation(deps.test.junit) + testImplementation(deps.test.coroutine) + testImplementation(deps.test.truth) + testImplementation(deps.test.mockk) + // end } \ No newline at end of file diff --git a/domain/wallets/models/src/main/java/com/tangem/domain/wallets/models/UserWalletRemoteInfo.kt b/domain/wallets/models/src/main/java/com/tangem/domain/wallets/models/UserWalletRemoteInfo.kt new file mode 100644 index 0000000000..5c6e3669a5 --- /dev/null +++ b/domain/wallets/models/src/main/java/com/tangem/domain/wallets/models/UserWalletRemoteInfo.kt @@ -0,0 +1,7 @@ +package com.tangem.domain.wallets.models + +class UserWalletRemoteInfo( + val walletId: UserWalletId, + val name: String, + val isNotificationsEnabled: Boolean, +) \ No newline at end of file diff --git a/domain/wallets/src/main/java/com/tangem/domain/wallets/delegate/DefaultUserWalletsSyncDelegate.kt b/domain/wallets/src/main/java/com/tangem/domain/wallets/delegate/DefaultUserWalletsSyncDelegate.kt new file mode 100644 index 0000000000..b1c1e11c7e --- /dev/null +++ b/domain/wallets/src/main/java/com/tangem/domain/wallets/delegate/DefaultUserWalletsSyncDelegate.kt @@ -0,0 +1,55 @@ +package com.tangem.domain.wallets.delegate + +import arrow.core.Either +import arrow.core.raise.either +import arrow.core.raise.ensure +import com.tangem.common.CompletionResult +import com.tangem.domain.wallets.legacy.UserWalletsListManager +import com.tangem.domain.wallets.models.UpdateWalletError +import com.tangem.domain.wallets.models.UserWallet +import com.tangem.domain.wallets.models.UserWalletId +import com.tangem.domain.wallets.models.UserWalletRemoteInfo +import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import kotlinx.coroutines.withContext + +class DefaultUserWalletsSyncDelegate( + private val userWalletsListManager: UserWalletsListManager, + private val dispatchers: CoroutineDispatcherProvider, +) : UserWalletsSyncDelegate { + + override suspend fun syncWallet(userWalletId: UserWalletId, name: String): Either { + return renameUserWallet(userWalletId, name) + } + + override suspend fun syncWallets(list: List): Either = either { + list.forEach { userWallet -> + renameUserWallet(userWallet.walletId, userWallet.name).bind() + } + } + + private suspend fun renameUserWallet( + userWalletId: UserWalletId, + name: String, + ): Either = withContext(dispatchers.io) { + either { + val existingNames = userWalletsListManager.userWalletsSync + + ensure(existingNames.none { it.name == name && it.walletId != userWalletId }) { + UpdateWalletError.NameAlreadyExists + } + + val previousName = existingNames.firstOrNull { it.walletId == userWalletId }?.name.orEmpty() + if (previousName == name) { + raise(UpdateWalletError.NameAlreadyExists) + } + + return@withContext when ( + val result = + userWalletsListManager.update(userWalletId) { it.copy(name = name) } + ) { + is CompletionResult.Failure -> raise(UpdateWalletError.DataError(result.error)) + is CompletionResult.Success -> result.data + } + } + } +} \ No newline at end of file diff --git a/domain/wallets/src/main/java/com/tangem/domain/wallets/delegate/UserWalletsSyncDelegate.kt b/domain/wallets/src/main/java/com/tangem/domain/wallets/delegate/UserWalletsSyncDelegate.kt new file mode 100644 index 0000000000..94f72df038 --- /dev/null +++ b/domain/wallets/src/main/java/com/tangem/domain/wallets/delegate/UserWalletsSyncDelegate.kt @@ -0,0 +1,14 @@ +package com.tangem.domain.wallets.delegate + +import arrow.core.Either +import com.tangem.domain.wallets.models.UpdateWalletError +import com.tangem.domain.wallets.models.UserWallet +import com.tangem.domain.wallets.models.UserWalletId +import com.tangem.domain.wallets.models.UserWalletRemoteInfo + +interface UserWalletsSyncDelegate { + + suspend fun syncWallet(userWalletId: UserWalletId, name: String): Either + + suspend fun syncWallets(list: List): Either +} \ No newline at end of file diff --git a/domain/wallets/src/main/java/com/tangem/domain/wallets/repository/WalletsRepository.kt b/domain/wallets/src/main/java/com/tangem/domain/wallets/repository/WalletsRepository.kt index 541e601c54..90768f5e78 100644 --- a/domain/wallets/src/main/java/com/tangem/domain/wallets/repository/WalletsRepository.kt +++ b/domain/wallets/src/main/java/com/tangem/domain/wallets/repository/WalletsRepository.kt @@ -2,8 +2,10 @@ package com.tangem.domain.wallets.repository import com.tangem.domain.wallets.models.SeedPhraseNotificationsStatus import com.tangem.domain.wallets.models.UserWalletId +import com.tangem.domain.wallets.models.UserWalletRemoteInfo import kotlinx.coroutines.flow.Flow +@Suppress("TooManyFunctions") interface WalletsRepository { suspend fun shouldSaveUserWalletsSync(): Boolean @@ -43,4 +45,13 @@ interface WalletsRepository { @Throws suspend fun setNotificationsEnabled(userWalletId: UserWalletId, isEnabled: Boolean) + + @Throws + suspend fun setWalletName(walletId: String, walletName: String) + + @Throws + suspend fun getWalletInfo(walletId: String): UserWalletRemoteInfo + + @Throws + suspend fun getWalletsInfo(applicationId: String, updateCache: Boolean = true): List } \ No newline at end of file diff --git a/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/RenameWalletUseCase.kt b/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/RenameWalletUseCase.kt index ff568c6794..8225cd9220 100644 --- a/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/RenameWalletUseCase.kt +++ b/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/RenameWalletUseCase.kt @@ -2,38 +2,23 @@ package com.tangem.domain.wallets.usecase import arrow.core.Either import arrow.core.raise.either -import arrow.core.raise.ensure -import com.tangem.common.CompletionResult -import com.tangem.domain.wallets.legacy.UserWalletsListManager +import com.tangem.domain.wallets.delegate.UserWalletsSyncDelegate import com.tangem.domain.wallets.models.UpdateWalletError import com.tangem.domain.wallets.models.UserWallet import com.tangem.domain.wallets.models.UserWalletId -import com.tangem.utils.coroutines.CoroutineDispatcherProvider -import kotlinx.coroutines.withContext +import com.tangem.domain.wallets.repository.WalletsRepository -/** - * Use case for rename user wallet - * - * @property userWalletsListManager user wallets list manager - */ class RenameWalletUseCase( - private val userWalletsListManager: UserWalletsListManager, - private val dispatchers: CoroutineDispatcherProvider, + private val walletsRepository: WalletsRepository, + private val userWalletsSyncDelegate: UserWalletsSyncDelegate, ) { suspend operator fun invoke(userWalletId: UserWalletId, name: String): Either = - withContext(dispatchers.io) { - either { - val existingNames = userWalletsListManager.userWalletsSync - - ensure(existingNames.none { it.name == name && it.walletId != userWalletId }) { - UpdateWalletError.NameAlreadyExists - } - - when (val result = userWalletsListManager.update(userWalletId) { it.copy(name = name) }) { - is CompletionResult.Failure -> raise(UpdateWalletError.DataError(result.error)) - is CompletionResult.Success -> result.data - } + either { + runCatching { + walletsRepository.setWalletName(userWalletId.stringValue, name) } + + userWalletsSyncDelegate.syncWallet(userWalletId, name).bind() } } \ No newline at end of file diff --git a/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/UpdateRemoteWalletsInfoUseCase.kt b/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/UpdateRemoteWalletsInfoUseCase.kt new file mode 100644 index 0000000000..5305089cd7 --- /dev/null +++ b/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/UpdateRemoteWalletsInfoUseCase.kt @@ -0,0 +1,19 @@ +package com.tangem.domain.wallets.usecase + +import arrow.core.Either +import arrow.core.raise.either +import com.tangem.domain.notifications.models.ApplicationId +import com.tangem.domain.wallets.delegate.UserWalletsSyncDelegate +import com.tangem.domain.wallets.models.UpdateWalletError +import com.tangem.domain.wallets.repository.WalletsRepository + +class UpdateRemoteWalletsInfoUseCase( + private val walletsRepository: WalletsRepository, + private val userWalletsSyncDelegate: UserWalletsSyncDelegate, +) { + + suspend operator fun invoke(applicationId: ApplicationId): Either = either { + val walletsInfo = walletsRepository.getWalletsInfo(applicationId.value) + userWalletsSyncDelegate.syncWallets(walletsInfo).bind() + } +} \ No newline at end of file From d17f0c4f0163c464a3b0afdff04d6a84ad0a3d95 Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 6 May 2025 12:51:54 +0400 Subject: [PATCH 002/165] Updated on 2026-08-14 --- core/res/src/main/res/values/strings.xml | 4 +- .../src/main/res/drawable/ic_edit_new_24.xml | 12 ++ .../transaction/entity/WcSignTransactionUM.kt | 60 ------ .../entity/approve/SpendAllowanceUM.kt | 6 + .../entity/approve/WcApproveTransactionUM.kt | 20 ++ .../common/WcTransactionRequestInfoUM.kt | 21 +++ .../entity/common/WcTransactionUM.kt | 36 ++++ .../entity/sign/WcSignTransactionUM.kt | 20 ++ .../model/WcSignTransactionModel.kt | 6 +- .../ui/approve/SpendAllowanceItem.kt | 82 +++++++++ .../WcApproveTransactionModalBottomSheet.kt | 122 ++++++++++++ .../common/TransactionRequestInfoContent.kt | 64 ------- .../transaction/ui/common/WcNetworkItem.kt | 2 +- .../ui/common/WcRequestFromItem.kt | 6 +- .../ui/common/WcTransactionRequestButtons.kt | 5 +- .../common/WcTransactionRequestInfoContent.kt | 90 +++++++++ .../sign/WcSignTransactionModalBottomSheet.kt | 174 +++++++++++++++--- ...cSignTransactionModalBottomSheetContent.kt | 89 --------- .../WcTransactionModalBottomSheetContent.kt | 119 ++++++++++++ .../utils/WcSignTransactionUtils.kt | 101 ++++++++-- 20 files changed, 770 insertions(+), 269 deletions(-) create mode 100644 core/ui/src/main/res/drawable/ic_edit_new_24.xml delete mode 100644 features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/entity/WcSignTransactionUM.kt create mode 100644 features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/entity/approve/SpendAllowanceUM.kt create mode 100644 features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/entity/approve/WcApproveTransactionUM.kt create mode 100644 features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/entity/common/WcTransactionRequestInfoUM.kt create mode 100644 features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/entity/common/WcTransactionUM.kt create mode 100644 features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/entity/sign/WcSignTransactionUM.kt create mode 100644 features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/ui/approve/SpendAllowanceItem.kt create mode 100644 features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/ui/approve/WcApproveTransactionModalBottomSheet.kt delete mode 100644 features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/ui/common/TransactionRequestInfoContent.kt create mode 100644 features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/ui/common/WcTransactionRequestInfoContent.kt delete mode 100644 features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/ui/sign/WcSignTransactionModalBottomSheetContent.kt create mode 100644 features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/ui/sign/WcTransactionModalBottomSheetContent.kt diff --git a/core/res/src/main/res/values/strings.xml b/core/res/src/main/res/values/strings.xml index b4b174998e..5c2ba817f3 100644 --- a/core/res/src/main/res/values/strings.xml +++ b/core/res/src/main/res/values/strings.xml @@ -1063,7 +1063,7 @@ validator: %s Minimum %s The minimum transaction amount is %1$s. - Tron network fees for popular tokens are higher. Stake some TRX for cheaper or free transactions. + Tron network fees for popular tokens can be higher. Staking TRX may help reduce transaction costs. Save on Tron network fees Try again You\'ve scanned the same card. To create a twin wallet you need to scan the card with number %d @@ -1334,6 +1334,8 @@ Transaction request Transaction request Wallet connect + Custom allowance + Allow to spend Discard You have an interrupted backup. Do you want to resume? Yes, resume diff --git a/core/ui/src/main/res/drawable/ic_edit_new_24.xml b/core/ui/src/main/res/drawable/ic_edit_new_24.xml new file mode 100644 index 0000000000..438006f6fa --- /dev/null +++ b/core/ui/src/main/res/drawable/ic_edit_new_24.xml @@ -0,0 +1,12 @@ + + + + diff --git a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/entity/WcSignTransactionUM.kt b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/entity/WcSignTransactionUM.kt deleted file mode 100644 index 3f796ce6b1..0000000000 --- a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/entity/WcSignTransactionUM.kt +++ /dev/null @@ -1,60 +0,0 @@ -package com.tangem.features.walletconnect.transaction.entity - -import androidx.annotation.DrawableRes -import androidx.compose.runtime.Immutable -import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfigContent -import com.tangem.core.ui.extensions.TextReference -import kotlinx.collections.immutable.ImmutableList - -@Immutable -internal data class WcSignTransactionUM( - @DrawableRes val startIconRes: Int, - @DrawableRes val endIconRes: Int, - @DrawableRes val transactionIconRes: Int, - val actions: WcTransactionActionsUM, - val state: State = State.TRANSACTION, - val transaction: WcTransactionUM, - val transactionRequestInfo: WcTransactionRequestInfoUM, -) : TangemBottomSheetConfigContent { - - enum class State { - TRANSACTION, TRANSACTION_REQUEST_INFO - } -} - -@Immutable -internal data class WcTransactionUM( - val appName: String, - val appIcon: String, - val isVerified: Boolean, - val appSubtitle: String, - val walletName: String, - val networkInfo: WcNetworkInfoUM, - val isLoading: Boolean = false, -) - -@Immutable -internal data class WcTransactionRequestInfoUM( - val info: ImmutableList, -) - -@Immutable -internal data class WcTransactionRequestInfoItemUM( - val title: TextReference, - val description: String, -) - -@Immutable -internal data class WcTransactionActionsUM( - val transactionRequestOnClick: () -> Unit, - val onDismiss: () -> Unit, - val onSign: () -> Unit, - val onBack: () -> Unit, - val onCopy: () -> Unit, -) - -@Immutable -internal data class WcNetworkInfoUM( - val name: String, - @DrawableRes val iconRes: Int, -) \ No newline at end of file diff --git a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/entity/approve/SpendAllowanceUM.kt b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/entity/approve/SpendAllowanceUM.kt new file mode 100644 index 0000000000..5a4c5a888c --- /dev/null +++ b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/entity/approve/SpendAllowanceUM.kt @@ -0,0 +1,6 @@ +package com.tangem.features.walletconnect.transaction.entity.approve + +internal data class SpendAllowanceUM( + val amountText: String, + val tokenImageUrl: String, +) \ No newline at end of file diff --git a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/entity/approve/WcApproveTransactionUM.kt b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/entity/approve/WcApproveTransactionUM.kt new file mode 100644 index 0000000000..cdf8da7c02 --- /dev/null +++ b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/entity/approve/WcApproveTransactionUM.kt @@ -0,0 +1,20 @@ +package com.tangem.features.walletconnect.transaction.entity.approve + +import androidx.compose.runtime.Immutable +import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfigContent +import com.tangem.features.walletconnect.transaction.entity.common.WcTransactionActionsUM +import com.tangem.features.walletconnect.transaction.entity.common.WcTransactionRequestInfoUM +import com.tangem.features.walletconnect.transaction.entity.common.WcTransactionUM + +@Immutable +internal data class WcApproveTransactionUM( + val actions: WcTransactionActionsUM, + val state: State = State.TRANSACTION, + val transaction: WcTransactionUM, + val transactionRequestInfo: WcTransactionRequestInfoUM, +) : TangemBottomSheetConfigContent { + + enum class State { + TRANSACTION, CUSTOM_ALLOWANCE, TRANSACTION_REQUEST_INFO + } +} \ No newline at end of file diff --git a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/entity/common/WcTransactionRequestInfoUM.kt b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/entity/common/WcTransactionRequestInfoUM.kt new file mode 100644 index 0000000000..dbd160ac11 --- /dev/null +++ b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/entity/common/WcTransactionRequestInfoUM.kt @@ -0,0 +1,21 @@ +package com.tangem.features.walletconnect.transaction.entity.common + +import androidx.compose.runtime.Immutable +import com.tangem.core.ui.extensions.TextReference +import kotlinx.collections.immutable.ImmutableList + +@Immutable +internal data class WcTransactionRequestInfoUM( + val blocks: ImmutableList, +) + +@Immutable +internal data class WcTransactionRequestBlockUM( + val info: ImmutableList, +) + +@Immutable +internal data class WcTransactionRequestInfoItemUM( + val title: TextReference, + val description: String = "", +) \ No newline at end of file diff --git a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/entity/common/WcTransactionUM.kt b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/entity/common/WcTransactionUM.kt new file mode 100644 index 0000000000..ae92092db0 --- /dev/null +++ b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/entity/common/WcTransactionUM.kt @@ -0,0 +1,36 @@ +package com.tangem.features.walletconnect.transaction.entity.common + +import androidx.annotation.DrawableRes +import androidx.compose.runtime.Immutable +import com.tangem.core.ui.extensions.TextReference +import com.tangem.features.walletconnect.transaction.entity.approve.SpendAllowanceUM + +@Immutable +internal data class WcTransactionUM( + val appName: String, + val appIcon: String, + val isVerified: Boolean, + val appSubtitle: String, + val walletName: String, + val networkInfo: WcNetworkInfoUM, + val activeButtonText: TextReference, + val addressText: String? = null, + val networkFee: String? = null, + val spendAllowance: SpendAllowanceUM? = null, + val isLoading: Boolean = false, +) + +@Immutable +internal data class WcTransactionActionsUM( + val transactionRequestOnClick: () -> Unit, + val onDismiss: () -> Unit, + val activeButtonOnClick: () -> Unit, + val onBack: () -> Unit, + val onCopy: () -> Unit, +) + +@Immutable +internal data class WcNetworkInfoUM( + val name: String, + @DrawableRes val iconRes: Int, +) \ No newline at end of file diff --git a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/entity/sign/WcSignTransactionUM.kt b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/entity/sign/WcSignTransactionUM.kt new file mode 100644 index 0000000000..1a86205361 --- /dev/null +++ b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/entity/sign/WcSignTransactionUM.kt @@ -0,0 +1,20 @@ +package com.tangem.features.walletconnect.transaction.entity.sign + +import androidx.compose.runtime.Immutable +import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfigContent +import com.tangem.features.walletconnect.transaction.entity.common.WcTransactionActionsUM +import com.tangem.features.walletconnect.transaction.entity.common.WcTransactionRequestInfoUM +import com.tangem.features.walletconnect.transaction.entity.common.WcTransactionUM + +@Immutable +internal data class WcSignTransactionUM( + val actions: WcTransactionActionsUM, + val state: State = State.TRANSACTION, + val transaction: WcTransactionUM, + val transactionRequestInfo: WcTransactionRequestInfoUM, +) : TangemBottomSheetConfigContent { + + enum class State { + TRANSACTION, TRANSACTION_REQUEST_INFO + } +} \ No newline at end of file diff --git a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/model/WcSignTransactionModel.kt b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/model/WcSignTransactionModel.kt index e5b5c90b30..68343c308e 100644 --- a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/model/WcSignTransactionModel.kt +++ b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/model/WcSignTransactionModel.kt @@ -11,8 +11,8 @@ import com.tangem.domain.walletconnect.usecase.method.WcSignState import com.tangem.domain.walletconnect.usecase.method.WcSignStep import com.tangem.domain.walletconnect.usecase.method.WcSignUseCase import com.tangem.features.walletconnect.transaction.components.WcSignTransactionComponent -import com.tangem.features.walletconnect.transaction.entity.WcSignTransactionUM -import com.tangem.features.walletconnect.transaction.entity.WcTransactionActionsUM +import com.tangem.features.walletconnect.transaction.entity.sign.WcSignTransactionUM +import com.tangem.features.walletconnect.transaction.entity.common.WcTransactionActionsUM import com.tangem.features.walletconnect.transaction.utils.toUM import com.tangem.utils.coroutines.CoroutineDispatcherProvider import kotlinx.coroutines.flow.MutableStateFlow @@ -48,7 +48,7 @@ internal class WcSignTransactionModel @Inject constructor( actions = WcTransactionActionsUM( onDismiss = { cancel(useCase) }, onBack = ::showTransactionState, - onSign = useCase::sign, + activeButtonOnClick = useCase::sign, onCopy = { copyData(useCase.rawSdkRequest.request.params) }, transactionRequestOnClick = ::showTransactionRequestState, ), diff --git a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/ui/approve/SpendAllowanceItem.kt b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/ui/approve/SpendAllowanceItem.kt new file mode 100644 index 0000000000..8dc9381c4c --- /dev/null +++ b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/ui/approve/SpendAllowanceItem.kt @@ -0,0 +1,82 @@ +package com.tangem.features.walletconnect.transaction.ui.approve + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Icon +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import com.tangem.features.walletconnect.impl.R +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import coil.compose.AsyncImage +import com.tangem.core.ui.res.TangemTheme +import com.tangem.features.walletconnect.transaction.entity.approve.SpendAllowanceUM +import com.tangem.features.walletconnect.transaction.ui.common.WcSmallTitleItem + +@Composable +internal fun SpendAllowanceItem(spendAllowance: SpendAllowanceUM, modifier: Modifier = Modifier) { + Column( + modifier = modifier + .clip(RoundedCornerShape(TangemTheme.dimens.radius14)) + .background(TangemTheme.colors.background.action) + .fillMaxWidth() + .padding( + end = TangemTheme.dimens.spacing12, + bottom = TangemTheme.dimens.spacing12, + ), + ) { + WcSmallTitleItem(R.string.wc_allow_to_spend) + + Spacer(modifier = Modifier.height(TangemTheme.dimens.spacing8)) + + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier.fillMaxWidth(), + ) { + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier.weight(1f), + ) { + AsyncImage( + model = spendAllowance.tokenImageUrl, + modifier = Modifier + .size(TangemTheme.dimens.size24) + .clip(CircleShape), + contentDescription = null, + ) + + Spacer(modifier = Modifier.width(TangemTheme.dimens.spacing12)) + + Text( + text = spendAllowance.amountText, + style = TangemTheme.typography.body1, + color = TangemTheme.colors.text.primary1, + ) + } + + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.End, + ) { + Text( + text = stringResource(R.string.manage_tokens_edit), + style = TangemTheme.typography.body1, + color = TangemTheme.colors.text.tertiary, + ) + + Spacer(modifier = Modifier.width(TangemTheme.dimens.spacing8)) + + Icon( + painter = painterResource(id = R.drawable.ic_edit_new_24), + contentDescription = null, + tint = TangemTheme.colors.icon.informative, + ) + } + } + } +} \ No newline at end of file diff --git a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/ui/approve/WcApproveTransactionModalBottomSheet.kt b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/ui/approve/WcApproveTransactionModalBottomSheet.kt new file mode 100644 index 0000000000..37449a66ab --- /dev/null +++ b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/ui/approve/WcApproveTransactionModalBottomSheet.kt @@ -0,0 +1,122 @@ +package com.tangem.features.walletconnect.transaction.ui.approve + +import android.content.res.Configuration +import androidx.compose.animation.animateContentSize +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.tooling.preview.Devices +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.tooling.preview.PreviewParameter +import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider +import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig +import com.tangem.core.ui.components.bottomsheets.modal.TangemModalBottomSheet +import com.tangem.core.ui.components.bottomsheets.modal.TangemModalBottomSheetTitle +import com.tangem.core.ui.extensions.resourceReference +import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.res.TangemThemePreview +import com.tangem.features.walletconnect.impl.R +import com.tangem.features.walletconnect.transaction.entity.approve.SpendAllowanceUM +import com.tangem.features.walletconnect.transaction.entity.approve.WcApproveTransactionUM +import com.tangem.features.walletconnect.transaction.entity.common.WcNetworkInfoUM +import com.tangem.features.walletconnect.transaction.entity.common.WcTransactionActionsUM +import com.tangem.features.walletconnect.transaction.entity.common.WcTransactionRequestInfoUM +import com.tangem.features.walletconnect.transaction.entity.common.WcTransactionUM +import com.tangem.features.walletconnect.transaction.ui.common.WcTransactionRequestInfoContent +import com.tangem.features.walletconnect.transaction.ui.sign.WcTransactionModalBottomSheetContent +import kotlinx.collections.immutable.persistentListOf + +@Composable +internal fun WcApproveTransactionModalBottomSheet(config: TangemBottomSheetConfig) { + TangemModalBottomSheet( + config = config, + containerColor = TangemTheme.colors.background.tertiary, + title = { state -> + TangemModalBottomSheetTitle( + title = when (state.state) { + WcApproveTransactionUM.State.TRANSACTION -> { + resourceReference(R.string.wallet_connect_title) + } + WcApproveTransactionUM.State.CUSTOM_ALLOWANCE -> { + resourceReference(R.string.wc_custom_allowance_title) + } + WcApproveTransactionUM.State.TRANSACTION_REQUEST_INFO -> { + resourceReference(R.string.wc_transaction_request_title) + } + }, + endIconRes = R.drawable.ic_close_24.takeIf { + state.state == WcApproveTransactionUM.State.TRANSACTION + }, + onEndClick = state.actions.onDismiss, + startIconRes = R.drawable.ic_back_24.takeIf { + state.state != WcApproveTransactionUM.State.TRANSACTION + }, + onStartClick = state.actions.onBack, + ) + }, + content = { state -> + Box( + modifier = Modifier + .fillMaxWidth() + .animateContentSize(), + ) { + when (state.state) { + WcApproveTransactionUM.State.TRANSACTION -> { + WcTransactionModalBottomSheetContent(state.transaction, state.actions) + } + WcApproveTransactionUM.State.CUSTOM_ALLOWANCE -> { + TODO("Will be done in the second part of the PR") + } + WcApproveTransactionUM.State.TRANSACTION_REQUEST_INFO -> { + WcTransactionRequestInfoContent(state.transactionRequestInfo, state.actions) + } + } + } + }, + ) +} + +@Composable +@Preview(showBackground = true, device = Devices.PIXEL_7_PRO) +@Preview(showBackground = true, device = Devices.PIXEL_7_PRO, uiMode = Configuration.UI_MODE_NIGHT_YES) +private fun WcApproveTransactionBottomSheetPreview( + @PreviewParameter(WcApproveTransactionStateProvider::class) state: WcApproveTransactionUM, +) { + TangemThemePreview { + WcApproveTransactionModalBottomSheet( + config = TangemBottomSheetConfig( + isShown = true, + onDismissRequest = {}, + content = state, + ), + ) + } +} + +private class WcApproveTransactionStateProvider : CollectionPreviewParameterProvider( + listOf( + WcApproveTransactionUM( + state = WcApproveTransactionUM.State.TRANSACTION, + transaction = WcTransactionUM( + appName = "React App", + appIcon = "", + isVerified = true, + appSubtitle = "react-app.walletconnect.com", + walletName = "Tangem 2.0", + activeButtonText = resourceReference(R.string.common_send), + networkInfo = WcNetworkInfoUM(name = "Ethereum", iconRes = R.drawable.img_eth_22), + networkFee = "~ 0.22 $", + spendAllowance = SpendAllowanceUM(amountText = "Unlimited USDT", tokenImageUrl = ""), + ), + transactionRequestInfo = WcTransactionRequestInfoUM(persistentListOf()), + actions = WcTransactionActionsUM( + onDismiss = {}, + onBack = {}, + activeButtonOnClick = {}, + onCopy = {}, + transactionRequestOnClick = {}, + ), + ), + ), +) \ No newline at end of file diff --git a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/ui/common/TransactionRequestInfoContent.kt b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/ui/common/TransactionRequestInfoContent.kt deleted file mode 100644 index d7c61d3af8..0000000000 --- a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/ui/common/TransactionRequestInfoContent.kt +++ /dev/null @@ -1,64 +0,0 @@ -package com.tangem.features.walletconnect.transaction.ui.common - -import androidx.compose.foundation.background -import androidx.compose.foundation.layout.* -import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.clip -import androidx.compose.ui.unit.dp -import com.tangem.core.ui.components.SecondaryButtonIconEnd -import com.tangem.core.ui.extensions.resolveReference -import com.tangem.core.ui.extensions.stringResourceSafe -import com.tangem.core.ui.res.TangemTheme -import com.tangem.features.walletconnect.impl.R -import com.tangem.features.walletconnect.transaction.entity.WcSignTransactionUM - -@Composable -internal fun TransactionRequestInfoContent(state: WcSignTransactionUM) { - Box( - modifier = Modifier - .fillMaxWidth() - .heightIn(min = 310.dp) - .padding(horizontal = TangemTheme.dimens.spacing16), - ) { - Column( - modifier = Modifier - .fillMaxWidth() - .clip(RoundedCornerShape(TangemTheme.dimens.radius16)) - .background(TangemTheme.colors.background.action) - .padding( - start = TangemTheme.dimens.spacing16, - end = TangemTheme.dimens.spacing16, - bottom = TangemTheme.dimens.spacing20, - ), - ) { - state.transactionRequestInfo.info.forEach { item -> - Text( - text = item.title.resolveReference(), - modifier = Modifier.padding(top = TangemTheme.dimens.spacing20), - style = TangemTheme.typography.subtitle2, - color = TangemTheme.colors.text.tertiary, - ) - Text( - text = item.description, - modifier = Modifier.padding(top = TangemTheme.dimens.spacing4), - style = TangemTheme.typography.body2, - color = TangemTheme.colors.text.primary1, - ) - } - } - - SecondaryButtonIconEnd( - modifier = Modifier - .align(Alignment.BottomCenter) - .padding(bottom = TangemTheme.dimens.spacing20) - .fillMaxWidth(), - text = stringResourceSafe(R.string.wc_copy_data_button_text), - onClick = state.actions.onCopy, - iconResId = R.drawable.ic_copy_24, - ) - } -} \ No newline at end of file diff --git a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/ui/common/WcNetworkItem.kt b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/ui/common/WcNetworkItem.kt index 1f337688aa..493c205d0e 100644 --- a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/ui/common/WcNetworkItem.kt +++ b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/ui/common/WcNetworkItem.kt @@ -14,7 +14,7 @@ import androidx.compose.ui.res.painterResource import com.tangem.core.ui.extensions.stringResourceSafe import com.tangem.core.ui.res.TangemTheme import com.tangem.features.walletconnect.impl.R -import com.tangem.features.walletconnect.transaction.entity.WcNetworkInfoUM +import com.tangem.features.walletconnect.transaction.entity.common.WcNetworkInfoUM @Composable internal fun WcNetworkItem(networkInfo: WcNetworkInfoUM, modifier: Modifier = Modifier) { diff --git a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/ui/common/WcRequestFromItem.kt b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/ui/common/WcRequestFromItem.kt index a36d7b2365..e6d70ced09 100644 --- a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/ui/common/WcRequestFromItem.kt +++ b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/ui/common/WcRequestFromItem.kt @@ -1,5 +1,6 @@ package com.tangem.features.walletconnect.transaction.ui.common +import androidx.annotation.StringRes import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.material3.Text @@ -7,15 +8,14 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import com.tangem.core.ui.extensions.stringResourceSafe import com.tangem.core.ui.res.TangemTheme -import com.tangem.features.walletconnect.impl.R @Composable -fun RequestFromItem(modifier: Modifier = Modifier) { +fun WcSmallTitleItem(@StringRes textRex: Int, modifier: Modifier = Modifier) { Text( modifier = modifier .fillMaxWidth() .padding(top = TangemTheme.dimens.spacing12, start = TangemTheme.dimens.spacing12), - text = stringResourceSafe(R.string.wc_request_from), + text = stringResourceSafe(textRex), color = TangemTheme.colors.text.tertiary, style = TangemTheme.typography.subtitle2, ) diff --git a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/ui/common/WcTransactionRequestButtons.kt b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/ui/common/WcTransactionRequestButtons.kt index 8451c004cb..650d30d63e 100644 --- a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/ui/common/WcTransactionRequestButtons.kt +++ b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/ui/common/WcTransactionRequestButtons.kt @@ -7,12 +7,15 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import com.tangem.core.ui.components.PrimaryButtonIconEnd import com.tangem.core.ui.components.SecondaryButton +import com.tangem.core.ui.extensions.TextReference +import com.tangem.core.ui.extensions.resolveReference import com.tangem.core.ui.extensions.stringResourceSafe import com.tangem.core.ui.res.TangemTheme import com.tangem.features.walletconnect.impl.R @Composable internal fun WcTransactionRequestButtons( + activeButtonText: TextReference, isLoading: Boolean, onDismiss: () -> Unit, onSign: () -> Unit, @@ -30,7 +33,7 @@ internal fun WcTransactionRequestButtons( modifier = Modifier .fillMaxWidth() .weight(1f), - text = stringResourceSafe(R.string.common_sign), + text = activeButtonText.resolveReference(), onClick = onSign, iconResId = R.drawable.ic_tangem_24, showProgress = isLoading, diff --git a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/ui/common/WcTransactionRequestInfoContent.kt b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/ui/common/WcTransactionRequestInfoContent.kt new file mode 100644 index 0000000000..736c6b0936 --- /dev/null +++ b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/ui/common/WcTransactionRequestInfoContent.kt @@ -0,0 +1,90 @@ +package com.tangem.features.walletconnect.transaction.ui.common + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.platform.LocalConfiguration +import androidx.compose.ui.unit.dp +import com.tangem.core.ui.components.SecondaryButtonIconEnd +import com.tangem.core.ui.extensions.resolveReference +import com.tangem.core.ui.extensions.stringResourceSafe +import com.tangem.core.ui.res.TangemTheme +import com.tangem.features.walletconnect.impl.R +import com.tangem.features.walletconnect.transaction.entity.common.WcTransactionActionsUM +import com.tangem.features.walletconnect.transaction.entity.common.WcTransactionRequestInfoUM + +private const val MIN_HEIGHT_SCREEN_PERCENT = 0.35f +private const val MAX_HEIGHT_SCREEN_PERCENT = 0.75f + +@Composable +internal fun WcTransactionRequestInfoContent(info: WcTransactionRequestInfoUM, actions: WcTransactionActionsUM) { + val screenHeight = LocalConfiguration.current.screenHeightDp + val minHeight = (screenHeight * MIN_HEIGHT_SCREEN_PERCENT).dp + val maxHeight = (screenHeight * MAX_HEIGHT_SCREEN_PERCENT).dp + + Box( + modifier = Modifier + .fillMaxWidth() + .heightIn(min = minHeight, max = maxHeight) + .padding(horizontal = TangemTheme.dimens.spacing16), + ) { + LazyColumn( + modifier = Modifier + .fillMaxWidth(), + contentPadding = PaddingValues(top = TangemTheme.dimens.spacing8, bottom = TangemTheme.dimens.spacing70), + ) { + items(info.blocks) { block -> + Column( + modifier = Modifier + .fillMaxWidth() + .clip(RoundedCornerShape(TangemTheme.dimens.radius16)) + .background(TangemTheme.colors.background.action) + .padding( + start = TangemTheme.dimens.spacing16, + end = TangemTheme.dimens.spacing16, + top = TangemTheme.dimens.spacing20, + ), + ) { + block.info.forEach { item -> + Text( + text = item.title.resolveReference(), + style = TangemTheme.typography.subtitle2, + color = TangemTheme.colors.text.tertiary, + ) + if (item.description.isNotEmpty()) { + Text( + text = item.description, + modifier = Modifier.padding( + top = TangemTheme.dimens.spacing4, + bottom = TangemTheme.dimens.spacing20, + ), + style = TangemTheme.typography.body2, + color = TangemTheme.colors.text.primary1, + ) + } else { + Spacer(modifier = Modifier.size(TangemTheme.dimens.size12)) + } + } + } + Spacer(modifier = Modifier.size(TangemTheme.dimens.size20)) + } + } + + SecondaryButtonIconEnd( + modifier = Modifier + .align(Alignment.BottomCenter) + .padding(bottom = TangemTheme.dimens.spacing20) + .fillMaxWidth(), + text = stringResourceSafe(R.string.wc_copy_data_button_text), + onClick = actions.onCopy, + iconResId = R.drawable.ic_copy_24, + ) + } +} \ No newline at end of file diff --git a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/ui/sign/WcSignTransactionModalBottomSheet.kt b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/ui/sign/WcSignTransactionModalBottomSheet.kt index 665eb497b3..004b22bea8 100644 --- a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/ui/sign/WcSignTransactionModalBottomSheet.kt +++ b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/ui/sign/WcSignTransactionModalBottomSheet.kt @@ -17,8 +17,14 @@ import com.tangem.core.ui.extensions.resourceReference import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreview import com.tangem.features.walletconnect.impl.R -import com.tangem.features.walletconnect.transaction.entity.* -import com.tangem.features.walletconnect.transaction.ui.common.TransactionRequestInfoContent +import com.tangem.features.walletconnect.transaction.entity.common.WcNetworkInfoUM +import com.tangem.features.walletconnect.transaction.entity.common.WcTransactionActionsUM +import com.tangem.features.walletconnect.transaction.entity.common.WcTransactionRequestBlockUM +import com.tangem.features.walletconnect.transaction.entity.common.WcTransactionRequestInfoItemUM +import com.tangem.features.walletconnect.transaction.entity.common.WcTransactionRequestInfoUM +import com.tangem.features.walletconnect.transaction.entity.common.WcTransactionUM +import com.tangem.features.walletconnect.transaction.entity.sign.WcSignTransactionUM +import com.tangem.features.walletconnect.transaction.ui.common.WcTransactionRequestInfoContent import kotlinx.collections.immutable.persistentListOf @Composable @@ -36,11 +42,11 @@ internal fun WcSignTransactionModalBottomSheet(config: TangemBottomSheetConfig) resourceReference(R.string.wc_transaction_request_title) } }, - endIconRes = state.endIconRes.takeIf { + endIconRes = R.drawable.ic_close_24.takeIf { state.state == WcSignTransactionUM.State.TRANSACTION }, onEndClick = state.actions.onDismiss, - startIconRes = state.startIconRes.takeIf { + startIconRes = R.drawable.ic_back_24.takeIf { state.state == WcSignTransactionUM.State.TRANSACTION_REQUEST_INFO }, onStartClick = state.actions.onBack, @@ -54,10 +60,10 @@ internal fun WcSignTransactionModalBottomSheet(config: TangemBottomSheetConfig) ) { when (state.state) { WcSignTransactionUM.State.TRANSACTION -> { - WcSignTransactionModalBottomSheetContent(state) + WcTransactionModalBottomSheetContent(state.transaction, state.actions) } WcSignTransactionUM.State.TRANSACTION_REQUEST_INFO -> { - TransactionRequestInfoContent(state) + WcTransactionRequestInfoContent(state.transactionRequestInfo, state.actions) } } } @@ -85,9 +91,6 @@ private fun WcSignTransactionBottomSheetPreview( private class WcSignTransactionStateProvider : CollectionPreviewParameterProvider( listOf( WcSignTransactionUM( - startIconRes = R.drawable.ic_back_24, - endIconRes = R.drawable.ic_close_24, - transactionIconRes = R.drawable.ic_doc_new_24, state = WcSignTransactionUM.State.TRANSACTION, transaction = WcTransactionUM( appName = "React App", @@ -95,32 +98,34 @@ private class WcSignTransactionStateProvider : CollectionPreviewParameterProvide isVerified = true, appSubtitle = "react-app.walletconnect.com", walletName = "Tangem 2.0", + activeButtonText = resourceReference(R.string.common_sign), networkInfo = WcNetworkInfoUM(name = "Ethereum", iconRes = R.drawable.img_eth_22), ), transactionRequestInfo = WcTransactionRequestInfoUM( - persistentListOf( - WcTransactionRequestInfoItemUM( - title = resourceReference(R.string.wc_signature_type), - description = "personal_sign", - ), - WcTransactionRequestInfoItemUM( - title = resourceReference(R.string.wc_contents), - description = "Hello! My name is John Dow. test@tange.com", + blocks = persistentListOf( + WcTransactionRequestBlockUM( + persistentListOf( + WcTransactionRequestInfoItemUM( + title = resourceReference(R.string.wc_signature_type), + description = "personal_sign", + ), + WcTransactionRequestInfoItemUM( + title = resourceReference(R.string.wc_contents), + description = "Hello! My name is John Dow. test@tange.com", + ), + ), ), ), ), actions = WcTransactionActionsUM( onDismiss = {}, onBack = {}, - onSign = {}, + activeButtonOnClick = {}, onCopy = {}, transactionRequestOnClick = {}, ), ), WcSignTransactionUM( - startIconRes = R.drawable.ic_back_24, - endIconRes = R.drawable.ic_close_24, - transactionIconRes = R.drawable.ic_doc_new_24, state = WcSignTransactionUM.State.TRANSACTION_REQUEST_INFO, transaction = WcTransactionUM( appName = "React App", @@ -128,24 +133,133 @@ private class WcSignTransactionStateProvider : CollectionPreviewParameterProvide isVerified = true, appSubtitle = "react-app.walletconnect.com", walletName = "Tangem 2.0", + activeButtonText = resourceReference(R.string.common_sign), networkInfo = WcNetworkInfoUM(name = "Ethereum", iconRes = R.drawable.img_eth_22), ), transactionRequestInfo = WcTransactionRequestInfoUM( - persistentListOf( - WcTransactionRequestInfoItemUM( - title = resourceReference(R.string.wc_signature_type), - description = "personal_sign", - ), - WcTransactionRequestInfoItemUM( - title = resourceReference(R.string.wc_contents), - description = "Hello! My name is John Dow. test@tange.com", + blocks = persistentListOf( + WcTransactionRequestBlockUM( + persistentListOf( + WcTransactionRequestInfoItemUM( + title = resourceReference(R.string.wc_signature_type), + description = "personal_sign", + ), + WcTransactionRequestInfoItemUM( + title = resourceReference(R.string.wc_contents), + description = "Hello! My name is John Dow. test@tange.com", + ), + ), ), ), ), actions = WcTransactionActionsUM( onDismiss = {}, onBack = {}, - onSign = {}, + activeButtonOnClick = {}, + onCopy = {}, + transactionRequestOnClick = {}, + ), + ), + WcSignTransactionUM( + state = WcSignTransactionUM.State.TRANSACTION, + transaction = WcTransactionUM( + appName = "React App", + appIcon = "", + isVerified = true, + appSubtitle = "react-app.walletconnect.com", + walletName = "Tangem 2.0", + activeButtonText = resourceReference(R.string.common_sign), + networkInfo = WcNetworkInfoUM(name = "Ethereum", iconRes = R.drawable.img_eth_22), + addressText = "0x345FF...34FA", + networkFee = "~ 0.22 $", + ), + transactionRequestInfo = WcTransactionRequestInfoUM( + blocks = persistentListOf( + WcTransactionRequestBlockUM( + persistentListOf( + WcTransactionRequestInfoItemUM( + title = resourceReference(R.string.wc_signature_type), + description = "personal_sign", + ), + WcTransactionRequestInfoItemUM( + title = resourceReference(R.string.wc_contents), + description = "Hello! My name is John Dow. test@tange.com", + ), + ), + ), + WcTransactionRequestBlockUM( + persistentListOf( + WcTransactionRequestInfoItemUM( + title = resourceReference(R.string.wc_transaction_info_to_title), + ), + WcTransactionRequestInfoItemUM( + title = resourceReference(R.string.settings_wallet_name_title), + description = "Bob", + ), + WcTransactionRequestInfoItemUM( + title = resourceReference(R.string.wc_common_wallet), + description = "0xCD2a3d9F938E13CD947Ec05AbC7FE734Df8DD826", + ), + ), + ), + ), + ), + actions = WcTransactionActionsUM( + onDismiss = {}, + onBack = {}, + activeButtonOnClick = {}, + onCopy = {}, + transactionRequestOnClick = {}, + ), + ), + WcSignTransactionUM( + state = WcSignTransactionUM.State.TRANSACTION_REQUEST_INFO, + transaction = WcTransactionUM( + appName = "React App", + appIcon = "", + isVerified = true, + appSubtitle = "react-app.walletconnect.com", + walletName = "Tangem 2.0", + activeButtonText = resourceReference(R.string.common_sign), + networkInfo = WcNetworkInfoUM(name = "Ethereum", iconRes = R.drawable.img_eth_22), + addressText = "0x345FF...34FA", + networkFee = "~ 0.22 $", + ), + transactionRequestInfo = WcTransactionRequestInfoUM( + blocks = persistentListOf( + WcTransactionRequestBlockUM( + persistentListOf( + WcTransactionRequestInfoItemUM( + title = resourceReference(R.string.wc_signature_type), + description = "personal_sign", + ), + WcTransactionRequestInfoItemUM( + title = resourceReference(R.string.wc_contents), + description = "Hello! My name is John Dow. test@tange.com", + ), + ), + ), + WcTransactionRequestBlockUM( + persistentListOf( + WcTransactionRequestInfoItemUM( + title = resourceReference(R.string.wc_transaction_info_to_title), + ), + WcTransactionRequestInfoItemUM( + title = resourceReference(R.string.settings_wallet_name_title), + description = "Bob", + ), + WcTransactionRequestInfoItemUM( + title = resourceReference(R.string.wc_common_wallet), + description = "0xCD2a3d9F938E13CD947Ec05AbC7FE734Df8DD826", + ), + ), + ), + ), + ), + actions = WcTransactionActionsUM( + onDismiss = {}, + onBack = {}, + activeButtonOnClick = {}, onCopy = {}, transactionRequestOnClick = {}, ), diff --git a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/ui/sign/WcSignTransactionModalBottomSheetContent.kt b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/ui/sign/WcSignTransactionModalBottomSheetContent.kt deleted file mode 100644 index 7548486b13..0000000000 --- a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/ui/sign/WcSignTransactionModalBottomSheetContent.kt +++ /dev/null @@ -1,89 +0,0 @@ -package com.tangem.features.walletconnect.transaction.ui.sign - -import androidx.compose.animation.animateContentSize -import androidx.compose.foundation.background -import androidx.compose.foundation.clickable -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.material3.HorizontalDivider -import androidx.compose.runtime.Composable -import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.clip -import com.tangem.core.ui.res.TangemTheme -import com.tangem.features.walletconnect.connections.ui.WcAppInfoItem -import com.tangem.features.walletconnect.transaction.entity.WcSignTransactionUM -import com.tangem.features.walletconnect.transaction.ui.common.* -import com.tangem.features.walletconnect.transaction.ui.common.WcNetworkItem -import com.tangem.features.walletconnect.transaction.ui.common.WcTransactionRequestButtons -import com.tangem.features.walletconnect.transaction.ui.common.WcTransactionRequestItem -import com.tangem.features.walletconnect.transaction.ui.common.WcWalletItem - -@Composable -internal fun WcSignTransactionModalBottomSheetContent(state: WcSignTransactionUM) { - Column(modifier = Modifier.padding(horizontal = TangemTheme.dimens.spacing16)) { - Column( - modifier = Modifier - .clip(RoundedCornerShape(TangemTheme.dimens.radius14)) - .background(color = TangemTheme.colors.background.action) - .fillMaxWidth() - .animateContentSize(), - ) { - RequestFromItem() - WcAppInfoItem( - iconUrl = state.transaction.appIcon, - title = state.transaction.appName, - subtitle = state.transaction.appSubtitle, - isVerified = state.transaction.isVerified, - ) - HorizontalDivider( - thickness = TangemTheme.dimens.size1, - color = TangemTheme.colors.stroke.primary, - ) - WcTransactionRequestItem( - iconRes = state.transactionIconRes, - modifier = Modifier - .fillMaxWidth() - .clickable { state.actions.transactionRequestOnClick() } - .padding(TangemTheme.dimens.spacing12), - ) - } - Column(modifier = Modifier.padding(top = TangemTheme.dimens.spacing16)) { - Column( - modifier = Modifier - .clip(RoundedCornerShape(TangemTheme.dimens.radius14)) - .background(color = TangemTheme.colors.background.action) - .fillMaxWidth() - .animateContentSize(), - ) { - val itemsModifier = Modifier - .fillMaxWidth() - .padding(TangemTheme.dimens.spacing12) - - HorizontalDivider( - thickness = TangemTheme.dimens.size1, - color = TangemTheme.colors.stroke.primary, - ) - WcWalletItem( - modifier = itemsModifier, - walletName = state.transaction.walletName, - ) - HorizontalDivider( - thickness = TangemTheme.dimens.size1, - color = TangemTheme.colors.stroke.primary, - ) - WcNetworkItem( - modifier = itemsModifier, - networkInfo = state.transaction.networkInfo, - ) - } - WcTransactionRequestButtons( - modifier = Modifier.padding(vertical = TangemTheme.dimens.spacing16), - onDismiss = state.actions.onDismiss, - onSign = state.actions.onSign, - isLoading = state.transaction.isLoading, - ) - } - } -} \ No newline at end of file diff --git a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/ui/sign/WcTransactionModalBottomSheetContent.kt b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/ui/sign/WcTransactionModalBottomSheetContent.kt new file mode 100644 index 0000000000..84d7b4c90e --- /dev/null +++ b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/ui/sign/WcTransactionModalBottomSheetContent.kt @@ -0,0 +1,119 @@ +package com.tangem.features.walletconnect.transaction.ui.sign + +import androidx.compose.animation.animateContentSize +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.HorizontalDivider +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp +import com.tangem.core.ui.res.TangemTheme +import com.tangem.features.walletconnect.connections.ui.WcAppInfoItem +import com.tangem.features.walletconnect.impl.R +import com.tangem.features.walletconnect.transaction.entity.common.WcTransactionActionsUM +import com.tangem.features.walletconnect.transaction.entity.common.WcTransactionUM +import com.tangem.features.walletconnect.transaction.ui.approve.SpendAllowanceItem +import com.tangem.features.walletconnect.transaction.ui.common.* +import com.tangem.features.walletconnect.transaction.ui.common.WcNetworkItem +import com.tangem.features.walletconnect.transaction.ui.common.WcTransactionRequestButtons +import com.tangem.features.walletconnect.transaction.ui.common.WcTransactionRequestItem +import com.tangem.features.walletconnect.transaction.ui.common.WcWalletItem + +@Composable +internal fun WcTransactionModalBottomSheetContent(transaction: WcTransactionUM, actions: WcTransactionActionsUM) { + Column(modifier = Modifier.padding(horizontal = TangemTheme.dimens.spacing16)) { + Column( + modifier = Modifier + .clip(RoundedCornerShape(TangemTheme.dimens.radius14)) + .background(color = TangemTheme.colors.background.action) + .fillMaxWidth() + .animateContentSize(), + ) { + WcSmallTitleItem(R.string.wc_request_from) + WcAppInfoItem( + iconUrl = transaction.appIcon, + title = transaction.appName, + subtitle = transaction.appSubtitle, + isVerified = transaction.isVerified, + ) + DividerWithPadding(start = 0.dp, end = 0.dp) + WcTransactionRequestItem( + iconRes = R.drawable.ic_doc_new_24, + modifier = Modifier + .fillMaxWidth() + .clickable { actions.transactionRequestOnClick() } + .padding(TangemTheme.dimens.spacing12), + ) + } + Column(modifier = Modifier.padding(top = TangemTheme.dimens.spacing16)) { + if (transaction.spendAllowance != null) { + SpendAllowanceItem(transaction.spendAllowance) + Spacer(Modifier.height(TangemTheme.dimens.spacing16)) + } + WcSignTransactionItems(transaction) + WcTransactionRequestButtons( + modifier = Modifier.padding(vertical = TangemTheme.dimens.spacing16), + onDismiss = actions.onDismiss, + onSign = actions.activeButtonOnClick, + activeButtonText = transaction.activeButtonText, + isLoading = transaction.isLoading, + ) + } + } +} + +@Composable +private fun WcSignTransactionItems(transaction: WcTransactionUM) { + Column( + modifier = Modifier + .clip(RoundedCornerShape(TangemTheme.dimens.radius14)) + .background(color = TangemTheme.colors.background.action) + .fillMaxWidth() + .animateContentSize(), + ) { + val itemsModifier = Modifier + .fillMaxWidth() + .padding(TangemTheme.dimens.spacing12) + + DividerWithPadding(start = 0.dp, end = 0.dp) + WcWalletItem( + modifier = itemsModifier, + walletName = transaction.walletName, + ) + DividerWithPadding(start = TangemTheme.dimens.spacing40, end = TangemTheme.dimens.spacing12) + WcNetworkItem( + modifier = itemsModifier, + networkInfo = transaction.networkInfo, + ) + if (!transaction.addressText.isNullOrEmpty()) { + DividerWithPadding(start = TangemTheme.dimens.spacing40, end = TangemTheme.dimens.spacing12) + WcAddressItem( + modifier = itemsModifier, + addressText = transaction.addressText, + ) + } + if (!transaction.networkFee.isNullOrEmpty()) { + DividerWithPadding(start = TangemTheme.dimens.spacing40, end = TangemTheme.dimens.spacing12) + WcNetworkFeeItem( + modifier = itemsModifier, + networkFeeText = transaction.networkFee, + ) + } + } +} + +@Composable +private fun DividerWithPadding(start: Dp, end: Dp) { + HorizontalDivider( + modifier = Modifier.padding( + start = start, + end = end, + ), + thickness = TangemTheme.dimens.size1, + color = TangemTheme.colors.stroke.primary, + ) +} \ No newline at end of file diff --git a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/utils/WcSignTransactionUtils.kt b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/utils/WcSignTransactionUtils.kt index 5eba6dcc9f..cb56800329 100644 --- a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/utils/WcSignTransactionUtils.kt +++ b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/utils/WcSignTransactionUtils.kt @@ -3,40 +3,50 @@ package com.tangem.features.walletconnect.transaction.utils import com.domain.blockaid.models.dapp.CheckDAppResult import com.tangem.core.ui.extensions.getActiveIconRes import com.tangem.core.ui.extensions.resourceReference +import com.tangem.domain.walletconnect.model.WcEthMethod +import com.tangem.domain.walletconnect.model.WcSolanaMethod +import com.tangem.domain.walletconnect.model.sdkcopy.WcSdkSessionRequest import com.tangem.domain.walletconnect.usecase.method.WcMessageSignUseCase import com.tangem.domain.walletconnect.usecase.method.WcSignState import com.tangem.domain.walletconnect.usecase.method.WcSignStep import com.tangem.domain.walletconnect.usecase.method.WcSignUseCase import com.tangem.features.walletconnect.impl.R -import com.tangem.features.walletconnect.transaction.entity.WcNetworkInfoUM -import com.tangem.features.walletconnect.transaction.entity.WcSignTransactionUM -import com.tangem.features.walletconnect.transaction.entity.WcTransactionActionsUM -import com.tangem.features.walletconnect.transaction.entity.WcTransactionRequestInfoItemUM -import com.tangem.features.walletconnect.transaction.entity.WcTransactionRequestInfoUM -import com.tangem.features.walletconnect.transaction.entity.WcTransactionUM +import com.tangem.features.walletconnect.transaction.entity.common.WcNetworkInfoUM +import com.tangem.features.walletconnect.transaction.entity.sign.WcSignTransactionUM +import com.tangem.features.walletconnect.transaction.entity.common.WcTransactionActionsUM +import com.tangem.features.walletconnect.transaction.entity.common.WcTransactionRequestBlockUM +import com.tangem.features.walletconnect.transaction.entity.common.WcTransactionRequestInfoItemUM +import com.tangem.features.walletconnect.transaction.entity.common.WcTransactionRequestInfoUM +import com.tangem.features.walletconnect.transaction.entity.common.WcTransactionUM import kotlinx.collections.immutable.persistentListOf +import kotlinx.collections.immutable.toImmutableList internal fun WcSignUseCase.toUM(signState: WcSignState<*>, actions: WcTransactionActionsUM): WcSignTransactionUM? { return when (this) { is WcMessageSignUseCase -> { - ethMessageSignToUM( - signState = signState, - signModel = signState.signModel as WcMessageSignUseCase.SignModel, - actions = actions, - ) + when (method) { + is WcEthMethod.SignTypedData -> signTypedDataToUM( + signState = signState, + signModel = signState.signModel as WcMessageSignUseCase.SignModel, + actions = actions, + ) + is WcEthMethod.MessageSign, is WcSolanaMethod.SignMessage -> messageSignToUM( + signState = signState, + signModel = signState.signModel as WcMessageSignUseCase.SignModel, + actions = actions, + ) + else -> null + } } else -> null } } -private fun WcMessageSignUseCase.ethMessageSignToUM( +private fun WcMessageSignUseCase.signTypedDataToUM( signState: WcSignState<*>, signModel: WcMessageSignUseCase.SignModel, actions: WcTransactionActionsUM, ) = WcSignTransactionUM( - startIconRes = R.drawable.ic_back_24, - endIconRes = R.drawable.ic_close_24, - transactionIconRes = R.drawable.ic_doc_new_24, state = WcSignTransactionUM.State.TRANSACTION, actions = actions, transaction = WcTransactionUM( @@ -49,9 +59,66 @@ private fun WcMessageSignUseCase.ethMessageSignToUM( name = network.name, iconRes = getActiveIconRes(network.id.value), ), + addressText = walletAddress.toShortAddressText(), + activeButtonText = resourceReference(R.string.common_sign), isLoading = signState.domainStep == WcSignStep.Signing, ), transactionRequestInfo = WcTransactionRequestInfoUM( + buildList { + add(createInfoBlockUM(rawSdkRequest, signModel)) + (method as? WcEthMethod.SignTypedData)?.params?.message?.to?.let { to -> + add( + WcTransactionRequestBlockUM( + persistentListOf( + WcTransactionRequestInfoItemUM( + title = resourceReference(R.string.wc_transaction_info_to_title), + ), + WcTransactionRequestInfoItemUM( + title = resourceReference(R.string.settings_wallet_name_title), + description = to.name, + ), + WcTransactionRequestInfoItemUM( + title = resourceReference(R.string.wc_common_wallet), + description = to.wallet, + ), + ), + ), + ) + } + }.toImmutableList(), + ), +) + +private fun WcMessageSignUseCase.messageSignToUM( + signState: WcSignState<*>, + signModel: WcMessageSignUseCase.SignModel, + actions: WcTransactionActionsUM, +) = WcSignTransactionUM( + state = WcSignTransactionUM.State.TRANSACTION, + actions = actions, + transaction = WcTransactionUM( + appName = session.sdkModel.appMetaData.name, + appIcon = session.sdkModel.appMetaData.url, + isVerified = session.securityStatus == CheckDAppResult.SAFE, + appSubtitle = session.sdkModel.appMetaData.description, + walletName = session.wallet.name, + networkInfo = WcNetworkInfoUM( + name = network.name, + iconRes = getActiveIconRes(network.id.value), + ), + activeButtonText = resourceReference(R.string.common_sign), + isLoading = signState.domainStep == WcSignStep.Signing, + ), + transactionRequestInfo = WcTransactionRequestInfoUM( + persistentListOf(createInfoBlockUM(rawSdkRequest, signModel)), + ), +) + +private fun createInfoBlockUM( + rawSdkRequest: WcSdkSessionRequest, + signModel: WcMessageSignUseCase.SignModel, +): WcTransactionRequestBlockUM { + return WcTransactionRequestBlockUM( persistentListOf( WcTransactionRequestInfoItemUM( title = resourceReference(R.string.wc_signature_type), @@ -62,5 +129,5 @@ private fun WcMessageSignUseCase.ethMessageSignToUM( description = signModel.humanMsg, ), ), - ), -) \ No newline at end of file + ) +} \ No newline at end of file From bc3e0b799c2b0dfe061066cb430ec3c7e6ad5188 Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 6 May 2025 14:11:51 +0500 Subject: [PATCH 003/165] Updated on 2026-08-14 --- .../DefaultLegacyWalletConnectRepository.kt | 13 +- .../domain/LegacyWalletConnectRepository.kt | 3 +- .../domain/WalletConnectInteractor.kt | 13 +- .../walletconnect/WalletConnectAction.kt | 2 + .../walletconnect/WalletConnectMiddleware.kt | 6 +- .../DefaultWalletConnectComponent.kt | 9 +- .../ui/walletconnect/WalletConnectModel.kt | 13 +- .../api/WalletConnectComponent.kt | 4 +- .../tangem/tap/routing/utils/ChildFactory.kt | 4 +- .../com/tangem/common/routing/AppRoute.kt | 2 +- .../walletconnect/DefaultWcPairUseCaseTest.kt | 3 +- .../walletconnect/model/WcPairRequest.kt | 2 + .../preview/PreviewDetailsComponent.kt | 8 +- .../features/details/model/DetailsModel.kt | 1 + .../features/details/utils/ItemsBuilder.kt | 8 +- .../components/WalletConnectEntryComponent.kt | 4 +- features/walletconnect/impl/build.gradle.kts | 12 +- .../components/ConnectionsComponent.kt | 6 +- .../DefaultWalletConnectEntryComponent.kt | 12 +- .../components/WcAppInfoContainerComponent.kt | 50 +++++-- .../components/WcSelectWalletComponent.kt | 130 +++++++++++++++++ .../connections/entity/WcAppInfoUM.kt | 1 + .../connections/entity/WcAppInfoWalletUM.kt | 10 ++ .../connections/model/WcAppInfoModel.kt | 87 +++++++++-- .../connections/model/WcConnectionsModel.kt | 16 +- .../transformers/WcAppInfoTransformer.kt | 28 +--- .../WcAppInfoWalletChangedTransformer.kt | 22 +++ .../transformers/WcNetworksInfoConverter.kt | 32 ++++ .../routing/DefaultWcRoutingComponent.kt | 6 +- .../connections/ui/WcAppInfoContent.kt | 9 +- .../connections/utils/WcUserWalletsFetcher.kt | 137 ++++++++++++++++++ 31 files changed, 568 insertions(+), 85 deletions(-) create mode 100644 features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/components/WcSelectWalletComponent.kt create mode 100644 features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/entity/WcAppInfoWalletUM.kt create mode 100644 features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/model/transformers/WcAppInfoWalletChangedTransformer.kt create mode 100644 features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/model/transformers/WcNetworksInfoConverter.kt create mode 100644 features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/utils/WcUserWalletsFetcher.kt diff --git a/app/src/main/java/com/tangem/tap/domain/walletconnect2/data/DefaultLegacyWalletConnectRepository.kt b/app/src/main/java/com/tangem/tap/domain/walletconnect2/data/DefaultLegacyWalletConnectRepository.kt index 689db8c1c0..0ac30a5cb0 100644 --- a/app/src/main/java/com/tangem/tap/domain/walletconnect2/data/DefaultLegacyWalletConnectRepository.kt +++ b/app/src/main/java/com/tangem/tap/domain/walletconnect2/data/DefaultLegacyWalletConnectRepository.kt @@ -13,6 +13,7 @@ import com.tangem.domain.walletconnect.WcPairService import com.tangem.domain.walletconnect.model.WcPairRequest import com.tangem.domain.walletconnect.model.legacy.Account import com.tangem.domain.walletconnect.usecase.initialize.WcInitializeUseCase +import com.tangem.domain.wallets.models.UserWalletId import com.tangem.features.walletconnect.components.WalletConnectFeatureToggles import com.tangem.tap.common.analytics.events.WalletConnect import com.tangem.tap.domain.walletconnect2.app.TangemWcBlockchainHelper @@ -64,14 +65,18 @@ internal class DefaultLegacyWalletConnectRepositoryFacade constructor( if (isNewWc) stub.updateSessions() else legacy.updateSessions() } - override fun pair(uri: String, source: SourceType) { + override fun pair(userWalletId: UserWalletId, uri: String, source: SourceType) { val src = when (source) { SourceType.QR -> WcPairRequest.Source.QR SourceType.DEEPLINK -> WcPairRequest.Source.DEEPLINK SourceType.CLIPBOARD -> WcPairRequest.Source.CLIPBOARD SourceType.ETC -> WcPairRequest.Source.ETC } - if (isNewWc) wcPairService.pair(WcPairRequest(uri, src)) else legacy.pair(uri, source) + if (isNewWc) { + wcPairService.pair(WcPairRequest(uri = uri, source = src, userWalletId = userWalletId)) + } else { + legacy.pair(userWalletId = userWalletId, uri = uri, source = source) + } } override fun disconnect(topic: String) { @@ -110,7 +115,7 @@ internal class LegacyWalletConnectRepositoryStub : LegacyWalletConnectRepository override fun updateSessions() = Unit - override fun pair(uri: String, source: SourceType) = Unit + override fun pair(userWalletId: UserWalletId, uri: String, source: SourceType) = Unit override fun disconnect(topic: String) = Unit @@ -405,7 +410,7 @@ internal class DefaultLegacyWalletConnectRepository( this.userNamespaces = userNamespaces } - override fun pair(uri: String, source: SourceType) { + override fun pair(userWalletId: UserWalletId, uri: String, source: SourceType) { analyticsHandler.send(WalletConnect.NewSessionInitiated(source = source)) WalletKit.pair( params = Wallet.Params.Pair(uri), diff --git a/app/src/main/java/com/tangem/tap/domain/walletconnect2/domain/LegacyWalletConnectRepository.kt b/app/src/main/java/com/tangem/tap/domain/walletconnect2/domain/LegacyWalletConnectRepository.kt index 59d5954e00..c0e22a3b7d 100644 --- a/app/src/main/java/com/tangem/tap/domain/walletconnect2/domain/LegacyWalletConnectRepository.kt +++ b/app/src/main/java/com/tangem/tap/domain/walletconnect2/domain/LegacyWalletConnectRepository.kt @@ -1,6 +1,7 @@ package com.tangem.tap.domain.walletconnect2.domain import com.tangem.domain.walletconnect.model.legacy.Account +import com.tangem.domain.wallets.models.UserWalletId import com.tangem.tap.domain.walletconnect2.domain.models.* import com.tangem.tap.features.details.redux.walletconnect.WalletConnectAction.OpenSession.SourceType import kotlinx.coroutines.flow.Flow @@ -19,7 +20,7 @@ interface LegacyWalletConnectRepository { fun updateSessions() - fun pair(uri: String, source: SourceType) + fun pair(userWalletId: UserWalletId, uri: String, source: SourceType) fun disconnect(topic: String) diff --git a/app/src/main/java/com/tangem/tap/domain/walletconnect2/domain/WalletConnectInteractor.kt b/app/src/main/java/com/tangem/tap/domain/walletconnect2/domain/WalletConnectInteractor.kt index d8d5a989ae..92418004d0 100644 --- a/app/src/main/java/com/tangem/tap/domain/walletconnect2/domain/WalletConnectInteractor.kt +++ b/app/src/main/java/com/tangem/tap/domain/walletconnect2/domain/WalletConnectInteractor.kt @@ -12,6 +12,7 @@ import com.tangem.domain.walletconnect.model.legacy.WalletConnectSessionsReposit import com.tangem.domain.walletmanager.WalletManagersFacade import com.tangem.domain.wallets.legacy.UserWalletsListManager import com.tangem.domain.wallets.models.UserWallet +import com.tangem.domain.wallets.models.UserWalletId import com.tangem.domain.wallets.usecase.GetSelectedWalletUseCase import com.tangem.tap.common.extensions.dispatchOnMain import com.tangem.tap.common.extensions.filterNotNull @@ -139,7 +140,11 @@ class WalletConnectInteractor( if (deeplinkStack.empty()) return val lastDeeplink = deeplinkStack.pop() val action = WalletConnectAction - .OpenSession(lastDeeplink, WalletConnectAction.OpenSession.SourceType.DEEPLINK) + .OpenSession( + wcUri = lastDeeplink, + source = WalletConnectAction.OpenSession.SourceType.DEEPLINK, + userWalletId = UserWalletId(userWalletId), + ) store.dispatchOnMain(action) }.onFailure { Timber.e("WC deeplink handling failed. $it") @@ -393,7 +398,11 @@ class WalletConnectInteractor( } if (isWalletConnectReadyForDeepLinks) { - val action = WalletConnectAction.OpenSession(deeplink, WalletConnectAction.OpenSession.SourceType.DEEPLINK) + val action = WalletConnectAction.OpenSession( + wcUri = deeplink, + source = WalletConnectAction.OpenSession.SourceType.DEEPLINK, + userWalletId = UserWalletId(userWalletId), + ) store.dispatchOnMain(action) } else { deeplinkStack.push(deeplink) diff --git a/app/src/main/java/com/tangem/tap/features/details/redux/walletconnect/WalletConnectAction.kt b/app/src/main/java/com/tangem/tap/features/details/redux/walletconnect/WalletConnectAction.kt index 7391c77f5b..4288f711b9 100644 --- a/app/src/main/java/com/tangem/tap/features/details/redux/walletconnect/WalletConnectAction.kt +++ b/app/src/main/java/com/tangem/tap/features/details/redux/walletconnect/WalletConnectAction.kt @@ -1,5 +1,6 @@ package com.tangem.tap.features.details.redux.walletconnect +import com.tangem.domain.wallets.models.UserWalletId import com.tangem.tap.domain.walletconnect2.domain.WcPreparedRequest import com.tangem.tap.domain.walletconnect2.domain.models.WalletConnectError import com.tangem.tap.domain.walletconnect2.domain.models.WalletConnectEvents @@ -14,6 +15,7 @@ sealed class WalletConnectAction : Action { data class OpenSession( val wcUri: String, val source: SourceType, + val userWalletId: UserWalletId, ) : WalletConnectAction() { enum class SourceType { QR, DEEPLINK, CLIPBOARD, ETC } } diff --git a/app/src/main/java/com/tangem/tap/features/details/redux/walletconnect/WalletConnectMiddleware.kt b/app/src/main/java/com/tangem/tap/features/details/redux/walletconnect/WalletConnectMiddleware.kt index 6bbd548de4..7439241a50 100644 --- a/app/src/main/java/com/tangem/tap/features/details/redux/walletconnect/WalletConnectMiddleware.kt +++ b/app/src/main/java/com/tangem/tap/features/details/redux/walletconnect/WalletConnectMiddleware.kt @@ -63,7 +63,11 @@ class WalletConnectMiddleware { is WalletConnectAction.OpenSession -> { val index = action.wcUri.indexOf("@") when (action.wcUri[index + 1]) { - '2' -> walletConnectRepository.pair(uri = action.wcUri, source = action.source) + '2' -> walletConnectRepository.pair( + uri = action.wcUri, + source = action.source, + userWalletId = action.userWalletId, + ) '1' -> { store.dispatchOnMain(WalletConnectAction.UnsupportedDappRequest) store.dispatchOnMain( diff --git a/app/src/main/java/com/tangem/tap/features/details/ui/walletconnect/DefaultWalletConnectComponent.kt b/app/src/main/java/com/tangem/tap/features/details/ui/walletconnect/DefaultWalletConnectComponent.kt index 73a0989519..0974e0db1b 100644 --- a/app/src/main/java/com/tangem/tap/features/details/ui/walletconnect/DefaultWalletConnectComponent.kt +++ b/app/src/main/java/com/tangem/tap/features/details/ui/walletconnect/DefaultWalletConnectComponent.kt @@ -22,10 +22,10 @@ import org.rekotlin.StoreSubscriber @Suppress("UnusedPrivateMember") internal class DefaultWalletConnectComponent @AssistedInject constructor( @Assisted appComponentContext: AppComponentContext, - @Assisted params: Unit, + @Assisted params: WalletConnectComponent.Params, ) : WalletConnectComponent, AppComponentContext by appComponentContext, StoreSubscriber { - private val model: WalletConnectModel = getOrCreateModel() + private val model: WalletConnectModel = getOrCreateModel(params) private var screenState: MutableState = mutableStateOf(model.updateState(store.state.walletConnectState)) @@ -65,6 +65,9 @@ internal class DefaultWalletConnectComponent @AssistedInject constructor( @AssistedFactory interface Factory : WalletConnectComponent.Factory { - override fun create(context: AppComponentContext, params: Unit): DefaultWalletConnectComponent + override fun create( + context: AppComponentContext, + params: WalletConnectComponent.Params, + ): DefaultWalletConnectComponent } } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/details/ui/walletconnect/WalletConnectModel.kt b/app/src/main/java/com/tangem/tap/features/details/ui/walletconnect/WalletConnectModel.kt index 21deae59a7..762b7afb71 100644 --- a/app/src/main/java/com/tangem/tap/features/details/ui/walletconnect/WalletConnectModel.kt +++ b/app/src/main/java/com/tangem/tap/features/details/ui/walletconnect/WalletConnectModel.kt @@ -4,10 +4,12 @@ import androidx.compose.runtime.Stable import arrow.core.getOrElse 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.qrscanning.models.SourceType import com.tangem.domain.qrscanning.usecases.ListenToQrScanningUseCase import com.tangem.tap.features.details.redux.walletconnect.WalletConnectAction import com.tangem.tap.features.details.redux.walletconnect.WalletConnectState +import com.tangem.tap.features.details.ui.walletconnect.api.WalletConnectComponent import com.tangem.tap.store import com.tangem.utils.coroutines.CoroutineDispatcherProvider import kotlinx.collections.immutable.toImmutableList @@ -22,13 +24,22 @@ import javax.inject.Inject internal class WalletConnectModel @Inject constructor( override val dispatchers: CoroutineDispatcherProvider, private val listenToQrScanningUseCase: ListenToQrScanningUseCase, + paramsContainer: ParamsContainer, ) : Model() { + private val params = paramsContainer.require() + init { modelScope.launch { listenToQrScanningUseCase(SourceType.WALLET_CONNECT) .getOrElse { emptyFlow() } - .map { WalletConnectAction.OpenSession(it, WalletConnectAction.OpenSession.SourceType.QR) } + .map { + WalletConnectAction.OpenSession( + wcUri = it, + source = WalletConnectAction.OpenSession.SourceType.QR, + userWalletId = params.userWalletId, + ) + } .collect { store.dispatch(it) } } } diff --git a/app/src/main/java/com/tangem/tap/features/details/ui/walletconnect/api/WalletConnectComponent.kt b/app/src/main/java/com/tangem/tap/features/details/ui/walletconnect/api/WalletConnectComponent.kt index 4e1f4ab3fb..6b9d8c4330 100644 --- a/app/src/main/java/com/tangem/tap/features/details/ui/walletconnect/api/WalletConnectComponent.kt +++ b/app/src/main/java/com/tangem/tap/features/details/ui/walletconnect/api/WalletConnectComponent.kt @@ -2,7 +2,9 @@ package com.tangem.tap.features.details.ui.walletconnect.api import com.tangem.core.decompose.factory.ComponentFactory import com.tangem.core.ui.decompose.ComposableContentComponent +import com.tangem.domain.wallets.models.UserWalletId interface WalletConnectComponent : ComposableContentComponent { - interface Factory : ComponentFactory + data class Params(val userWalletId: UserWalletId) + interface Factory : ComponentFactory } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/routing/utils/ChildFactory.kt b/app/src/main/java/com/tangem/tap/routing/utils/ChildFactory.kt index 0f2b7d5303..f1cd71bbe3 100644 --- a/app/src/main/java/com/tangem/tap/routing/utils/ChildFactory.kt +++ b/app/src/main/java/com/tangem/tap/routing/utils/ChildFactory.kt @@ -291,13 +291,13 @@ internal class ChildFactory @Inject constructor( if (walletConnectFeatureToggles.isRedesignedWalletConnectEnabled) { createComponentChild( context = context, - params = Unit, + params = RedesignedWalletConnectComponent.Params(route.userWalletId), componentFactory = redesignedWalletConnectComponentFactory, ) } else { createComponentChild( context = context, - params = Unit, + params = WalletConnectComponent.Params(route.userWalletId), componentFactory = walletConnectComponentFactory, ) } diff --git a/common/routing/src/main/kotlin/com/tangem/common/routing/AppRoute.kt b/common/routing/src/main/kotlin/com/tangem/common/routing/AppRoute.kt index 0a280474be..af27366ceb 100644 --- a/common/routing/src/main/kotlin/com/tangem/common/routing/AppRoute.kt +++ b/common/routing/src/main/kotlin/com/tangem/common/routing/AppRoute.kt @@ -122,7 +122,7 @@ sealed class AppRoute(val path: String) : Route { } @Serializable - data object WalletConnectSessions : AppRoute(path = "/wallet_connect_sessions") + data class WalletConnectSessions(val userWalletId: UserWalletId) : AppRoute(path = "/wallet_connect_sessions") @Serializable data class QrScanning(val source: Source) : AppRoute(path = "/$source/qr_scanning${source.path}") { diff --git a/data/wallet-connect/src/test/kotlin/com/tangem/domain/walletconnect/DefaultWcPairUseCaseTest.kt b/data/wallet-connect/src/test/kotlin/com/tangem/domain/walletconnect/DefaultWcPairUseCaseTest.kt index 1792340585..9b0b650724 100644 --- a/data/wallet-connect/src/test/kotlin/com/tangem/domain/walletconnect/DefaultWcPairUseCaseTest.kt +++ b/data/wallet-connect/src/test/kotlin/com/tangem/domain/walletconnect/DefaultWcPairUseCaseTest.kt @@ -20,6 +20,7 @@ import com.tangem.domain.walletconnect.model.WcSession import com.tangem.domain.walletconnect.model.WcSessionApprove import com.tangem.domain.walletconnect.repository.WcSessionsManager import com.tangem.domain.walletconnect.usecase.pair.WcPairState +import com.tangem.domain.wallets.models.UserWalletId import io.mockk.coEvery import io.mockk.coVerifyOrder import io.mockk.mockk @@ -99,7 +100,7 @@ internal class DefaultWcPairUseCaseTest { caipNamespaceDelegate = caipNamespaceDelegate, sdkDelegate = sdkDelegate, blockAidVerifier = blockAidVerifier, - pairRequest = WcPairRequest(url, source), + pairRequest = WcPairRequest(userWalletId = UserWalletId(""), uri = url, source = source), ) @Before diff --git a/domain/wallet-connect/models/src/main/java/com/tangem/domain/walletconnect/model/WcPairRequest.kt b/domain/wallet-connect/models/src/main/java/com/tangem/domain/walletconnect/model/WcPairRequest.kt index 1c1c3f3674..2d70af0e55 100644 --- a/domain/wallet-connect/models/src/main/java/com/tangem/domain/walletconnect/model/WcPairRequest.kt +++ b/domain/wallet-connect/models/src/main/java/com/tangem/domain/walletconnect/model/WcPairRequest.kt @@ -1,11 +1,13 @@ package com.tangem.domain.walletconnect.model +import com.tangem.domain.wallets.models.UserWalletId import kotlinx.serialization.Serializable @Serializable data class WcPairRequest( val uri: String, val source: Source, + val userWalletId: UserWalletId, ) { enum class Source { QR, DEEPLINK, CLIPBOARD, ETC } } \ No newline at end of file diff --git a/features/details/impl/src/main/kotlin/com/tangem/features/details/component/preview/PreviewDetailsComponent.kt b/features/details/impl/src/main/kotlin/com/tangem/features/details/component/preview/PreviewDetailsComponent.kt index 37abe05472..8efcb31776 100644 --- a/features/details/impl/src/main/kotlin/com/tangem/features/details/component/preview/PreviewDetailsComponent.kt +++ b/features/details/impl/src/main/kotlin/com/tangem/features/details/component/preview/PreviewDetailsComponent.kt @@ -5,6 +5,7 @@ import androidx.compose.ui.Modifier import com.tangem.core.decompose.navigation.DummyRouter import com.tangem.core.navigation.url.DummyUrlOpener import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig +import com.tangem.domain.wallets.models.UserWalletId import com.tangem.features.details.component.DetailsComponent import com.tangem.features.details.entity.DetailsFooterUM import com.tangem.features.details.entity.DetailsUM @@ -18,7 +19,12 @@ internal class PreviewDetailsComponent : DetailsComponent { private val previewBlocks = runBlocking { ItemsBuilder( router = DummyRouter(), - ).buildAll(isWalletConnectAvailable = true, onSupportClick = {}, onBuyClick = {}) + ).buildAll( + isWalletConnectAvailable = true, + userWalletId = UserWalletId(""), + onSupportClick = {}, + onBuyClick = {}, + ) } private val previewFooter = DetailsFooterUM( diff --git a/features/details/impl/src/main/kotlin/com/tangem/features/details/model/DetailsModel.kt b/features/details/impl/src/main/kotlin/com/tangem/features/details/model/DetailsModel.kt index d2068c9fcf..10afab1fde 100644 --- a/features/details/impl/src/main/kotlin/com/tangem/features/details/model/DetailsModel.kt +++ b/features/details/impl/src/main/kotlin/com/tangem/features/details/model/DetailsModel.kt @@ -80,6 +80,7 @@ internal class DetailsModel @Inject constructor( items = MutableStateFlow( itemsBuilder.buildAll( isWalletConnectAvailable = isWalletConnectAvailable, + userWalletId = params.userWalletId, onSupportClick = ::sendFeedback, onBuyClick = ::onBuyClick, ), diff --git a/features/details/impl/src/main/kotlin/com/tangem/features/details/utils/ItemsBuilder.kt b/features/details/impl/src/main/kotlin/com/tangem/features/details/utils/ItemsBuilder.kt index 7cd8933ad5..f0fd520172 100644 --- a/features/details/impl/src/main/kotlin/com/tangem/features/details/utils/ItemsBuilder.kt +++ b/features/details/impl/src/main/kotlin/com/tangem/features/details/utils/ItemsBuilder.kt @@ -6,6 +6,7 @@ import com.tangem.core.decompose.navigation.Router import com.tangem.core.ui.components.block.model.BlockUM import com.tangem.core.ui.extensions.resourceReference import com.tangem.core.ui.extensions.stringReference +import com.tangem.domain.wallets.models.UserWalletId import com.tangem.features.details.entity.DetailsItemUM import com.tangem.features.details.impl.BuildConfig import com.tangem.features.details.impl.R @@ -19,20 +20,21 @@ internal class ItemsBuilder @Inject constructor(private val router: Router) { fun buildAll( isWalletConnectAvailable: Boolean, + userWalletId: UserWalletId, onSupportClick: () -> Unit, onBuyClick: () -> Unit, ): ImmutableList = buildList { - buildWalletConnectBlock(isWalletConnectAvailable)?.let(::add) + buildWalletConnectBlock(isWalletConnectAvailable, userWalletId)?.let(::add) buildUserWalletListBlock().let(::add) buildShopBlock(onBuyClick).let(::add) buildSettingsBlock().let(::add) buildSupportBlock(onSupportClick).let(::add) }.toImmutableList() - private fun buildWalletConnectBlock(isWalletConnectAvailable: Boolean): DetailsItemUM? { + private fun buildWalletConnectBlock(isWalletConnectAvailable: Boolean, userWalletId: UserWalletId): DetailsItemUM? { return if (isWalletConnectAvailable) { DetailsItemUM.WalletConnect( - onClick = { router.push(AppRoute.WalletConnectSessions) }, + onClick = { router.push(AppRoute.WalletConnectSessions(userWalletId)) }, ) } else { null diff --git a/features/walletconnect/api/src/main/kotlin/com/tangem/features/walletconnect/components/WalletConnectEntryComponent.kt b/features/walletconnect/api/src/main/kotlin/com/tangem/features/walletconnect/components/WalletConnectEntryComponent.kt index c7d60d1091..2b584ceaae 100644 --- a/features/walletconnect/api/src/main/kotlin/com/tangem/features/walletconnect/components/WalletConnectEntryComponent.kt +++ b/features/walletconnect/api/src/main/kotlin/com/tangem/features/walletconnect/components/WalletConnectEntryComponent.kt @@ -2,7 +2,9 @@ package com.tangem.features.walletconnect.components import com.tangem.core.decompose.factory.ComponentFactory import com.tangem.core.ui.decompose.ComposableContentComponent +import com.tangem.domain.wallets.models.UserWalletId interface WalletConnectEntryComponent : ComposableContentComponent { - interface Factory : ComponentFactory + data class Params(val userWalletId: UserWalletId) + interface Factory : ComponentFactory } \ No newline at end of file diff --git a/features/walletconnect/impl/build.gradle.kts b/features/walletconnect/impl/build.gradle.kts index 02b082fd94..77ae59129c 100644 --- a/features/walletconnect/impl/build.gradle.kts +++ b/features/walletconnect/impl/build.gradle.kts @@ -12,9 +12,8 @@ android { } dependencies { + implementation(projects.features.wallet.api) implementation(projects.features.walletconnect.api) - implementation(projects.domain.walletConnect) - implementation(projects.domain.walletConnect.models) /** Core */ implementation(projects.core.configToggles) @@ -24,13 +23,22 @@ dependencies { implementation(projects.common.ui) /** Domain models */ + implementation(projects.domain.appCurrency.models) + implementation(projects.domain.balanceHiding.models) implementation(projects.domain.blockaid.models) + implementation(projects.domain.models) implementation(projects.domain.qrScanning.models) implementation(projects.domain.tokens.models) implementation(projects.domain.wallets.models) + implementation(projects.domain.walletConnect) + implementation(projects.domain.walletConnect.models) /** Domain */ + implementation(projects.domain.appCurrency) + implementation(projects.domain.balanceHiding) implementation(projects.domain.qrScanning) + implementation(projects.domain.tokens) + implementation(projects.domain.wallets) /** DI */ implementation(deps.hilt.android) diff --git a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/components/ConnectionsComponent.kt b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/components/ConnectionsComponent.kt index 6c6a74e65a..45e927da97 100644 --- a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/components/ConnectionsComponent.kt +++ b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/components/ConnectionsComponent.kt @@ -13,15 +13,17 @@ import com.tangem.core.decompose.context.childByContext import com.tangem.core.decompose.model.getOrCreateModel import com.tangem.core.ui.decompose.ComposableBottomSheetComponent import com.tangem.core.ui.decompose.ComposableContentComponent +import com.tangem.domain.wallets.models.UserWalletId import com.tangem.features.walletconnect.connections.model.WcConnectionsModel import com.tangem.features.walletconnect.connections.routes.WcConnectionsBottomSheetConfig import com.tangem.features.walletconnect.connections.ui.WcConnectionsContent internal class ConnectionsComponent( appComponentContext: AppComponentContext, + params: Params, ) : AppComponentContext by appComponentContext, ComposableContentComponent { - private val model: WcConnectionsModel = getOrCreateModel() + private val model: WcConnectionsModel = getOrCreateModel(params) private val bottomSheetSlot = childSlot( source = model.bottomSheetNavigation, serializer = null, @@ -52,4 +54,6 @@ internal class ConnectionsComponent( ) } } + + data class Params(val userWalletId: UserWalletId) } \ No newline at end of file diff --git a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/components/DefaultWalletConnectEntryComponent.kt b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/components/DefaultWalletConnectEntryComponent.kt index 3f3ee812d0..f6cce51fd1 100644 --- a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/components/DefaultWalletConnectEntryComponent.kt +++ b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/components/DefaultWalletConnectEntryComponent.kt @@ -22,7 +22,7 @@ import dagger.assisted.AssistedInject @Suppress("UnusedPrivateMember") internal class DefaultWalletConnectEntryComponent @AssistedInject constructor( @Assisted private val appComponentContext: AppComponentContext, - @Assisted params: Unit, + @Assisted private val params: WalletConnectEntryComponent.Params, ) : AppComponentContext by appComponentContext, WalletConnectEntryComponent { private val contentNavigation = StackNavigation() @@ -47,12 +47,18 @@ internal class DefaultWalletConnectEntryComponent @AssistedInject constructor( config: ConnectionsInnerRoute, componentContext: ComponentContext, ): ComposableContentComponent = when (config) { - ConnectionsInnerRoute.Connections -> ConnectionsComponent(childByContext(componentContext)) + ConnectionsInnerRoute.Connections -> ConnectionsComponent( + appComponentContext = childByContext(componentContext), + params = ConnectionsComponent.Params(params.userWalletId), + ) ConnectionsInnerRoute.QrScan -> TODO("[REDACTED_JIRA]") } @AssistedFactory interface Factory : WalletConnectEntryComponent.Factory { - override fun create(context: AppComponentContext, params: Unit): DefaultWalletConnectEntryComponent + override fun create( + context: AppComponentContext, + params: WalletConnectEntryComponent.Params, + ): DefaultWalletConnectEntryComponent } } \ No newline at end of file diff --git a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/components/WcAppInfoContainerComponent.kt b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/components/WcAppInfoContainerComponent.kt index bcfbcc79ea..be95b71cd3 100644 --- a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/components/WcAppInfoContainerComponent.kt +++ b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/components/WcAppInfoContainerComponent.kt @@ -1,13 +1,12 @@ package com.tangem.features.walletconnect.connections.components +import androidx.compose.animation.animateContentSize import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.ui.Modifier import com.arkivanov.decompose.ComponentContext import com.arkivanov.decompose.extensions.compose.stack.Children -import com.arkivanov.decompose.extensions.compose.stack.animation.stackAnimation import com.arkivanov.decompose.extensions.compose.subscribeAsState -import com.arkivanov.decompose.router.stack.StackNavigation import com.arkivanov.decompose.router.stack.childStack import com.arkivanov.decompose.router.stack.pop import com.tangem.core.decompose.context.AppComponentContext @@ -18,8 +17,10 @@ import com.tangem.core.ui.components.bottomsheets.modal.TangemModalBottomSheet import com.tangem.core.ui.components.bottomsheets.modal.TangemModalBottomSheetTitle import com.tangem.core.ui.decompose.ComposableContentComponent import com.tangem.core.ui.extensions.resourceReference +import com.tangem.core.ui.extensions.stringReference import com.tangem.core.ui.res.TangemTheme import com.tangem.domain.walletconnect.model.WcPairRequest +import com.tangem.domain.wallets.models.UserWalletId import com.tangem.features.walletconnect.connections.model.WcAppInfoModel import com.tangem.features.walletconnect.connections.routes.WcAppInfoRoutes import com.tangem.features.walletconnect.impl.R @@ -30,9 +31,8 @@ internal class WcAppInfoContainerComponent( ) : AppComponentContext by appComponentContext, ComposableContentComponent { private val model: WcAppInfoModel = getOrCreateModel(params = params) - private val contentNavigation = StackNavigation() private val contentStack = childStack( - source = contentNavigation, + source = model.contentNavigation, serializer = WcAppInfoRoutes.serializer(), initialConfiguration = WcAppInfoRoutes.AppInfo, childFactory = ::screenChild, @@ -51,10 +51,17 @@ internal class WcAppInfoContainerComponent( onDismissRequest = ::dismiss, content = content.active.configuration, ), - containerColor = TangemTheme.colors.background.tertiary, + onBack = ::contentBack, + containerColor = when (content.active.configuration) { + is WcAppInfoRoutes.Alert, + is WcAppInfoRoutes.SelectNetworks, + is WcAppInfoRoutes.AppInfo, + -> TangemTheme.colors.background.tertiary + is WcAppInfoRoutes.SelectWallet -> TangemTheme.colors.background.primary + }, title = { config -> Title(route = config) }, content = { - Children(stack = content, animation = stackAnimation()) { child -> + Children(modifier = Modifier.animateContentSize(), stack = content) { child -> child.instance.Content(modifier = Modifier) } }, @@ -64,7 +71,12 @@ internal class WcAppInfoContainerComponent( @Composable private fun Title(route: WcAppInfoRoutes) { TangemModalBottomSheetTitle( - title = resourceReference(R.string.wc_wallet_connect), + title = when (route) { + is WcAppInfoRoutes.Alert -> null + WcAppInfoRoutes.AppInfo -> resourceReference(R.string.wc_wallet_connect) + WcAppInfoRoutes.SelectNetworks -> TODO("[REDACTED_TASK_KEY]") + WcAppInfoRoutes.SelectWallet -> stringReference("Choose wallet") + }, startIconRes = when (route) { is WcAppInfoRoutes.AppInfo, is WcAppInfoRoutes.Alert -> null is WcAppInfoRoutes.SelectNetworks, is WcAppInfoRoutes.SelectWallet -> R.drawable.ic_back_24 @@ -88,19 +100,25 @@ internal class WcAppInfoContainerComponent( } private fun contentBack() { - contentNavigation.pop() + if (contentStack.value.active.configuration == WcAppInfoRoutes.AppInfo) { + dismiss() + } else { + model.contentNavigation.pop() + } } - private fun screenChild(config: WcAppInfoRoutes, componentContext: ComponentContext): ComposableContentComponent = - when (config) { - is WcAppInfoRoutes.AppInfo -> WcAppInfoComponent( - appComponentContext = childByContext(componentContext), - model = model, - ) + private fun screenChild(config: WcAppInfoRoutes, componentContext: ComponentContext): ComposableContentComponent { + val appComponentContext = childByContext(componentContext) + return when (config) { + is WcAppInfoRoutes.AppInfo -> WcAppInfoComponent(appComponentContext = appComponentContext, model = model) is WcAppInfoRoutes.Alert -> TODO() WcAppInfoRoutes.SelectNetworks -> TODO() - WcAppInfoRoutes.SelectWallet -> TODO() + WcAppInfoRoutes.SelectWallet -> WcSelectWalletComponent( + appComponentContext = appComponentContext, + model = model, + ) } + } - data class Params(val wcUrl: String, val source: WcPairRequest.Source) + data class Params(val userWalletId: UserWalletId, val wcUrl: String, val source: WcPairRequest.Source) } \ No newline at end of file diff --git a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/components/WcSelectWalletComponent.kt b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/components/WcSelectWalletComponent.kt new file mode 100644 index 0000000000..d6c7e19964 --- /dev/null +++ b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/components/WcSelectWalletComponent.kt @@ -0,0 +1,130 @@ +package com.tangem.features.walletconnect.connections.components + +import android.content.res.Configuration +import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.key +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.tooling.preview.Devices +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.compose.ui.util.fastForEach +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import com.tangem.common.ui.userwallet.UserWalletItem +import com.tangem.common.ui.userwallet.state.UserWalletItemUM +import com.tangem.core.decompose.context.AppComponentContext +import com.tangem.core.ui.components.block.TangemBlockCardColors +import com.tangem.core.ui.decompose.ComposableContentComponent +import com.tangem.core.ui.extensions.stringReference +import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.res.TangemThemePreview +import com.tangem.domain.wallets.models.UserWalletId +import com.tangem.features.walletconnect.connections.model.WcAppInfoModel +import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.persistentListOf + +internal class WcSelectWalletComponent( + appComponentContext: AppComponentContext, + private val model: WcAppInfoModel, +) : AppComponentContext by appComponentContext, ComposableContentComponent { + + @Composable + override fun Content(modifier: Modifier) { + val walletsState by model.walletsUiState.collectAsStateWithLifecycle() + WcSelectWalletContent( + modifier = modifier.padding(start = 16.dp, end = 16.dp, bottom = 16.dp), + wallets = walletsState.wallets, + selectedWalletId = walletsState.selectedUserWallet.walletId, + ) + } +} + +@Composable +private fun WcSelectWalletContent( + wallets: ImmutableList, + selectedWalletId: UserWalletId, + modifier: Modifier = Modifier, +) { + Column(modifier = modifier) { + wallets.fastForEach { state -> + key(state.id) { + val baseModifier = Modifier + .clip(RoundedCornerShape(14.dp)) + .clickable(onClick = state.onClick) + val itemModifier = if (state.id == selectedWalletId) { + baseModifier.border( + width = 1.dp, + color = TangemTheme.colors.text.accent, + shape = RoundedCornerShape(14.dp), + ) + } else { + baseModifier + } + UserWalletItem( + modifier = itemModifier, + state = state, + blockColors = TangemBlockCardColors.copy( + containerColor = Color.Unspecified, + disabledContainerColor = Color.Unspecified, + ), + ) + } + } + } +} + +@Composable +@Preview(showBackground = true, device = Devices.PIXEL_7_PRO) +@Preview(showBackground = true, device = Devices.PIXEL_7_PRO, uiMode = Configuration.UI_MODE_NIGHT_YES) +private fun WcSelectWalletContent_Preview() { + TangemThemePreview { + WcSelectWalletContent( + modifier = Modifier + .background(TangemTheme.colors.background.tertiary) + .padding(start = 16.dp, end = 16.dp, bottom = 16.dp), + selectedWalletId = UserWalletId("user_wallet_1".encodeToByteArray()), + wallets = persistentListOf( + UserWalletItemUM( + id = UserWalletId("user_wallet_1".encodeToByteArray()), + name = stringReference("Tangem 2.0"), + information = stringReference("42 tokens"), + balance = UserWalletItemUM.Balance.Loaded("1 496,34 \$", isFlickering = false), + isEnabled = true, + onClick = {}, + ), + UserWalletItemUM( + id = UserWalletId("user_wallet_2".encodeToByteArray()), + name = stringReference("Tangem White"), + information = stringReference("24 tokens"), + balance = UserWalletItemUM.Balance.Loaded("1 496,34 \$", isFlickering = false), + isEnabled = true, + onClick = {}, + ), + UserWalletItemUM( + id = UserWalletId("user_wallet_3".encodeToByteArray()), + name = stringReference("Bitcoin"), + information = stringReference("1 token"), + balance = UserWalletItemUM.Balance.Loaded("1 496,34 \$", isFlickering = false), + isEnabled = true, + onClick = {}, + ), + UserWalletItemUM( + id = UserWalletId("user_wallet_4".encodeToByteArray()), + name = stringReference("Tangem 1.0"), + information = stringReference("21 tokens"), + balance = UserWalletItemUM.Balance.Loaded("1 496,34 \$", isFlickering = false), + isEnabled = true, + onClick = {}, + ), + ), + ) + } +} \ No newline at end of file diff --git a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/entity/WcAppInfoUM.kt b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/entity/WcAppInfoUM.kt index f7d17a2af5..92e385a3fa 100644 --- a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/entity/WcAppInfoUM.kt +++ b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/entity/WcAppInfoUM.kt @@ -26,6 +26,7 @@ internal sealed class WcAppInfoUM : TangemBottomSheetConfigContent { val appSubtitle: String, val notification: WcAppInfoSecurityNotification?, val walletName: String, + val onWalletClick: () -> Unit, val networksInfo: WcNetworksInfo, override val connectButtonConfig: WcPrimaryButtonConfig, override val onDismiss: () -> Unit, diff --git a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/entity/WcAppInfoWalletUM.kt b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/entity/WcAppInfoWalletUM.kt new file mode 100644 index 0000000000..0c682702a0 --- /dev/null +++ b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/entity/WcAppInfoWalletUM.kt @@ -0,0 +1,10 @@ +package com.tangem.features.walletconnect.connections.entity + +import com.tangem.common.ui.userwallet.state.UserWalletItemUM +import com.tangem.domain.wallets.models.UserWallet +import kotlinx.collections.immutable.ImmutableList + +internal data class WcAppInfoWalletUM( + val wallets: ImmutableList, + val selectedUserWallet: UserWallet, +) \ No newline at end of file diff --git a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/model/WcAppInfoModel.kt b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/model/WcAppInfoModel.kt index 3a6e4520ba..fe9348ef4d 100644 --- a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/model/WcAppInfoModel.kt +++ b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/model/WcAppInfoModel.kt @@ -1,47 +1,98 @@ package com.tangem.features.walletconnect.connections.model import androidx.compose.runtime.Stable +import com.arkivanov.decompose.router.stack.StackNavigation +import com.arkivanov.decompose.router.stack.pop +import com.arkivanov.decompose.router.stack.pushNew +import com.tangem.common.ui.userwallet.state.UserWalletItemUM import com.tangem.core.decompose.di.ModelScoped import com.tangem.core.decompose.model.Model import com.tangem.core.decompose.model.ParamsContainer import com.tangem.core.decompose.navigation.Router +import com.tangem.core.decompose.ui.UiMessageSender +import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase +import com.tangem.domain.balancehiding.GetBalanceHidingSettingsUseCase +import com.tangem.domain.tokens.GetWalletTotalBalanceUseCase import com.tangem.domain.walletconnect.model.WcPairRequest import com.tangem.domain.walletconnect.model.WcSessionApprove import com.tangem.domain.walletconnect.model.WcSessionProposal import com.tangem.domain.walletconnect.usecase.pair.WcPairState import com.tangem.domain.walletconnect.usecase.pair.WcPairUseCase import com.tangem.domain.wallets.models.UserWallet +import com.tangem.domain.wallets.models.UserWalletId +import com.tangem.domain.wallets.usecase.GetCardImageUseCase +import com.tangem.domain.wallets.usecase.GetWalletsUseCase import com.tangem.features.walletconnect.connections.components.WcAppInfoContainerComponent import com.tangem.features.walletconnect.connections.entity.WcAppInfoUM +import com.tangem.features.walletconnect.connections.entity.WcAppInfoWalletUM import com.tangem.features.walletconnect.connections.entity.WcPrimaryButtonConfig import com.tangem.features.walletconnect.connections.model.transformers.WcAppInfoTransformer +import com.tangem.features.walletconnect.connections.model.transformers.WcAppInfoWalletChangedTransformer import com.tangem.features.walletconnect.connections.model.transformers.WcConnectButtonProgressTransformer +import com.tangem.features.walletconnect.connections.routes.WcAppInfoRoutes +import com.tangem.features.walletconnect.connections.utils.WcUserWalletsFetcher import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.persistentListOf import kotlinx.coroutines.flow.* import javax.inject.Inject import kotlin.properties.Delegates import com.tangem.utils.transformer.update as transformerUpdate +@Suppress("LongParameterList") @Stable @ModelScoped internal class WcAppInfoModel @Inject constructor( + private val router: Router, override val dispatchers: CoroutineDispatcherProvider, wcPairUseCaseFactory: WcPairUseCase.Factory, - private val router: Router, + getWalletsUseCase: GetWalletsUseCase, + getWalletTotalBalanceUseCase: GetWalletTotalBalanceUseCase, + getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase, + getBalanceHidingSettingsUseCase: GetBalanceHidingSettingsUseCase, + messageSender: UiMessageSender, + getCardImageUseCase: GetCardImageUseCase, paramsContainer: ParamsContainer, ) : Model() { private val params: WcAppInfoContainerComponent.Params = paramsContainer.require() - private val wcPairUseCase = wcPairUseCaseFactory.create(WcPairRequest(uri = params.wcUrl, source = params.source)) + private val wcPairUseCase = wcPairUseCaseFactory.create( + WcPairRequest( + userWalletId = params.userWalletId, + uri = params.wcUrl, + source = params.source, + ), + ) + private val userWalletsFetcher = WcUserWalletsFetcher( + getWalletsUseCase = getWalletsUseCase, + getWalletTotalBalanceUseCase = getWalletTotalBalanceUseCase, + getSelectedAppCurrencyUseCase = getSelectedAppCurrencyUseCase, + getBalanceHidingSettingsUseCase = getBalanceHidingSettingsUseCase, + messageSender = messageSender, + getCardImageUseCase = getCardImageUseCase, + onWalletSelected = ::onWalletSelected, + ) + + private val selectedUserWalletFlow = + MutableStateFlow(getWalletsUseCase.invokeSync().first { it.walletId == params.userWalletId }) + // TODO(wc) Doston: Temp solution, will be fixed in next PR`s + private var proposalNetwork by Delegates.notNull() + internal var sessionProposal by Delegates.notNull() + + // UI states + internal val contentNavigation = StackNavigation() val appInfoUiState: StateFlow field = MutableStateFlow(createLoadingState()) - - // TODO(wc) Doston: Temp solution, will be fixed in next PR`s - private var userWallet by Delegates.notNull() - private var proposalNetwork by Delegates.notNull() + val walletsUiState: StateFlow + field = MutableStateFlow(WcAppInfoWalletUM(persistentListOf(), selectedUserWalletFlow.value)) init { loadDAppInfo() + combine( + flow = userWalletsFetcher.userWallets, + flow2 = selectedUserWalletFlow, + transform = ::updateWalletsState, + ).launchIn(modelScope) } private fun loadDAppInfo() { @@ -62,14 +113,15 @@ internal class WcAppInfoModel @Inject constructor( } is WcPairState.Loading -> appInfoUiState.update { createLoadingState() } is WcPairState.Proposal -> { - userWallet = state.dAppSession.proposalNetwork.keys.first { !it.isLocked } - proposalNetwork = state.dAppSession.proposalNetwork.getValue(userWallet) + sessionProposal = state.dAppSession + proposalNetwork = sessionProposal.proposalNetwork.getValue(selectedUserWalletFlow.value) appInfoUiState.transformerUpdate( WcAppInfoTransformer( dAppSession = state.dAppSession, onDismiss = ::dismiss, onConnect = ::onConnect, - userWallet = userWallet, + onWalletClick = { contentNavigation.pushNew(WcAppInfoRoutes.SelectWallet) }, + userWallet = selectedUserWalletFlow.value, proposalNetwork = proposalNetwork, ), ) @@ -87,13 +139,24 @@ internal class WcAppInfoModel @Inject constructor( private fun onConnect() { wcPairUseCase.approve( WcSessionApprove( - wallet = userWallet, - network = - proposalNetwork.required.plus(proposalNetwork.available).toList(), + wallet = selectedUserWalletFlow.value, + network = proposalNetwork.required.plus(proposalNetwork.available).toList(), ), ) } + private fun onWalletSelected(userWalletId: UserWalletId) { + val selectedUserWallet = sessionProposal.proposalNetwork.keys.first { it.walletId == userWalletId } + selectedUserWalletFlow.update { selectedUserWallet } + proposalNetwork = sessionProposal.proposalNetwork.getValue(selectedUserWallet) + appInfoUiState.transformerUpdate(WcAppInfoWalletChangedTransformer(selectedUserWallet, proposalNetwork)) + contentNavigation.pop() + } + + private fun updateWalletsState(items: ImmutableList, userWallet: UserWallet) { + walletsUiState.update { state -> state.copy(wallets = items, selectedUserWallet = userWallet) } + } + private fun createLoadingState(): WcAppInfoUM.Loading { return WcAppInfoUM.Loading( onDismiss = ::dismiss, 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 49062a9de9..96d5df7e28 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 @@ -6,6 +6,7 @@ import com.arkivanov.decompose.router.slot.activate import com.tangem.common.routing.AppRoute import com.tangem.core.decompose.di.ModelScoped import com.tangem.core.decompose.model.Model +import com.tangem.core.decompose.model.ParamsContainer import com.tangem.core.decompose.navigation.Router import com.tangem.core.decompose.ui.UiMessageSender import com.tangem.core.ui.components.appbar.models.TopAppBarButtonUM @@ -16,11 +17,12 @@ import com.tangem.core.ui.message.EventMessageAction import com.tangem.core.ui.res.TangemTheme import com.tangem.domain.qrscanning.models.SourceType import com.tangem.domain.qrscanning.usecases.ListenToQrScanningUseCase -import com.tangem.domain.walletconnect.model.WcSession import com.tangem.domain.walletconnect.WcPairService import com.tangem.domain.walletconnect.model.WcPairRequest +import com.tangem.domain.walletconnect.model.WcSession import com.tangem.domain.walletconnect.usecase.WcSessionsUseCase import com.tangem.domain.walletconnect.usecase.disconnect.WcDisconnectUseCase +import com.tangem.features.walletconnect.connections.components.ConnectionsComponent import com.tangem.features.walletconnect.connections.entity.WcConnectionsState import com.tangem.features.walletconnect.connections.entity.WcConnectionsTopAppBarConfig import com.tangem.features.walletconnect.connections.model.transformers.WcSessionsTransformer @@ -44,8 +46,10 @@ internal class WcConnectionsModel @Inject constructor( private val wcDisconnectUseCase: WcDisconnectUseCase, private val wcPairService: WcPairService, override val dispatchers: CoroutineDispatcherProvider, + paramsContainer: ParamsContainer, ) : Model() { + private val params = paramsContainer.require() val uiState: StateFlow field = MutableStateFlow(getInitialState()) val bottomSheetNavigation: SlotNavigation = SlotNavigation() @@ -58,7 +62,15 @@ internal class WcConnectionsModel @Inject constructor( private fun listenQrUpdates() { listenToQrScanningUseCase(SourceType.WALLET_CONNECT) .getOrElse { emptyFlow() } - .onEach { wcUrl -> wcPairService.pair(WcPairRequest(wcUrl, WcPairRequest.Source.QR)) } + .onEach { wcUrl -> + wcPairService.pair( + WcPairRequest( + userWalletId = params.userWalletId, + uri = wcUrl, + source = WcPairRequest.Source.QR, + ), + ) + } .launchIn(modelScope) } diff --git a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/model/transformers/WcAppInfoTransformer.kt b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/model/transformers/WcAppInfoTransformer.kt index 134338c410..2249504df5 100644 --- a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/model/transformers/WcAppInfoTransformer.kt +++ b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/model/transformers/WcAppInfoTransformer.kt @@ -1,17 +1,16 @@ package com.tangem.features.walletconnect.connections.model.transformers import com.domain.blockaid.models.dapp.CheckDAppResult -import com.tangem.core.ui.extensions.iconResId import com.tangem.domain.walletconnect.model.WcSessionProposal import com.tangem.domain.wallets.models.UserWallet import com.tangem.features.walletconnect.connections.entity.* import com.tangem.utils.transformer.Transformer -import kotlinx.collections.immutable.toImmutableList internal class WcAppInfoTransformer( private val dAppSession: WcSessionProposal, private val onDismiss: () -> Unit, private val onConnect: () -> Unit, + private val onWalletClick: () -> Unit, private val userWallet: UserWallet, private val proposalNetwork: WcSessionProposal.ProposalNetwork, ) : Transformer { @@ -24,7 +23,8 @@ internal class WcAppInfoTransformer( appSubtitle = dAppSession.dAppMetaData.description, notification = createNotification(dAppSession.securityStatus), walletName = userWallet.name, - networksInfo = convertNetworksInfo(proposalNetwork), + onWalletClick = onWalletClick, + networksInfo = WcNetworksInfoConverter.convert(proposalNetwork), connectButtonConfig = WcPrimaryButtonConfig( showProgress = false, enabled = proposalNetwork.missingRequired.isEmpty(), @@ -41,26 +41,4 @@ internal class WcAppInfoTransformer( CheckDAppResult.FAILED_TO_VERIFY -> WcAppInfoSecurityNotification.UnknownDomain } } - - private fun convertNetworksInfo(proposalNetwork: WcSessionProposal.ProposalNetwork): WcNetworksInfo { - return if (proposalNetwork.missingRequired.isNotEmpty()) { - WcNetworksInfo.MissingRequiredNetworkInfo( - networks = proposalNetwork.missingRequired - .joinToString { it.name }, - ) - } else { - WcNetworksInfo.ContainsAllRequiredNetworks( - items = (proposalNetwork.required + proposalNetwork.available) - .map { - WcNetworkInfoItem( - id = it.id.value, - icon = it.iconResId, - name = it.name, - symbol = it.currencySymbol, - ) - } - .toImmutableList(), - ) - } - } } \ No newline at end of file diff --git a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/model/transformers/WcAppInfoWalletChangedTransformer.kt b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/model/transformers/WcAppInfoWalletChangedTransformer.kt new file mode 100644 index 0000000000..453ab9b608 --- /dev/null +++ b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/model/transformers/WcAppInfoWalletChangedTransformer.kt @@ -0,0 +1,22 @@ +package com.tangem.features.walletconnect.connections.model.transformers + +import com.tangem.domain.walletconnect.model.WcSessionProposal +import com.tangem.domain.wallets.models.UserWallet +import com.tangem.features.walletconnect.connections.entity.WcAppInfoUM +import com.tangem.utils.transformer.Transformer + +internal class WcAppInfoWalletChangedTransformer( + private val selectedUserWallet: UserWallet, + private val proposalNetwork: WcSessionProposal.ProposalNetwork, +) : Transformer { + override fun transform(prevState: WcAppInfoUM): WcAppInfoUM { + val contentState = prevState as? WcAppInfoUM.Content ?: return prevState + return contentState.copy( + walletName = selectedUserWallet.name, + networksInfo = WcNetworksInfoConverter.convert(proposalNetwork), + connectButtonConfig = prevState.connectButtonConfig.copy( + enabled = proposalNetwork.missingRequired.isEmpty(), + ), + ) + } +} \ No newline at end of file diff --git a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/model/transformers/WcNetworksInfoConverter.kt b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/model/transformers/WcNetworksInfoConverter.kt new file mode 100644 index 0000000000..8bf63c01cf --- /dev/null +++ b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/model/transformers/WcNetworksInfoConverter.kt @@ -0,0 +1,32 @@ +package com.tangem.features.walletconnect.connections.model.transformers + +import com.tangem.core.ui.extensions.iconResId +import com.tangem.domain.walletconnect.model.WcSessionProposal +import com.tangem.features.walletconnect.connections.entity.WcNetworkInfoItem +import com.tangem.features.walletconnect.connections.entity.WcNetworksInfo +import com.tangem.utils.converter.Converter +import kotlinx.collections.immutable.toImmutableList + +internal object WcNetworksInfoConverter : Converter { + override fun convert(value: WcSessionProposal.ProposalNetwork): WcNetworksInfo { + return if (value.missingRequired.isNotEmpty()) { + WcNetworksInfo.MissingRequiredNetworkInfo( + networks = value.missingRequired + .joinToString { it.name }, + ) + } else { + WcNetworksInfo.ContainsAllRequiredNetworks( + items = (value.required + value.available) + .map { + WcNetworkInfoItem( + id = it.id.value, + icon = it.iconResId, + name = it.name, + symbol = it.currencySymbol, + ) + } + .toImmutableList(), + ) + } + } +} \ No newline at end of file diff --git a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/routing/DefaultWcRoutingComponent.kt b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/routing/DefaultWcRoutingComponent.kt index cfe15122ed..6a424d83be 100644 --- a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/routing/DefaultWcRoutingComponent.kt +++ b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/routing/DefaultWcRoutingComponent.kt @@ -61,7 +61,11 @@ internal class DefaultWcRoutingComponent @AssistedInject constructor( ) is WcInnerRoute.Pair -> WcAppInfoContainerComponent( childContext, - WcAppInfoContainerComponent.Params(config.request.uri, config.request.source), + WcAppInfoContainerComponent.Params( + userWalletId = config.request.userWalletId, + wcUrl = config.request.uri, + source = config.request.source, + ), ) } } diff --git a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/ui/WcAppInfoContent.kt b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/ui/WcAppInfoContent.kt index f248d6323c..136d5ac26b 100644 --- a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/ui/WcAppInfoContent.kt +++ b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/ui/WcAppInfoContent.kt @@ -220,7 +220,12 @@ private fun WcAppInfoSecondBlock(state: WcAppInfoUM.Content, modifier: Modifier val itemsModifier = Modifier .fillMaxWidth() .padding(TangemTheme.dimens.spacing12) - WalletRowItem(modifier = itemsModifier, walletName = state.walletName) + WalletRowItem( + modifier = Modifier + .clickable(onClick = state.onWalletClick) + .then(itemsModifier), + walletName = state.walletName, + ) HorizontalDivider(thickness = 1.dp, color = TangemTheme.colors.stroke.primary) SelectNetworksBlock( networksInfo = state.networksInfo, @@ -565,6 +570,7 @@ private class WcAppInfoStateProvider : CollectionPreviewParameterProvider Unit, +) { + + private var loadedArtworks: HashMap = hashMapOf() + + @OptIn(ExperimentalCoroutinesApi::class) + val userWallets: Flow> = getWalletsUseCase().transformLatest { wallets -> + val uiModels = UserWalletItemUMConverter(onClick = onWalletSelected) + .convertList(wallets) + .toImmutableList() + + emit(uiModels) + combine( + flow = getSelectedAppCurrencyUseCase().distinctUntilChanged(), + flow2 = getBalanceHidingSettingsUseCase().distinctUntilChanged(), + flow3 = getWalletTotalBalanceUseCase(wallets.map(UserWallet::walletId)).distinctUntilChanged(), + flow4 = loadArtworks(wallets), + ) { maybeAppCurrency, balanceHidingSettings, maybeBalances, artworks -> + val models = createUiModels( + wallets = wallets, + maybeAppCurrency = maybeAppCurrency, + maybeBalances = maybeBalances, + balanceHidingSettings = balanceHidingSettings, + artworks = artworks, + ).getOrElse( + ifLoading = { return@combine }, + ifError = { + val message = resourceReference(R.string.common_unknown_error) + messageSender.send(SnackbarMessage(message)) + + return@combine + }, + ) + + emit(models) + }.collect() + } + + private fun loadArtworks(wallets: List): Flow> { + return flow { + emit(hashMapOf()) // emits right away so the transform doesn't wait for the images' loading to finish + wallets.forEach { wallet -> + val artwork = getCardImageUseCase( + cardId = wallet.cardId, + cardPublicKey = wallet.scanResponse.card.cardPublicKey, + size = ArtworkSize.SMALL, + ) + loadedArtworks[wallet.walletId] = artwork + emit(loadedArtworks) + } + } + } + + private fun createUiModels( + wallets: List, + maybeAppCurrency: Either, + maybeBalances: Lce>, + balanceHidingSettings: BalanceHidingSettings, + artworks: HashMap, + ): Lce> = lce { + val balances = withError( + transform = { Error.UnableToGetBalances }, + block = { + maybeBalances.bindOrNull().orEmpty() + .filterKeys { userWalletId -> wallets.any { it.walletId == userWalletId } } + .mapKeys { entry -> wallets.first { it.walletId == entry.key } } + }, + ) + + val appCurrency = withError( + transform = { Error.UnableToGetAppCurrency }, + block = { maybeAppCurrency.toLce().bind() }, + ) + + balances + .map { (userWallet, balance) -> + UserWalletItemUMConverter( + onClick = onWalletSelected, + appCurrency = appCurrency, + balance = balance, + isBalanceHidden = balanceHidingSettings.isBalanceHidden, + artwork = artworks[userWallet.walletId], + ) + .convert(userWallet) + } + .toImmutableList() + } + + sealed class Error { + + data object UnableToGetAppCurrency : Error() + + data object UnableToGetBalances : Error() + } +} \ No newline at end of file From 091c6066adf3757530b104bb994334b4b7c74394 Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 6 May 2025 09:47:24 +0000 Subject: [PATCH 004/165] Updated on 2026-08-14 --- gradle/tangem_dependencies.toml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/gradle/tangem_dependencies.toml b/gradle/tangem_dependencies.toml index ef2ebcb18f..5fbfb3c5e8 100644 --- a/gradle/tangem_dependencies.toml +++ b/gradle/tangem_dependencies.toml @@ -5,9 +5,9 @@ # https://github.com/tangem/tangem-sdk-android/ # https://github.com/tangem/vico -tangemBlockchainSdk = "releases-5.24.0-1056" +tangemBlockchainSdk = "develop-1052" #tangemBlockchainSdk = "0.0.1" # Keep it! - used for local builds -tangemCardSdk = "releases-5.24.0-467" +tangemCardSdk = "develop-464" #tangemCardSdk = "0.0.1" # Keep it! - used for local builds ^ tangemVico = "2.0.0-alpha.25-tangem12" #tangemVico = "0.0.1" # Keep it! - used for local builds ^ From 5e58fca4782eaa67bfd97c7d5ec3892172a311a8 Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 6 May 2025 12:59:17 +0300 Subject: [PATCH 005/165] Updated on 2026-08-14 --- gradle/tangem_dependencies.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/tangem_dependencies.toml b/gradle/tangem_dependencies.toml index 5fbfb3c5e8..9e58bcf67c 100644 --- a/gradle/tangem_dependencies.toml +++ b/gradle/tangem_dependencies.toml @@ -7,7 +7,7 @@ tangemBlockchainSdk = "develop-1052" #tangemBlockchainSdk = "0.0.1" # Keep it! - used for local builds -tangemCardSdk = "develop-464" +tangemCardSdk = "develop-468" #tangemCardSdk = "0.0.1" # Keep it! - used for local builds ^ tangemVico = "2.0.0-alpha.25-tangem12" #tangemVico = "0.0.1" # Keep it! - used for local builds ^ From c234cf59584da3a82a748b42fdc25256735760b8 Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 6 May 2025 13:21:50 +0300 Subject: [PATCH 006/165] Updated on 2026-08-14 --- .../walletconnect/connections/utils/WcUserWalletsFetcher.kt | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/utils/WcUserWalletsFetcher.kt b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/utils/WcUserWalletsFetcher.kt index c9361957b0..b27f364699 100644 --- a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/utils/WcUserWalletsFetcher.kt +++ b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/utils/WcUserWalletsFetcher.kt @@ -82,10 +82,13 @@ internal class WcUserWalletsFetcher( return flow { emit(hashMapOf()) // emits right away so the transform doesn't wait for the images' loading to finish wallets.forEach { wallet -> + val card = wallet.scanResponse.card val artwork = getCardImageUseCase( cardId = wallet.cardId, - cardPublicKey = wallet.scanResponse.card.cardPublicKey, + cardPublicKey = card.cardPublicKey, size = ArtworkSize.SMALL, + manufacturerName = card.manufacturer.name, + firmwareVersion = card.firmwareVersion.toSdkFirmwareVersion(), ) loadedArtworks[wallet.walletId] = artwork emit(loadedArtworks) From 7c35da77db9a7c30d50fd0a645591b9d9617a018 Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 6 May 2025 09:30:47 +0400 Subject: [PATCH 007/165] Updated on 2026-08-14 --- .../tap/di/domain/ManageTokensDomainModule.kt | 3 - .../tap/di/domain/MarketsDomainModule.kt | 3 - .../tap/di/domain/TokensDomainModule.kt | 16 -- .../tap/di/domain/TransactionDomainModule.kt | 8 - .../tokens/DefaultTokensFeatureToggles.kt | 3 - .../configs/feature_toggles_config.json | 4 - .../managetokens/SaveManagedTokensUseCase.kt | 18 +- .../domain/markets/SaveMarketTokensUseCase.kt | 18 +- .../tokens/AddCryptoCurrenciesUseCase.kt | 29 +-- .../tokens/FetchCardTokenListUseCase.kt | 20 +- .../tokens/FetchCurrencyStatusUseCase.kt | 28 +-- .../domain/tokens/FetchTokenListUseCase.kt | 29 +-- .../domain/tokens/TokensFeatureToggles.kt | 2 - .../UpdateDelayedNetworkStatusUseCase.kt | 34 +-- .../BaseCurrencyStatusOperations.kt | 70 ++----- .../CachedCurrenciesStatusesOperations.kt | 59 +----- ...PrimaryCurrencyStatusUpdatesUseCaseTest.kt | 197 ------------------ .../usecase/AssociateAssetUseCase.kt | 23 +- .../usecase/SendTransactionUseCase.kt | 17 +- .../send/v2/common/SendBalanceUpdater.kt | 1 - .../send/impl/presentation/model/SendModel.kt | 1 - .../state/helpers/StakingBalanceUpdater.kt | 1 - .../tangem/feature/swap/model/SwapModel.kt | 3 +- .../tokendetails/model/TokenDetailsModel.kt | 1 - .../intents/WalletWarningsClickIntents.kt | 26 +-- 25 files changed, 89 insertions(+), 525 deletions(-) delete mode 100644 domain/tokens/src/test/kotlin/com/tangem/domain/tokens/GetPrimaryCurrencyStatusUpdatesUseCaseTest.kt diff --git a/app/src/main/java/com/tangem/tap/di/domain/ManageTokensDomainModule.kt b/app/src/main/java/com/tangem/tap/di/domain/ManageTokensDomainModule.kt index b55ef9a8f7..e32ac8b237 100644 --- a/app/src/main/java/com/tangem/tap/di/domain/ManageTokensDomainModule.kt +++ b/app/src/main/java/com/tangem/tap/di/domain/ManageTokensDomainModule.kt @@ -10,7 +10,6 @@ import com.tangem.domain.staking.multi.MultiYieldBalanceFetcher import com.tangem.domain.staking.repositories.StakingRepository import com.tangem.domain.tokens.TokensFeatureToggles import com.tangem.domain.tokens.repository.CurrenciesRepository -import com.tangem.domain.tokens.repository.NetworksRepository import com.tangem.domain.tokens.repository.QuotesRepository import com.tangem.domain.walletmanager.WalletManagersFacade import com.tangem.utils.coroutines.CoroutineDispatcherProvider @@ -70,7 +69,6 @@ internal object ManageTokensDomainModule { customTokensRepository: CustomTokensRepository, walletManagersFacade: WalletManagersFacade, currenciesRepository: CurrenciesRepository, - networksRepository: NetworksRepository, derivationsRepository: DerivationsRepository, stakingRepository: StakingRepository, quotesRepository: QuotesRepository, @@ -83,7 +81,6 @@ internal object ManageTokensDomainModule { customTokensRepository = customTokensRepository, walletManagersFacade = walletManagersFacade, currenciesRepository = currenciesRepository, - networksRepository = networksRepository, derivationsRepository = derivationsRepository, stakingRepository = stakingRepository, quotesRepository = quotesRepository, diff --git a/app/src/main/java/com/tangem/tap/di/domain/MarketsDomainModule.kt b/app/src/main/java/com/tangem/tap/di/domain/MarketsDomainModule.kt index 7666729a36..9338ab2914 100644 --- a/app/src/main/java/com/tangem/tap/di/domain/MarketsDomainModule.kt +++ b/app/src/main/java/com/tangem/tap/di/domain/MarketsDomainModule.kt @@ -11,7 +11,6 @@ import com.tangem.domain.staking.multi.MultiYieldBalanceFetcher import com.tangem.domain.staking.repositories.StakingRepository import com.tangem.domain.tokens.TokensFeatureToggles import com.tangem.domain.tokens.repository.CurrenciesRepository -import com.tangem.domain.tokens.repository.NetworksRepository import com.tangem.domain.tokens.repository.QuotesRepository import com.tangem.domain.wallets.legacy.UserWalletsListManager import dagger.Module @@ -70,7 +69,6 @@ object MarketsDomainModule { derivationsRepository: DerivationsRepository, marketsTokenRepository: MarketsTokenRepository, currenciesRepository: CurrenciesRepository, - networksRepository: NetworksRepository, stakingRepository: StakingRepository, quotesRepository: QuotesRepository, multiNetworkStatusFetcher: MultiNetworkStatusFetcher, @@ -82,7 +80,6 @@ object MarketsDomainModule { derivationsRepository = derivationsRepository, marketsTokenRepository = marketsTokenRepository, currenciesRepository = currenciesRepository, - networksRepository = networksRepository, stakingRepository = stakingRepository, quotesRepository = quotesRepository, multiNetworkStatusFetcher = multiNetworkStatusFetcher, diff --git a/app/src/main/java/com/tangem/tap/di/domain/TokensDomainModule.kt b/app/src/main/java/com/tangem/tap/di/domain/TokensDomainModule.kt index ff23a2c9e2..5b56c4dc11 100644 --- a/app/src/main/java/com/tangem/tap/di/domain/TokensDomainModule.kt +++ b/app/src/main/java/com/tangem/tap/di/domain/TokensDomainModule.kt @@ -37,7 +37,6 @@ internal object TokensDomainModule { @Singleton fun provideAddCryptoCurrenciesUseCase( currenciesRepository: CurrenciesRepository, - networksRepository: NetworksRepository, stakingRepository: StakingRepository, quotesRepository: QuotesRepository, multiNetworkStatusFetcher: MultiNetworkStatusFetcher, @@ -47,7 +46,6 @@ internal object TokensDomainModule { ): AddCryptoCurrenciesUseCase { return AddCryptoCurrenciesUseCase( currenciesRepository = currenciesRepository, - networksRepository = networksRepository, stakingRepository = stakingRepository, quotesRepository = quotesRepository, multiNetworkStatusFetcher = multiNetworkStatusFetcher, @@ -62,7 +60,6 @@ internal object TokensDomainModule { fun provideFetchTokenListUseCase( currenciesRepository: CurrenciesRepository, quotesRepository: QuotesRepository, - networksRepository: NetworksRepository, stakingRepository: StakingRepository, multiNetworkStatusFetcher: MultiNetworkStatusFetcher, multiQuoteFetcher: MultiQuoteFetcher, @@ -71,7 +68,6 @@ internal object TokensDomainModule { ): FetchTokenListUseCase { return FetchTokenListUseCase( currenciesRepository = currenciesRepository, - networksRepository = networksRepository, quotesRepository = quotesRepository, stakingRepository = stakingRepository, multiNetworkStatusFetcher = multiNetworkStatusFetcher, @@ -179,7 +175,6 @@ internal object TokensDomainModule { fun provideFetchCurrencyStatusUseCase( currenciesRepository: CurrenciesRepository, quotesRepository: QuotesRepository, - networksRepository: NetworksRepository, stakingRepository: StakingRepository, singleNetworkStatusFetcher: SingleNetworkStatusFetcher, multiQuoteFetcher: MultiQuoteFetcher, @@ -188,7 +183,6 @@ internal object TokensDomainModule { ): FetchCurrencyStatusUseCase { return FetchCurrencyStatusUseCase( currenciesRepository = currenciesRepository, - networksRepository = networksRepository, quotesRepository = quotesRepository, stakingRepository = stakingRepository, singleNetworkStatusFetcher = singleNetworkStatusFetcher, @@ -203,7 +197,6 @@ internal object TokensDomainModule { fun provideFetchCardTokenListUseCase( currenciesRepository: CurrenciesRepository, quotesRepository: QuotesRepository, - networksRepository: NetworksRepository, stakingRepository: StakingRepository, multiNetworkStatusFetcher: MultiNetworkStatusFetcher, multiQuoteFetcher: MultiQuoteFetcher, @@ -212,7 +205,6 @@ internal object TokensDomainModule { ): FetchCardTokenListUseCase { return FetchCardTokenListUseCase( currenciesRepository = currenciesRepository, - networksRepository = networksRepository, quotesRepository = quotesRepository, stakingRepository = stakingRepository, multiNetworkStatusFetcher = multiNetworkStatusFetcher, @@ -328,14 +320,10 @@ internal object TokensDomainModule { @Provides @Singleton fun provideUpdateDelayedCurrencyStatusUseCase( - networksRepository: NetworksRepository, singleNetworkStatusFetcher: SingleNetworkStatusFetcher, - tokensFeatureToggles: TokensFeatureToggles, ): UpdateDelayedNetworkStatusUseCase { return UpdateDelayedNetworkStatusUseCase( - networksRepository = networksRepository, singleNetworkStatusFetcher = singleNetworkStatusFetcher, - tokensFeatureToggles = tokensFeatureToggles, ) } @@ -413,7 +401,6 @@ internal object TokensDomainModule { currenciesRepository: CurrenciesRepository, quotesRepository: QuotesRepository, quotesRepositoryV2: QuotesRepositoryV2, - networksRepository: NetworksRepository, stakingRepository: StakingRepository, singleNetworkStatusSupplier: SingleNetworkStatusSupplier, multiNetworkStatusSupplier: MultiNetworkStatusSupplier, @@ -427,7 +414,6 @@ internal object TokensDomainModule { currenciesRepository = currenciesRepository, quotesRepository = quotesRepository, quotesRepositoryV2 = quotesRepositoryV2, - networksRepository = networksRepository, stakingRepository = stakingRepository, singleNetworkStatusSupplier = singleNetworkStatusSupplier, multiNetworkStatusSupplier = multiNetworkStatusSupplier, @@ -447,7 +433,6 @@ internal object TokensDomainModule { currenciesRepository: CurrenciesRepository, quotesRepository: QuotesRepository, quotesRepositoryV2: QuotesRepositoryV2, - networksRepository: NetworksRepository, stakingRepository: StakingRepository, singleNetworkStatusSupplier: SingleNetworkStatusSupplier, multiNetworkStatusSupplier: MultiNetworkStatusSupplier, @@ -461,7 +446,6 @@ internal object TokensDomainModule { currenciesRepository = currenciesRepository, quotesRepository = quotesRepository, quotesRepositoryV2 = quotesRepositoryV2, - networksRepository = networksRepository, stakingRepository = stakingRepository, singleNetworkStatusSupplier = singleNetworkStatusSupplier, multiNetworkStatusSupplier = multiNetworkStatusSupplier, diff --git a/app/src/main/java/com/tangem/tap/di/domain/TransactionDomainModule.kt b/app/src/main/java/com/tangem/tap/di/domain/TransactionDomainModule.kt index b0b9e8f8ad..ea48365cb7 100644 --- a/app/src/main/java/com/tangem/tap/di/domain/TransactionDomainModule.kt +++ b/app/src/main/java/com/tangem/tap/di/domain/TransactionDomainModule.kt @@ -4,9 +4,7 @@ import com.tangem.domain.card.repository.CardSdkConfigRepository import com.tangem.domain.demo.DemoConfig import com.tangem.domain.networks.single.SingleNetworkStatusFetcher import com.tangem.domain.networks.single.SingleNetworkStatusSupplier -import com.tangem.domain.tokens.TokensFeatureToggles import com.tangem.domain.tokens.repository.CurrenciesRepository -import com.tangem.domain.tokens.repository.NetworksRepository import com.tangem.domain.transaction.FeeRepository import com.tangem.domain.transaction.TransactionRepository import com.tangem.domain.transaction.usecase.* @@ -43,7 +41,6 @@ internal object TransactionDomainModule { transactionRepository: TransactionRepository, walletManagersFacade: WalletManagersFacade, singleNetworkStatusFetcher: SingleNetworkStatusFetcher, - tokensFeatureToggles: TokensFeatureToggles, ): SendTransactionUseCase { return SendTransactionUseCase( demoConfig = DemoConfig(), @@ -51,7 +48,6 @@ internal object TransactionDomainModule { transactionRepository = transactionRepository, walletManagersFacade = walletManagersFacade, singleNetworkStatusFetcher = singleNetworkStatusFetcher, - tokensFeatureToggles = tokensFeatureToggles, ) } @@ -61,17 +57,13 @@ internal object TransactionDomainModule { cardSdkConfigRepository: CardSdkConfigRepository, walletManagersFacade: WalletManagersFacade, currenciesRepository: CurrenciesRepository, - networksRepository: NetworksRepository, singleNetworkStatusSupplier: SingleNetworkStatusSupplier, - tokensFeatureToggles: TokensFeatureToggles, ): AssociateAssetUseCase { return AssociateAssetUseCase( cardSdkConfigRepository = cardSdkConfigRepository, walletManagersFacade = walletManagersFacade, currenciesRepository = currenciesRepository, - networksRepository = networksRepository, singleNetworkStatusSupplier = singleNetworkStatusSupplier, - tokensFeatureToggles = tokensFeatureToggles, ) } diff --git a/app/src/main/java/com/tangem/tap/domain/tokens/DefaultTokensFeatureToggles.kt b/app/src/main/java/com/tangem/tap/domain/tokens/DefaultTokensFeatureToggles.kt index adc8e80741..bfb76fbf06 100644 --- a/app/src/main/java/com/tangem/tap/domain/tokens/DefaultTokensFeatureToggles.kt +++ b/app/src/main/java/com/tangem/tap/domain/tokens/DefaultTokensFeatureToggles.kt @@ -7,9 +7,6 @@ internal class DefaultTokensFeatureToggles( private val featureTogglesManager: FeatureTogglesManager, ) : TokensFeatureToggles { - override val isNetworksLoadingRefactoringEnabled: Boolean - get() = featureTogglesManager.isFeatureEnabled(name = "NETWORKS_LOADING_REFACTORING_ENABLED") - override val isQuotesLoadingRefactoringEnabled: Boolean get() = featureTogglesManager.isFeatureEnabled(name = "QUOTES_LOADING_REFACTORING_ENABLED") 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 4a9a86f746..6a3b525e20 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 @@ -51,10 +51,6 @@ "name": "WALLET_CONNECT_REDESIGN_ENABLED", "version": "undefined" }, - { - "name": "NETWORKS_LOADING_REFACTORING_ENABLED", - "version": "5.23.0" - }, { "name": "QUOTES_LOADING_REFACTORING_ENABLED", "version": "5.24.0" diff --git a/domain/manage-tokens/src/main/kotlin/com/tangem/domain/managetokens/SaveManagedTokensUseCase.kt b/domain/manage-tokens/src/main/kotlin/com/tangem/domain/managetokens/SaveManagedTokensUseCase.kt index 0c83963864..29cddff1c0 100644 --- a/domain/manage-tokens/src/main/kotlin/com/tangem/domain/managetokens/SaveManagedTokensUseCase.kt +++ b/domain/manage-tokens/src/main/kotlin/com/tangem/domain/managetokens/SaveManagedTokensUseCase.kt @@ -14,7 +14,6 @@ import com.tangem.domain.tokens.TokensFeatureToggles import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.domain.tokens.model.Network import com.tangem.domain.tokens.repository.CurrenciesRepository -import com.tangem.domain.tokens.repository.NetworksRepository import com.tangem.domain.tokens.repository.QuotesRepository import com.tangem.domain.walletmanager.WalletManagersFacade import com.tangem.domain.wallets.models.UserWalletId @@ -24,7 +23,6 @@ class SaveManagedTokensUseCase( private val customTokensRepository: CustomTokensRepository, private val walletManagersFacade: WalletManagersFacade, private val currenciesRepository: CurrenciesRepository, - private val networksRepository: NetworksRepository, private val derivationsRepository: DerivationsRepository, private val stakingRepository: StakingRepository, private val quotesRepository: QuotesRepository, @@ -102,20 +100,12 @@ class SaveManagedTokensUseCase( val networkToUpdate = currenciesToAdd.map { it.network } .subtract(existingCurrencies.map { it.network }.toSet()) - if (tokensFeatureToggles.isNetworksLoadingRefactoringEnabled) { - multiNetworkStatusFetcher( - MultiNetworkStatusFetcher.Params( - userWalletId = userWalletId, - networks = networksToUpdate + networkToUpdate, - ), - ) - } else { - networksRepository.getNetworkStatusesSync( + multiNetworkStatusFetcher( + MultiNetworkStatusFetcher.Params( userWalletId = userWalletId, networks = networksToUpdate + networkToUpdate, - refresh = true, - ) - } + ), + ) } private suspend fun refreshUpdatedYieldBalances( diff --git a/domain/markets/src/main/java/com/tangem/domain/markets/SaveMarketTokensUseCase.kt b/domain/markets/src/main/java/com/tangem/domain/markets/SaveMarketTokensUseCase.kt index 1d0215f044..cbc09a8700 100644 --- a/domain/markets/src/main/java/com/tangem/domain/markets/SaveMarketTokensUseCase.kt +++ b/domain/markets/src/main/java/com/tangem/domain/markets/SaveMarketTokensUseCase.kt @@ -12,7 +12,6 @@ import com.tangem.domain.tokens.TokensFeatureToggles import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.domain.tokens.model.Network import com.tangem.domain.tokens.repository.CurrenciesRepository -import com.tangem.domain.tokens.repository.NetworksRepository import com.tangem.domain.tokens.repository.QuotesRepository import com.tangem.domain.wallets.models.UserWalletId @@ -30,7 +29,6 @@ class SaveMarketTokensUseCase( private val derivationsRepository: DerivationsRepository, private val marketsTokenRepository: MarketsTokenRepository, private val currenciesRepository: CurrenciesRepository, - private val networksRepository: NetworksRepository, private val stakingRepository: StakingRepository, private val quotesRepository: QuotesRepository, private val multiNetworkStatusFetcher: MultiNetworkStatusFetcher, @@ -83,20 +81,12 @@ class SaveMarketTokensUseCase( } private suspend fun refreshUpdatedNetworks(userWalletId: UserWalletId, addedCurrencies: List) { - if (tokensFeatureToggles.isNetworksLoadingRefactoringEnabled) { - multiNetworkStatusFetcher( - MultiNetworkStatusFetcher.Params( - userWalletId = userWalletId, - networks = addedCurrencies.map(CryptoCurrency::network).toSet(), - ), - ) - } else { - networksRepository.getNetworkStatusesSync( + multiNetworkStatusFetcher( + MultiNetworkStatusFetcher.Params( userWalletId = userWalletId, networks = addedCurrencies.map(CryptoCurrency::network).toSet(), - refresh = true, - ) - } + ), + ) } private suspend fun refreshUpdatedYieldBalances( diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/AddCryptoCurrenciesUseCase.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/AddCryptoCurrenciesUseCase.kt index ca0fecd294..a8bb9a147e 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/AddCryptoCurrenciesUseCase.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/AddCryptoCurrenciesUseCase.kt @@ -12,7 +12,6 @@ import com.tangem.domain.staking.single.SingleYieldBalanceFetcher import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.domain.tokens.model.Network import com.tangem.domain.tokens.repository.CurrenciesRepository -import com.tangem.domain.tokens.repository.NetworksRepository import com.tangem.domain.tokens.repository.QuotesRepository import com.tangem.domain.wallets.models.UserWalletId import kotlinx.coroutines.async @@ -29,7 +28,6 @@ import kotlinx.coroutines.coroutineScope @Suppress("LongParameterList") class AddCryptoCurrenciesUseCase( private val currenciesRepository: CurrenciesRepository, - private val networksRepository: NetworksRepository, private val stakingRepository: StakingRepository, private val quotesRepository: QuotesRepository, private val multiNetworkStatusFetcher: MultiNetworkStatusFetcher, @@ -139,26 +137,13 @@ class AddCryptoCurrenciesUseCase( } ?.network - if (tokensFeatureToggles.isNetworksLoadingRefactoringEnabled) { - multiNetworkStatusFetcher( - MultiNetworkStatusFetcher.Params( - userWalletId = userWalletId, - networks = setOfNotNull(networksToUpdate, networkToUpdate), - ), - ) - } else { - catch( - { - networksRepository.getNetworkStatusesSync( - userWalletId = userWalletId, - networks = setOfNotNull(networksToUpdate, networkToUpdate), - refresh = true, - ) - }, - ) { - raise(it) - } - } + multiNetworkStatusFetcher( + MultiNetworkStatusFetcher.Params( + userWalletId = userWalletId, + networks = setOfNotNull(networksToUpdate, networkToUpdate), + ), + ) + .bind() } private suspend fun refreshUpdatedYieldBalances(userWalletId: UserWalletId, addedCurrency: CryptoCurrency) { diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/FetchCardTokenListUseCase.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/FetchCardTokenListUseCase.kt index 7d56e24a4b..1019ef739e 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/FetchCardTokenListUseCase.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/FetchCardTokenListUseCase.kt @@ -13,7 +13,6 @@ import com.tangem.domain.tokens.error.TokenListError import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.domain.tokens.model.Network import com.tangem.domain.tokens.repository.CurrenciesRepository -import com.tangem.domain.tokens.repository.NetworksRepository import com.tangem.domain.tokens.repository.QuotesRepository import com.tangem.domain.wallets.models.UserWalletId import kotlinx.coroutines.async @@ -23,7 +22,6 @@ import kotlinx.coroutines.coroutineScope @Suppress("LongParameterList") class FetchCardTokenListUseCase( private val currenciesRepository: CurrenciesRepository, - private val networksRepository: NetworksRepository, private val quotesRepository: QuotesRepository, private val stakingRepository: StakingRepository, private val multiNetworkStatusFetcher: MultiNetworkStatusFetcher, @@ -41,7 +39,6 @@ class FetchCardTokenListUseCase( fetchNetworksStatuses( userWalletId = userWalletId, networks = currencies.mapTo(destination = hashSetOf(), transform = CryptoCurrency::network), - refresh = refresh, ) } val fetchQuotes = async { @@ -80,19 +77,12 @@ class FetchCardTokenListUseCase( private suspend fun Raise.fetchNetworksStatuses( userWalletId: UserWalletId, networks: Set, - refresh: Boolean, ) { - if (tokensFeatureToggles.isNetworksLoadingRefactoringEnabled) { - multiNetworkStatusFetcher( - MultiNetworkStatusFetcher.Params(userWalletId = userWalletId, networks = networks), - ) - .mapLeft { TokenListError.DataError(it) } - } else { - catch( - block = { networksRepository.getNetworkStatusesSync(userWalletId, networks, refresh) }, - catch = { raise(TokenListError.DataError(it)) }, - ) - } + multiNetworkStatusFetcher( + MultiNetworkStatusFetcher.Params(userWalletId = userWalletId, networks = networks), + ) + .mapLeft(TokenListError::DataError) + .bind() } private suspend fun fetchQuotes(currenciesIds: Set, refresh: Boolean) { diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/FetchCurrencyStatusUseCase.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/FetchCurrencyStatusUseCase.kt index e9775c5d32..dee7078730 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/FetchCurrencyStatusUseCase.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/FetchCurrencyStatusUseCase.kt @@ -13,7 +13,6 @@ import com.tangem.domain.tokens.error.CurrencyStatusError import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.domain.tokens.model.Network import com.tangem.domain.tokens.repository.CurrenciesRepository -import com.tangem.domain.tokens.repository.NetworksRepository import com.tangem.domain.tokens.repository.QuotesRepository import com.tangem.domain.wallets.models.UserWalletId import kotlinx.coroutines.async @@ -26,14 +25,12 @@ import kotlinx.coroutines.coroutineScope * by providing a specific currency ID or fetching the status of the primary currency. * * @param currenciesRepository The repository for retrieving currency-related data. - * @param networksRepository The repository for retrieving network-related data. * @param quotesRepository The repository for retrieving cryptocurrency quotes. */ // TODO: Add tests @Suppress("LongParameterList") class FetchCurrencyStatusUseCase( private val currenciesRepository: CurrenciesRepository, - private val networksRepository: NetworksRepository, private val quotesRepository: QuotesRepository, private val stakingRepository: StakingRepository, private val singleNetworkStatusFetcher: SingleNetworkStatusFetcher, @@ -86,7 +83,7 @@ class FetchCurrencyStatusUseCase( refresh: Boolean, ) = coroutineScope { val fetchStatus = async { - fetchNetworkStatus(userWalletId, currency.network, refresh) + fetchNetworkStatus(userWalletId, currency.network) } val fetchQuote = async { fetchQuote(currency.id, refresh) @@ -120,23 +117,12 @@ class FetchCurrencyStatusUseCase( } } - private suspend fun Raise.fetchNetworkStatus( - userWalletId: UserWalletId, - network: Network, - refresh: Boolean, - ) { - if (tokensFeatureToggles.isNetworksLoadingRefactoringEnabled) { - singleNetworkStatusFetcher( - params = SingleNetworkStatusFetcher.Params(userWalletId = userWalletId, network = network), - ) - .mapLeft { CurrencyStatusError.DataError(it) } - } else { - catch( - block = { networksRepository.getNetworkStatusesSync(userWalletId, setOf(network), refresh) }, - ) { - raise(CurrencyStatusError.DataError(it)) - } - } + private suspend fun Raise.fetchNetworkStatus(userWalletId: UserWalletId, network: Network) { + singleNetworkStatusFetcher( + params = SingleNetworkStatusFetcher.Params(userWalletId = userWalletId, network = network), + ) + .mapLeft { CurrencyStatusError.DataError(it) } + .bind() } private suspend fun Raise.fetchQuote(currencyId: CryptoCurrency.ID, refresh: Boolean) { diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/FetchTokenListUseCase.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/FetchTokenListUseCase.kt index e0300eb83a..3061b1152d 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/FetchTokenListUseCase.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/FetchTokenListUseCase.kt @@ -15,7 +15,6 @@ import com.tangem.domain.tokens.error.TokenListError import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.domain.tokens.model.Network import com.tangem.domain.tokens.repository.CurrenciesRepository -import com.tangem.domain.tokens.repository.NetworksRepository import com.tangem.domain.tokens.repository.QuotesRepository import com.tangem.domain.wallets.models.UserWalletId import kotlinx.coroutines.async @@ -27,7 +26,6 @@ import kotlinx.coroutines.coroutineScope * network statuses, and quotes for tokens associated with a user's wallet. * * @param currenciesRepository The repository for retrieving currency-related data. - * @param networksRepository The repository for retrieving network-related data. * @param quotesRepository The repository for retrieving cryptocurrency quotes. * @param stakingRepository The repository for retrieving staking-related data. */ @@ -35,7 +33,6 @@ import kotlinx.coroutines.coroutineScope @Suppress("LongParameterList") class FetchTokenListUseCase( private val currenciesRepository: CurrenciesRepository, - private val networksRepository: NetworksRepository, private val quotesRepository: QuotesRepository, private val stakingRepository: StakingRepository, private val multiNetworkStatusFetcher: MultiNetworkStatusFetcher, @@ -71,7 +68,6 @@ class FetchTokenListUseCase( fetchNetworksStatuses( userWalletId = userWalletId, networks = currencies.mapTo(hashSetOf()) { it.network }, - refresh = mode.refreshNetworksStatuses, ) } val fetchQuotes = async { @@ -111,23 +107,12 @@ class FetchTokenListUseCase( private suspend fun Raise.fetchNetworksStatuses( userWalletId: UserWalletId, networks: Set, - refresh: Boolean, ) { - if (tokensFeatureToggles.isNetworksLoadingRefactoringEnabled) { - if (refresh) { - multiNetworkStatusFetcher( - params = MultiNetworkStatusFetcher.Params(userWalletId, networks), - ) - .mapLeft { TokenListError.DataError(it) } - .bind() - } - } else { - catch( - block = { networksRepository.getNetworkStatusesSync(userWalletId, networks, refresh) }, - ) { - raise(TokenListError.DataError(it)) - } - } + multiNetworkStatusFetcher( + params = MultiNetworkStatusFetcher.Params(userWalletId = userWalletId, networks = networks), + ) + .mapLeft(TokenListError::DataError) + .bind() } private suspend fun fetchQuotes(currenciesIds: Set, refresh: Boolean) { @@ -175,25 +160,21 @@ class FetchTokenListUseCase( */ enum class RefreshMode( internal val refreshCurrencies: Boolean, - internal val refreshNetworksStatuses: Boolean, internal val refreshQuotes: Boolean, internal val refreshYieldBalances: Boolean, ) { NONE( refreshCurrencies = false, - refreshNetworksStatuses = false, refreshQuotes = false, refreshYieldBalances = false, ), FULL( refreshCurrencies = true, - refreshNetworksStatuses = true, refreshQuotes = true, refreshYieldBalances = true, ), SKIP_CURRENCIES( refreshCurrencies = false, - refreshNetworksStatuses = true, refreshQuotes = true, refreshYieldBalances = true, ), diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/TokensFeatureToggles.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/TokensFeatureToggles.kt index be40961b10..b969947239 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/TokensFeatureToggles.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/TokensFeatureToggles.kt @@ -7,8 +7,6 @@ package com.tangem.domain.tokens */ interface TokensFeatureToggles { - val isNetworksLoadingRefactoringEnabled: Boolean - val isQuotesLoadingRefactoringEnabled: Boolean val isStakingLoadingRefactoringEnabled: Boolean diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/UpdateDelayedNetworkStatusUseCase.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/UpdateDelayedNetworkStatusUseCase.kt index 5be8ae95ef..632b102018 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/UpdateDelayedNetworkStatusUseCase.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/UpdateDelayedNetworkStatusUseCase.kt @@ -2,12 +2,10 @@ package com.tangem.domain.tokens import arrow.core.Either import arrow.core.raise.Raise -import arrow.core.raise.catch import arrow.core.raise.either import com.tangem.domain.networks.single.SingleNetworkStatusFetcher import com.tangem.domain.tokens.error.CurrencyStatusError import com.tangem.domain.tokens.model.Network -import com.tangem.domain.tokens.repository.NetworksRepository import com.tangem.domain.wallets.models.UserWalletId import kotlinx.coroutines.delay @@ -15,14 +13,9 @@ import kotlinx.coroutines.delay * Use case responsible for fetching currency status information, including network status * and quotes for a given cryptocurrency. It provides methods to fetch currency status either * by providing a specific currency ID or fetching the status of the primary currency. - * - * @param networksRepository The repository for retrieving network-related data. */ - class UpdateDelayedNetworkStatusUseCase( - private val networksRepository: NetworksRepository, private val singleNetworkStatusFetcher: SingleNetworkStatusFetcher, - private val tokensFeatureToggles: TokensFeatureToggles, ) { /** @@ -30,37 +23,24 @@ class UpdateDelayedNetworkStatusUseCase( * * @param userWalletId The ID of the user's wallet. * @param network Network of the cryptocurrency. - * @param refresh Indicates whether to force a refresh of the status data. * @return An [Either] representing success (Right) or an error (Left) in fetching the status. */ suspend operator fun invoke( userWalletId: UserWalletId, network: Network, delayMillis: Long = 0L, - refresh: Boolean = false, ): Either { delay(delayMillis) return either { - fetchNetworkStatus(userWalletId, network, refresh) + fetchNetworkStatus(userWalletId, network) } } - private suspend fun Raise.fetchNetworkStatus( - userWalletId: UserWalletId, - network: Network, - refresh: Boolean, - ) { - if (tokensFeatureToggles.isNetworksLoadingRefactoringEnabled) { - singleNetworkStatusFetcher( - params = SingleNetworkStatusFetcher.Params(userWalletId = userWalletId, network = network), - ) - .mapLeft { CurrencyStatusError.DataError(it) } - } else { - catch( - block = { networksRepository.getNetworkStatusesSync(userWalletId, setOf(network), refresh) }, - ) { - raise(CurrencyStatusError.DataError(it)) - } - } + private suspend fun Raise.fetchNetworkStatus(userWalletId: UserWalletId, network: Network) { + singleNetworkStatusFetcher( + params = SingleNetworkStatusFetcher.Params(userWalletId = userWalletId, network = network), + ) + .mapLeft(CurrencyStatusError::DataError) + .bind() } } \ No newline at end of file diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/BaseCurrencyStatusOperations.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/BaseCurrencyStatusOperations.kt index c2d41790de..b1d6b2f500 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/BaseCurrencyStatusOperations.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/BaseCurrencyStatusOperations.kt @@ -22,7 +22,6 @@ import com.tangem.domain.tokens.TokensFeatureToggles import com.tangem.domain.tokens.model.* import com.tangem.domain.tokens.operations.CurrenciesStatusesOperations.Error import com.tangem.domain.tokens.repository.CurrenciesRepository -import com.tangem.domain.tokens.repository.NetworksRepository import com.tangem.domain.tokens.repository.QuotesRepository import com.tangem.domain.tokens.utils.CurrencyStatusProxyCreator import com.tangem.domain.wallets.models.UserWalletId @@ -33,7 +32,6 @@ import kotlinx.coroutines.flow.* * * @property currenciesRepository repository for currencies * @property quotesRepository repository for quotes - * @property networksRepository repository for networks * @property stakingRepository repository for staking * [REDACTED_AUTHOR] @@ -43,7 +41,6 @@ abstract class BaseCurrencyStatusOperations( private val currenciesRepository: CurrenciesRepository, private val quotesRepository: QuotesRepository, private val quotesRepositoryV2: QuotesRepositoryV2, - private val networksRepository: NetworksRepository, private val stakingRepository: StakingRepository, private val multiNetworkStatusSupplier: MultiNetworkStatusSupplier, private val singleNetworkStatusSupplier: SingleNetworkStatusSupplier, @@ -203,24 +200,14 @@ abstract class BaseCurrencyStatusOperations( ?.right() ?: Error.EmptyQuotes.left() - val networkStatuses = if (tokensFeatureToggles.isNetworksLoadingRefactoringEnabled) { - singleNetworkStatusSupplier( - params = SingleNetworkStatusProducer.Params( - userWalletId = userWalletId, - network = currency.network, - ), - ) - .firstOrNull() - .right() - } else { - networksRepository.getNetworkStatusesSync( + val networkStatuses = singleNetworkStatusSupplier( + params = SingleNetworkStatusProducer.Params( userWalletId = userWalletId, - networks = setOf(currency.network), - refresh = false, - ) - .firstOrNull { it.network == currency.network } - .right() - } + network = currency.network, + ), + ) + .firstOrNull() + .right() val yieldBalances = getYieldBalanceSync(userWalletId, currency) @@ -270,7 +257,7 @@ abstract class BaseCurrencyStatusOperations( val nonEmptyCurrencies = currenciesRepository.getMultiCurrencyWalletCurrenciesSync(userWalletId).toNonEmptyListOrNull() ?: return emptyList().right() - val (networks, currenciesIds) = getIds(nonEmptyCurrencies) + val (_, currenciesIds) = getIds(nonEmptyCurrencies) val rawIds = currenciesIds.mapNotNull { it.rawCurrencyId }.toSet() val quotes = if (tokensFeatureToggles.isQuotesLoadingRefactoringEnabled) { @@ -279,16 +266,13 @@ abstract class BaseCurrencyStatusOperations( quotesRepository.getQuotesSync(rawIds, false).right() } - val networkStatuses = if (tokensFeatureToggles.isNetworksLoadingRefactoringEnabled) { - multiNetworkStatusSupplier( - params = MultiNetworkStatusProducer.Params(userWalletId = userWalletId), - ) - .firstOrNull() - .orEmpty() - .right() - } else { - networksRepository.getNetworkStatusesSync(userWalletId, networks, false).right() - } + val networkStatuses = multiNetworkStatusSupplier( + params = MultiNetworkStatusProducer.Params(userWalletId = userWalletId), + ) + .firstOrNull() + .orEmpty() + .right() + val yieldBalances = getYieldBalancesSync(userWalletId, nonEmptyCurrencies) return currencyStatusProxyCreator.createCurrenciesStatuses( @@ -326,25 +310,11 @@ abstract class BaseCurrencyStatusOperations( ) } - val networkStatus = if (tokensFeatureToggles.isNetworksLoadingRefactoringEnabled) { - singleNetworkStatusSupplier( - params = SingleNetworkStatusProducer.Params( - userWalletId = userWalletId, - network = currency.network, - ), - ) - .firstOrNull() - .right() - } else { - catch( - block = { - networksRepository.getNetworkStatusesSync(userWalletId, setOf(currency.network)) - .firstOrNull { it.network == currency.network } - .right() - }, - catch = { Error.DataError(it).left() }, - ) - } + val networkStatus = singleNetworkStatusSupplier( + params = SingleNetworkStatusProducer.Params(userWalletId = userWalletId, network = currency.network), + ) + .firstOrNull() + .right() val yieldBalances = getYieldBalanceSync(userWalletId, currency) diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/CachedCurrenciesStatusesOperations.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/CachedCurrenciesStatusesOperations.kt index 8814392d0a..bfda6741cd 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/CachedCurrenciesStatusesOperations.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/CachedCurrenciesStatusesOperations.kt @@ -30,7 +30,6 @@ import com.tangem.domain.tokens.error.TokenListError import com.tangem.domain.tokens.model.* import com.tangem.domain.tokens.operations.CurrenciesStatusesOperations.Error import com.tangem.domain.tokens.repository.CurrenciesRepository -import com.tangem.domain.tokens.repository.NetworksRepository import com.tangem.domain.tokens.repository.QuotesRepository import com.tangem.domain.tokens.utils.extractAddress import com.tangem.domain.wallets.models.UserWalletId @@ -43,7 +42,6 @@ class CachedCurrenciesStatusesOperations( private val currenciesRepository: CurrenciesRepository, private val quotesRepository: QuotesRepository, quotesRepositoryV2: QuotesRepositoryV2, - private val networksRepository: NetworksRepository, private val stakingRepository: StakingRepository, private val singleNetworkStatusSupplier: SingleNetworkStatusSupplier, multiNetworkStatusSupplier: MultiNetworkStatusSupplier, @@ -58,7 +56,6 @@ class CachedCurrenciesStatusesOperations( currenciesRepository = currenciesRepository, quotesRepository = quotesRepository, quotesRepositoryV2 = quotesRepositoryV2, - networksRepository = networksRepository, stakingRepository = stakingRepository, multiNetworkStatusSupplier = multiNetworkStatusSupplier, singleNetworkStatusSupplier = singleNetworkStatusSupplier, @@ -156,7 +153,7 @@ class CachedCurrenciesStatusesOperations( combine( flow = getQuotes(currenciesIds), - flow2 = getNetworksStatuses(userWalletId, networks), + flow2 = getNetworkStatusesUpdates(userWalletId, networks), flow3 = getYieldBalances(userWalletId, currencies), flow4 = fetchingState.map { val state = it[userWalletId] ?: return@map false @@ -184,13 +181,9 @@ class CachedCurrenciesStatusesOperations( coroutineScope { awaitAll( async { - if (tokensFeatureToggles.isNetworksLoadingRefactoringEnabled) { - multiNetworkStatusFetcher( - params = MultiNetworkStatusFetcher.Params(userWalletId, networks), - ) - } else { - networksRepository.fetchNetworkStatuses(userWalletId, networks) - } + multiNetworkStatusFetcher( + params = MultiNetworkStatusFetcher.Params(userWalletId = userWalletId, networks = networks), + ) }, async { val rawCurrenciesIds = currenciesIds.mapNotNullTo(mutableSetOf()) { it.rawCurrencyId } @@ -327,44 +320,12 @@ class CachedCurrenciesStatusesOperations( userWalletId: UserWalletId, network: Network, ): EitherFlow> { - return if (tokensFeatureToggles.isNetworksLoadingRefactoringEnabled) { - singleNetworkStatusSupplier( - params = SingleNetworkStatusProducer.Params(userWalletId = userWalletId, network = network), - ) - .map>> { setOf(it).right() } - .distinctUntilChanged() - .onEmpty { emit(Error.EmptyNetworksStatuses.left()) } - } else { - networksRepository.getNetworkStatusesUpdates(userWalletId, setOf(network)) - .map, Either>> { it.right() } - .retryWhen { cause, _ -> - emit(Error.DataError(cause).left()) - // adding delay before retry to avoid spam when flow restarted - delay(RETRY_DELAY) - true - } - .distinctUntilChanged() - .onEmpty { emit(Error.EmptyNetworksStatuses.left()) } - } - } - - private fun getNetworksStatuses( - userWalletId: UserWalletId, - networks: NonEmptySet, - ): EitherFlow> { - return if (tokensFeatureToggles.isNetworksLoadingRefactoringEnabled) { - getNetworkStatusesUpdates(userWalletId, networks) - } else { - networksRepository.getNetworkStatusesUpdates(userWalletId, networks) - .map, Either>> { it.right() } - .retryWhen { cause, _ -> - emit(TokenListError.DataError(cause).left()) - // adding delay before retry to avoid spam when flow restarted - delay(RETRY_DELAY) - true - } - .distinctUntilChanged() - } + return singleNetworkStatusSupplier( + params = SingleNetworkStatusProducer.Params(userWalletId = userWalletId, network = network), + ) + .map>> { setOf(it).right() } + .distinctUntilChanged() + .onEmpty { emit(Error.EmptyNetworksStatuses.left()) } } private fun getYieldBalances( diff --git a/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/GetPrimaryCurrencyStatusUpdatesUseCaseTest.kt b/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/GetPrimaryCurrencyStatusUpdatesUseCaseTest.kt deleted file mode 100644 index be180baa2f..0000000000 --- a/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/GetPrimaryCurrencyStatusUpdatesUseCaseTest.kt +++ /dev/null @@ -1,197 +0,0 @@ -package com.tangem.domain.tokens - -import arrow.core.Either -import arrow.core.left -import arrow.core.right -import com.tangem.domain.core.error.DataError -import com.tangem.domain.tokens.error.CurrencyStatusError -import com.tangem.domain.tokens.mock.MockNetworks -import com.tangem.domain.tokens.mock.MockQuotes -import com.tangem.domain.tokens.mock.MockTokens -import com.tangem.domain.tokens.mock.MockTokensStates -import com.tangem.domain.tokens.model.* -import com.tangem.domain.tokens.operations.CachedCurrenciesStatusesOperations -import com.tangem.domain.tokens.repository.MockCurrenciesRepository -import com.tangem.domain.tokens.repository.MockNetworksRepository -import com.tangem.domain.tokens.repository.MockQuotesRepository -import com.tangem.domain.tokens.repository.MockStakingRepository -import com.tangem.domain.wallets.models.UserWalletId -import com.tangem.utils.coroutines.TestingCoroutineDispatcherProvider -import io.mockk.mockk -import junit.framework.TestCase.assertEquals -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.first -import kotlinx.coroutines.flow.flowOf -import kotlinx.coroutines.test.runTest -import org.junit.Test -import java.math.BigDecimal - -internal class GetPrimaryCurrencyStatusUpdatesUseCaseTest { - - private val dispatchers = TestingCoroutineDispatcherProvider() - private val userWalletId = UserWalletId(value = null) - - @Test - fun `when all data received then token should be received`() = runTest { - // Given - val expectedResult = MockTokensStates.loadedTokensStates.first().right() - - val useCase = getUseCase() - - // When - val result = useCase(userWalletId).first() - - // Then - assertEquals(expectedResult, result) - } - - @Test - fun `when token getting failed then error should be received`() = runTest { - // Given - val expectedResult = CurrencyStatusError.DataError(DataError.NetworkError.NoInternetConnection).left() - - val useCase = getUseCase(token = DataError.NetworkError.NoInternetConnection.left()) - - // When - val result = useCase(userWalletId).first() - - // Then - assertEquals(expectedResult, result) - } - - @Test - fun `when quotes getting failed then currency with no quote status should be received`() = runTest { - // Given - val expectedResult = MockTokensStates.noQuotesTokensStatuses.first().right() - - val useCase = getUseCase(quotes = flowOf(DataError.NetworkError.NoInternetConnection.left())) - - // When - val result = useCase(userWalletId).first() - - // Then - assertEquals(expectedResult, result) - } - - @Test - fun `when networks statuses getting failed then error should be received`() = runTest { - // Given - val expectedResult = CurrencyStatusError.DataError(DataError.NetworkError.NoInternetConnection).left() - - val useCase = getUseCase(statuses = flowOf(DataError.NetworkError.NoInternetConnection.left())) - - // When - val result = useCase(userWalletId).first() - - // Then - assertEquals(expectedResult, result) - } - - @Test - fun `when networks statuses flow is empty then error should be received`() = runTest { - val expectedResult = CurrencyStatusError.UnableToCreateCurrency.left() - - val useCase = getUseCase(statuses = flowOf()) - - // When - val result = useCase(userWalletId).first() - - // Then - assertEquals(expectedResult, result) - } - - @Test - fun `when quotes flow is empty then no quote status should be received`() = runTest { - val expectedResult = MockTokensStates.noQuotesTokensStatuses.first().right() - - val useCase = getUseCase(quotes = flowOf(emptySet().right())) - - // When - val result = useCase(userWalletId).first() - - // Then - assertEquals(expectedResult, result) - } - - @Test - fun `when quotes are empty and statuses are verified then token without quote should be received`() = runTest { - val expectedResult = with(MockTokensStates.tokenState1) { - copy( - value = CryptoCurrencyStatus.NoQuote( - amount = BigDecimal.TEN, - hasCurrentNetworkTransactions = false, - pendingTransactions = emptySet(), - networkAddress = NetworkAddress.Single( - defaultAddress = NetworkAddress.Address(value = "mock", NetworkAddress.Address.Type.Primary), - ), - yieldBalance = null, - sources = CryptoCurrencyStatus.Sources(), - ), - ) - } - .right() - - val useCase = getUseCase( - statuses = flowOf(MockNetworks.verifiedNetworksStatuses.right()), - quotes = flowOf(emptySet().right()), - ) - - // When - val result = useCase(userWalletId).first() - - // Then - assertEquals(expectedResult, result) - } - - @Test - fun `when quotes are loaded and statuses are empty then error be received`() = runTest { - val expectedResult = CurrencyStatusError.UnableToCreateCurrency.left() - - val useCase = getUseCase( - statuses = flowOf(emptySet().right()), - quotes = flowOf(MockQuotes.quotes.right()), - ) - - // When - val result = useCase(userWalletId).first() - - // Then - assertEquals(expectedResult, result) - } - - private fun getUseCase( - token: Either = MockTokens.token1.right(), - removeCurrencyResult: Either = Unit.right(), - quotes: Flow>> = flowOf(MockQuotes.quotes.right()), - statuses: Flow>> = flowOf(MockNetworks.verifiedNetworksStatuses.right()), - ) = GetPrimaryCurrencyStatusUpdatesUseCase( - currencyStatusOperations = CachedCurrenciesStatusesOperations( - currenciesRepository = MockCurrenciesRepository( - sortTokensResult = Unit.right(), - removeCurrencyResult = removeCurrencyResult, - token = token, - tokens = flowOf(), - isGrouped = flowOf(), - isSortedByBalance = flowOf(), - ), - quotesRepository = MockQuotesRepository(quotes), - networksRepository = MockNetworksRepository(statuses), - stakingRepository = MockStakingRepository(), - - tokensFeatureToggles = object : TokensFeatureToggles { - override val isNetworksLoadingRefactoringEnabled: Boolean = false - override val isQuotesLoadingRefactoringEnabled: Boolean = false - override val isStakingLoadingRefactoringEnabled: Boolean = false - }, - singleNetworkStatusSupplier = mockk(), - multiNetworkStatusFetcher = mockk(), - multiNetworkStatusSupplier = mockk(), - multiQuoteFetcher = mockk(), - singleQuoteSupplier = mockk(), - quotesRepositoryV2 = mockk(), - singleYieldBalanceSupplier = mockk(), - multiYieldBalanceFetcher = mockk(), - ), - dispatchers = dispatchers, - ) -} \ 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 3df20fa08d..f44929cdd3 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 @@ -7,12 +7,10 @@ import com.tangem.blockchain.extensions.SimpleResult import com.tangem.domain.card.repository.CardSdkConfigRepository import com.tangem.domain.networks.single.SingleNetworkStatusProducer import com.tangem.domain.networks.single.SingleNetworkStatusSupplier -import com.tangem.domain.tokens.TokensFeatureToggles import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.domain.tokens.model.CryptoCurrencyAmountStatus import com.tangem.domain.tokens.model.NetworkStatus import com.tangem.domain.tokens.repository.CurrenciesRepository -import com.tangem.domain.tokens.repository.NetworksRepository import com.tangem.domain.transaction.error.AssociateAssetError import com.tangem.domain.walletmanager.WalletManagersFacade import com.tangem.domain.wallets.models.UserWalletId @@ -23,9 +21,7 @@ class AssociateAssetUseCase( private val cardSdkConfigRepository: CardSdkConfigRepository, private val walletManagersFacade: WalletManagersFacade, private val currenciesRepository: CurrenciesRepository, - private val networksRepository: NetworksRepository, private val singleNetworkStatusSupplier: SingleNetworkStatusSupplier, - private val tokensFeatureToggles: TokensFeatureToggles, ) { suspend operator fun invoke( @@ -59,20 +55,13 @@ class AssociateAssetUseCase( } private suspend fun isBalanceZero(userWalletId: UserWalletId, currency: CryptoCurrency): Boolean { - val networkStatus = if (tokensFeatureToggles.isNetworksLoadingRefactoringEnabled) { - singleNetworkStatusSupplier( - params = SingleNetworkStatusProducer.Params( - userWalletId = userWalletId, - network = currency.network, - ), - ) - .firstOrNull() - } else { - networksRepository.getNetworkStatusesSync( + val networkStatus = singleNetworkStatusSupplier( + params = SingleNetworkStatusProducer.Params( userWalletId = userWalletId, - networks = setOf(currency.network), - ).find { it.network == currency.network } - } + network = currency.network, + ), + ) + .firstOrNull() val networkCoinAmountStatus = (networkStatus?.value as? NetworkStatus.Verified) ?.amounts diff --git a/domain/transaction/src/main/java/com/tangem/domain/transaction/usecase/SendTransactionUseCase.kt b/domain/transaction/src/main/java/com/tangem/domain/transaction/usecase/SendTransactionUseCase.kt index 15efbc7500..e519d17ac5 100644 --- a/domain/transaction/src/main/java/com/tangem/domain/transaction/usecase/SendTransactionUseCase.kt +++ b/domain/transaction/src/main/java/com/tangem/domain/transaction/usecase/SendTransactionUseCase.kt @@ -19,7 +19,6 @@ import com.tangem.domain.common.TapWorkarounds.isTangemTwins import com.tangem.domain.demo.DemoConfig import com.tangem.domain.demo.DemoTransactionSender import com.tangem.domain.networks.single.SingleNetworkStatusFetcher -import com.tangem.domain.tokens.TokensFeatureToggles import com.tangem.domain.tokens.model.Network import com.tangem.domain.transaction.TransactionRepository import com.tangem.domain.transaction.error.SendTransactionError @@ -33,7 +32,6 @@ class SendTransactionUseCase( private val transactionRepository: TransactionRepository, private val walletManagersFacade: WalletManagersFacade, private val singleNetworkStatusFetcher: SingleNetworkStatusFetcher, - private val tokensFeatureToggles: TokensFeatureToggles, ) { suspend operator fun invoke( txsData: List, @@ -80,16 +78,15 @@ class SendTransactionUseCase( } cardSdkConfigRepository.setLinkedTerminal(linkedTerminal) + return sendResult .onRight { - if (tokensFeatureToggles.isNetworksLoadingRefactoringEnabled) { - singleNetworkStatusFetcher( - params = SingleNetworkStatusFetcher.Params( - userWalletId = userWallet.walletId, - network = network, - ), - ) - } + singleNetworkStatusFetcher( + params = SingleNetworkStatusFetcher.Params( + userWalletId = userWallet.walletId, + network = network, + ), + ) } .fold( ifRight = { result -> result.hashes.right() }, diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/common/SendBalanceUpdater.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/common/SendBalanceUpdater.kt index 69254405cf..bb10f2b5d3 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/common/SendBalanceUpdater.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/common/SendBalanceUpdater.kt @@ -52,7 +52,6 @@ internal class SendBalanceUpdater @AssistedInject constructor( userWalletId = userWallet.walletId, network = cryptoCurrency.network, delayMillis = delay, - refresh = true, ) } diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/model/SendModel.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/model/SendModel.kt index be16dae8cd..1aadbe4b88 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/model/SendModel.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/model/SendModel.kt @@ -1048,7 +1048,6 @@ internal class SendModel @Inject constructor( userWalletId = userWallet.walletId, network = cryptoCurrency.network, delayMillis = BALANCE_UPDATE_DELAY, - refresh = true, ) }, ).awaitAll() diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/helpers/StakingBalanceUpdater.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/helpers/StakingBalanceUpdater.kt index 02db8ad802..88d2974d5c 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/helpers/StakingBalanceUpdater.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/helpers/StakingBalanceUpdater.kt @@ -83,7 +83,6 @@ internal class StakingBalanceUpdater @AssistedInject constructor( userWalletId = userWallet.walletId, network = cryptoCurrencyStatus.currency.network, delayMillis = delay, - refresh = true, ) } 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 0b3a1eb4b5..235b3002ef 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 @@ -98,7 +98,7 @@ internal class SwapModel @Inject constructor( private val getExplorerTransactionUrlUseCase: GetExplorerTransactionUrlUseCase, private val shouldShowStoriesUseCase: ShouldShowStoriesUseCase, private val getStoryContentUseCase: GetStoryContentUseCase, - private val getUserCountryUseCase: GetUserCountryUseCase, + getUserCountryUseCase: GetUserCountryUseCase, getBalanceHidingSettingsUseCase: GetBalanceHidingSettingsUseCase, swapInteractorFactory: SwapInteractor.Factory, private val urlOpener: UrlOpener, @@ -1311,7 +1311,6 @@ internal class SwapModel @Inject constructor( userWalletId = userWalletId, network = network, delayMillis = UPDATE_BALANCE_DELAY_MILLIS, - refresh = true, ) } 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 4d4b8bc5e7..19f0df36c5 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 @@ -364,7 +364,6 @@ internal class TokenDetailsModel @Inject constructor( updateDelayedCurrencyStatusUseCase( userWalletId = userWalletId, network = toCryptoCurrency.network, - refresh = true, ) } } diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/intents/WalletWarningsClickIntents.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/intents/WalletWarningsClickIntents.kt index fbe13e7f90..bdf8d3095b 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/intents/WalletWarningsClickIntents.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/intents/WalletWarningsClickIntents.kt @@ -19,9 +19,6 @@ import com.tangem.domain.promo.ShouldShowPromoWalletUseCase import com.tangem.domain.promo.models.PromoId import com.tangem.domain.settings.NeverToSuggestRateAppUseCase import com.tangem.domain.settings.RemindToRateAppLaterUseCase -import com.tangem.domain.tokens.FetchTokenListUseCase -import com.tangem.domain.tokens.FetchTokenListUseCase.RefreshMode -import com.tangem.domain.tokens.TokensFeatureToggles import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.domain.tokens.model.analytics.TokenSwapPromoAnalyticsEvent import com.tangem.domain.wallets.legacy.UserWalletsListManager.Lockable.UnlockType @@ -91,7 +88,6 @@ internal class WalletWarningsClickIntentsImplementor @Inject constructor( private val stateHolder: WalletStateController, private val walletEventSender: WalletEventSender, private val derivePublicKeysUseCase: DerivePublicKeysUseCase, - private val fetchTokenListUseCase: FetchTokenListUseCase, private val setCardWasScannedUseCase: SetCardWasScannedUseCase, private val neverToSuggestRateAppUseCase: NeverToSuggestRateAppUseCase, private val remindToRateAppLaterUseCase: RemindToRateAppLaterUseCase, @@ -105,7 +101,6 @@ internal class WalletWarningsClickIntentsImplementor @Inject constructor( private val sendFeedbackEmailUseCase: SendFeedbackEmailUseCase, private val seedPhraseNotificationUseCase: SeedPhraseNotificationUseCase, private val urlOpener: UrlOpener, - private val tokensFeatureToggles: TokensFeatureToggles, private val multiNetworkStatusFetcher: MultiNetworkStatusFetcher, private val appRouter: AppRouter, ) : BaseWalletClickIntents(), WalletWarningsClickIntents { @@ -148,22 +143,13 @@ internal class WalletWarningsClickIntentsImplementor @Inject constructor( ).fold( ifLeft = { Timber.e(it, "Failed to derive public keys") }, ifRight = { - if (tokensFeatureToggles.isNetworksLoadingRefactoringEnabled) { - multiNetworkStatusFetcher( - params = MultiNetworkStatusFetcher.Params( - userWalletId = userWallet.walletId, - networks = missedAddressCurrencies.map(CryptoCurrency::network).toSet(), - ), - ) - } else { - fetchTokenListUseCase( + multiNetworkStatusFetcher( + params = MultiNetworkStatusFetcher.Params( userWalletId = userWallet.walletId, - mode = RefreshMode.SKIP_CURRENCIES, - currencies = missedAddressCurrencies, - ).onLeft { - Timber.e("Unable to refresh token list: $it") - } - } + networks = missedAddressCurrencies.map(CryptoCurrency::network).toSet(), + ), + ) + .onLeft { Timber.e("Unable to refresh token list: $it") } }, ) } From 73344b92dcc143ce60c7dd55e3cbe6a48f64a635 Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 7 May 2025 07:23:04 +0700 Subject: [PATCH 008/165] Updated on 2026-08-14 --- .../di/WalletConnectDataModule.kt | 48 +++++++++++---- .../network/ethereum/WcEthNetwork.kt | 47 ++++++++------- .../network/solana/WcSolanaNetwork.kt | 59 ++++++++++--------- .../pair/AssociateNetworksDelegate.kt | 20 ++++++- .../pair/DefaultWcPairUseCase.kt | 13 +--- .../sessions/DefaultWcSessionsManager.kt | 14 ++++- .../walletconnect/DefaultWcPairUseCaseTest.kt | 1 + .../domain/walletconnect/model/WcSession.kt | 2 + .../model/WcConnectedAppInfoModel.kt | 14 ++++- 9 files changed, 140 insertions(+), 78 deletions(-) diff --git a/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/di/WalletConnectDataModule.kt b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/di/WalletConnectDataModule.kt index 8d43fc1ebe..84a40f03e0 100644 --- a/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/di/WalletConnectDataModule.kt +++ b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/di/WalletConnectDataModule.kt @@ -42,6 +42,7 @@ import javax.inject.Singleton @Module @InstallIn(SingletonComponent::class) +@Suppress("TooManyFunctions") internal object WalletConnectDataModule { @Provides @@ -86,6 +87,7 @@ internal object WalletConnectDataModule { dispatchers: CoroutineDispatcherProvider, legacyStore: WalletConnectSessionsRepository, getWallets: GetWalletsUseCase, + associateNetworks: AssociateNetworksDelegate, ): DefaultWcSessionsManager { val scope = CoroutineScope(SupervisorJob() + dispatchers.io) return DefaultWcSessionsManager( @@ -93,6 +95,7 @@ internal object WalletConnectDataModule { dispatchers = dispatchers, legacyStore = legacyStore, getWallets = getWallets, + associateNetworks = associateNetworks, scope = scope, ) } @@ -121,12 +124,12 @@ internal object WalletConnectDataModule { @Singleton fun wcEthNetwork( @SdkMoshi moshi: Moshi, - excludedBlockchains: ExcludedBlockchains, sessionsManager: WcSessionsManager, factories: WcEthNetwork.Factories, + namespaceConverter: WcEthNetwork.NamespaceConverter, ): WcEthNetwork = WcEthNetwork( moshi = moshi, - excludedBlockchains = excludedBlockchains, + namespaceConverter = namespaceConverter, sessionsManager = sessionsManager, factories = factories, ) @@ -135,7 +138,7 @@ internal object WalletConnectDataModule { @Singleton fun wcSolanaNetwork( @SdkMoshi moshi: Moshi, - excludedBlockchains: ExcludedBlockchains, + namespaceConverter: WcSolanaNetwork.NamespaceConverter, sessionsManager: WcSessionsManager, factories: WcSolanaNetwork.Factories, walletManager: UserWalletManager, @@ -143,28 +146,28 @@ internal object WalletConnectDataModule { moshi = moshi, sessionsManager = sessionsManager, factories = factories, - excludedBlockchains = excludedBlockchains, + namespaceConverter = namespaceConverter, walletManager = walletManager, ) @Provides @Singleton fun caipNamespaceDelegate( - diHelperBox: DiHelperBox, + namespaceConverters: Set<@JvmSuppressWildcards WcNamespaceConverter>, walletManagersFacade: WalletManagersFacade, ): CaipNamespaceDelegate = CaipNamespaceDelegate( - namespaceConverters = diHelperBox.converters, + namespaceConverters = namespaceConverters, walletManagersFacade = walletManagersFacade, ) @Provides @Singleton fun associateNetworksDelegate( - diHelperBox: DiHelperBox, + namespaceConverters: Set<@JvmSuppressWildcards WcNamespaceConverter>, getWallets: GetWalletsUseCase, currenciesRepository: CurrenciesRepository, ): AssociateNetworksDelegate = AssociateNetworksDelegate( - namespaceConverters = diHelperBox.converters, + namespaceConverters = namespaceConverters, getWallets = getWallets, currenciesRepository = currenciesRepository, ) @@ -176,10 +179,16 @@ internal object WalletConnectDataModule { ethNetwork, solanaNetwork, ), - converters = setOf( - ethNetwork, - solanaNetwork, - ), + ) + + @Provides + @Singleton + fun namespaceConverters( + ethNamespaceConverter: WcEthNetwork.NamespaceConverter, + solanaNamespaceConverter: WcSolanaNetwork.NamespaceConverter, + ): Set<@JvmSuppressWildcards WcNamespaceConverter> = setOf( + ethNamespaceConverter, + solanaNamespaceConverter, ) @Provides @@ -188,6 +197,20 @@ internal object WalletConnectDataModule { return DefaultWcRequestUseCaseFactory(diHelperBox.handlers) } + @Provides + @Singleton + fun wcEthNetworkNamespaceConverter(excludedBlockchains: ExcludedBlockchains): WcEthNetwork.NamespaceConverter { + return WcEthNetwork.NamespaceConverter(excludedBlockchains) + } + + @Provides + @Singleton + fun wcSolanaNetworkNamespaceConverter( + excludedBlockchains: ExcludedBlockchains, + ): WcSolanaNetwork.NamespaceConverter { + return WcSolanaNetwork.NamespaceConverter(excludedBlockchains) + } + @Provides @Singleton fun providesWcDisconnectUseCase(sessionsManager: WcSessionsManager): WcDisconnectUseCase { @@ -195,7 +218,6 @@ internal object WalletConnectDataModule { } internal class DiHelperBox( - val converters: Set, val handlers: Set, ) } \ No newline at end of file diff --git a/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/network/ethereum/WcEthNetwork.kt b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/network/ethereum/WcEthNetwork.kt index 175f715409..5dda26b898 100644 --- a/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/network/ethereum/WcEthNetwork.kt +++ b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/network/ethereum/WcEthNetwork.kt @@ -22,12 +22,10 @@ import jakarta.inject.Inject internal class WcEthNetwork( private val moshi: Moshi, - private val excludedBlockchains: ExcludedBlockchains, private val sessionsManager: WcSessionsManager, private val factories: Factories, -) : WcRequestToUseCaseConverter, WcNamespaceConverter { - - override val namespaceKey: NamespaceKey = NamespaceKey("eip155") + private val namespaceConverter: NamespaceConverter, +) : WcRequestToUseCaseConverter { override fun toWcMethodName(request: WcSdkSessionRequest): WcEthMethodName? { val methodKey = request.request.method @@ -39,7 +37,7 @@ internal class WcEthNetwork( val name = toWcMethodName(request) ?: return null val method: WcEthMethod = name.toMethod(request) ?: return null val session = sessionsManager.findSessionByTopic(request.topic) ?: return null - val network = toNetwork(request.chainId.orEmpty(), session.wallet) ?: return null + val network = namespaceConverter.toNetwork(request.chainId.orEmpty(), session.wallet) ?: return null val accountAddress = when (method) { is WcEthMethod.MessageSign -> method.account is WcEthMethod.SendTransaction -> method.transaction.from @@ -100,24 +98,31 @@ internal class WcEthNetwork( return WcEthMethod.SignTypedData(params = params, account = account, dataForSign = data) } - override fun toNetwork(chainId: String, wallet: UserWallet): Network? { - return toNetwork(chainId, wallet, excludedBlockchains) - } + internal class NamespaceConverter constructor( + private val excludedBlockchains: ExcludedBlockchains, + ) : WcNamespaceConverter { - override fun toBlockchain(chainId: CAIP2): Blockchain? { - if (chainId.namespace != namespaceKey.key) return null - val ethChainId = chainId.reference.toIntOrNull() ?: return null - return Blockchain.fromChainId(ethChainId) - } + override val namespaceKey: NamespaceKey = NamespaceKey("eip155") - override fun toCAIP2(network: Network): CAIP2? { - val blockchain = Blockchain.fromId(network.id.value) - if (!blockchain.isEvm()) return null - val chainId = blockchain.getChainId() ?: return null - return CAIP2( - namespace = namespaceKey.key, - reference = chainId.toString(), - ) + override fun toNetwork(chainId: String, wallet: UserWallet): Network? { + return toNetwork(chainId, wallet, excludedBlockchains) + } + + override fun toBlockchain(chainId: CAIP2): Blockchain? { + if (chainId.namespace != namespaceKey.key) return null + val ethChainId = chainId.reference.toIntOrNull() ?: return null + return Blockchain.fromChainId(ethChainId) + } + + override fun toCAIP2(network: Network): CAIP2? { + val blockchain = Blockchain.fromId(network.id.value) + if (!blockchain.isEvm()) return null + val chainId = blockchain.getChainId() ?: return null + return CAIP2( + namespace = namespaceKey.key, + reference = chainId.toString(), + ) + } } internal class Factories @Inject constructor( diff --git a/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/network/solana/WcSolanaNetwork.kt b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/network/solana/WcSolanaNetwork.kt index 275c995ecf..21cc7cb152 100644 --- a/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/network/solana/WcSolanaNetwork.kt +++ b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/network/solana/WcSolanaNetwork.kt @@ -26,11 +26,9 @@ internal class WcSolanaNetwork( private val moshi: Moshi, private val sessionsManager: WcSessionsManager, private val factories: Factories, - private val excludedBlockchains: ExcludedBlockchains, + private val namespaceConverter: NamespaceConverter, private val walletManager: UserWalletManager, -) : WcNamespaceConverter, WcRequestToUseCaseConverter { - - override val namespaceKey: NamespaceKey = NamespaceKey("solana") +) : WcRequestToUseCaseConverter { override fun toWcMethodName(request: WcSdkSessionRequest): WcSolanaMethodName? { val methodKey = request.request.method @@ -42,7 +40,7 @@ internal class WcSolanaNetwork( val name = toWcMethodName(request) ?: return null val method: WcSolanaMethod = name.toMethod(request) ?: return null val session = sessionsManager.findSessionByTopic(request.topic) ?: return null - val network = toNetwork(request.chainId.orEmpty(), session.wallet) ?: return null + val network = namespaceConverter.toNetwork(request.chainId.orEmpty(), session.wallet) ?: return null val accountAddress = getAccountAddress(network) val context = WcMethodUseCaseContext( session = session, @@ -66,31 +64,38 @@ internal class WcSolanaNetwork( } } - override fun toBlockchain(chainId: CAIP2): Blockchain? { - if (chainId.namespace != namespaceKey.key) return null - return when (chainId.reference) { - MAINNET_CHAIN_ID -> Blockchain.Solana - TESTNET_CHAIN_ID -> Blockchain.SolanaTestnet - else -> null - } - } + internal class NamespaceConverter @Inject constructor( + private val excludedBlockchains: ExcludedBlockchains, + ) : WcNamespaceConverter { - override fun toNetwork(chainId: String, wallet: UserWallet): Network? { - return toNetwork(chainId, wallet, excludedBlockchains) - } + override val namespaceKey: NamespaceKey = NamespaceKey("solana") - override fun toCAIP2(network: Network): CAIP2? { - val blockchain = Blockchain.fromId(network.id.value) - val chainId = when (blockchain) { - Blockchain.Solana -> MAINNET_CHAIN_ID - Blockchain.SolanaTestnet -> TESTNET_CHAIN_ID - else -> null + override fun toBlockchain(chainId: CAIP2): Blockchain? { + if (chainId.namespace != namespaceKey.key) return null + return when (chainId.reference) { + MAINNET_CHAIN_ID -> Blockchain.Solana + TESTNET_CHAIN_ID -> Blockchain.SolanaTestnet + else -> null + } + } + + override fun toNetwork(chainId: String, wallet: UserWallet): Network? { + return toNetwork(chainId, wallet, excludedBlockchains) + } + + override fun toCAIP2(network: Network): CAIP2? { + val blockchain = Blockchain.fromId(network.id.value) + val chainId = when (blockchain) { + Blockchain.Solana -> MAINNET_CHAIN_ID + Blockchain.SolanaTestnet -> TESTNET_CHAIN_ID + else -> null + } + chainId ?: return null + return CAIP2( + namespace = namespaceKey.key, + reference = chainId, + ) } - chainId ?: return null - return CAIP2( - namespace = namespaceKey.key, - reference = chainId, - ) } private fun WcSolanaMethodName.toMethod(request: WcSdkSessionRequest): WcSolanaMethod? { diff --git a/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/pair/AssociateNetworksDelegate.kt b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/pair/AssociateNetworksDelegate.kt index 15ffca4caf..04906abf44 100644 --- a/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/pair/AssociateNetworksDelegate.kt +++ b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/pair/AssociateNetworksDelegate.kt @@ -1,6 +1,7 @@ package com.tangem.data.walletconnect.pair import com.reown.walletkit.client.Wallet +import com.reown.walletkit.client.Wallet.Model.Namespace import com.tangem.data.walletconnect.utils.WcNamespaceConverter import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.domain.tokens.model.Network @@ -16,6 +17,16 @@ internal class AssociateNetworksDelegate constructor( private val currenciesRepository: CurrenciesRepository, ) { + suspend fun associate(wallet: UserWallet, namespaces: Map): Set { + val walletNetworks = getWalletNetworks(wallet) + val namespacesSet = namespaces.values.flatMap { proposal -> proposal.chains ?: listOf() }.toSet() + return namespacesSet.mapNotNullTo(mutableSetOf()) { chainId -> + val wcNetwork = namespaceConverters + .firstNotNullOfOrNull { it.toNetwork(chainId, wallet) } ?: return@mapNotNullTo null + walletNetworks.find { network -> wcNetwork.id == network.id } + } + } + @Throws(WcPairError.UnsupportedNetworks::class) suspend fun associate(sessionProposal: Wallet.Model.SessionProposal): Map { val userWallets = getWallets.invokeSync().filter { it.isMultiCurrency } @@ -31,9 +42,7 @@ internal class AssociateNetworksDelegate constructor( requiredNamespaces: Set, optionalNamespaces: Set, ): ProposalNetwork { - val walletNetworks = currenciesRepository.getMultiCurrencyWalletCurrenciesSync(wallet.walletId) - .filterIsInstance() - .map { it.network } + val walletNetworks = getWalletNetworks(wallet) val unknownRequired = mutableSetOf() val missingRequired = mutableSetOf() @@ -74,6 +83,11 @@ internal class AssociateNetworksDelegate constructor( ) } + private suspend fun getWalletNetworks(wallet: UserWallet): List = + currenciesRepository.getMultiCurrencyWalletCurrenciesSync(wallet.walletId) + .filterIsInstance() + .map { it.network } + private fun Map.setOfChainId(): Set = this.values.flatMap { proposal -> proposal.chains ?: listOf() }.toSet() diff --git a/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/pair/DefaultWcPairUseCase.kt b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/pair/DefaultWcPairUseCase.kt index fc91e5eefe..5bcbce241a 100644 --- a/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/pair/DefaultWcPairUseCase.kt +++ b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/pair/DefaultWcPairUseCase.kt @@ -14,7 +14,6 @@ import com.tangem.domain.walletconnect.model.sdkcopy.WcAppMetaData import com.tangem.domain.walletconnect.repository.WcSessionsManager import com.tangem.domain.walletconnect.usecase.pair.WcPairState import com.tangem.domain.walletconnect.usecase.pair.WcPairUseCase -import com.tangem.domain.wallets.models.UserWallet import dagger.assisted.Assisted import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject @@ -78,9 +77,11 @@ internal class DefaultWcPairUseCase @AssistedInject constructor( sessionForApprove = sessionForApprove, sdkSessionProposal = sdkSessionProposal, ).map { settledSession -> - val newSession = settledSession.session.toDomain( + val newSession = WcSession( wallet = sessionForApprove.wallet, + sdkModel = WcSdkSessionConverter.convert(settledSession.session), securityStatus = proposalState.dAppSession.securityStatus, + networks = sessionForApprove.network.toSet(), ) sessionsManager.saveSession(newSession) newSession @@ -141,14 +142,6 @@ internal class DefaultWcPairUseCase @AssistedInject constructor( } },) - private fun Wallet.Model.Session.toDomain(wallet: UserWallet, securityStatus: CheckDAppResult): WcSession { - return WcSession( - wallet = wallet, - sdkModel = WcSdkSessionConverter.convert(this), - securityStatus = securityStatus, - ) - } - private sealed interface TerminalAction { data class Approve(val sessionForApprove: WcSessionApprove) : TerminalAction data object Reject : TerminalAction diff --git a/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/sessions/DefaultWcSessionsManager.kt b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/sessions/DefaultWcSessionsManager.kt index 98e554f3fe..1c537ef70d 100644 --- a/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/sessions/DefaultWcSessionsManager.kt +++ b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/sessions/DefaultWcSessionsManager.kt @@ -6,6 +6,7 @@ import arrow.core.right import com.domain.blockaid.models.dapp.CheckDAppResult import com.reown.walletkit.client.Wallet import com.reown.walletkit.client.WalletKit +import com.tangem.data.walletconnect.pair.AssociateNetworksDelegate import com.tangem.data.walletconnect.utils.WcSdkObserver import com.tangem.data.walletconnect.utils.WcSdkSessionConverter import com.tangem.datasource.local.walletconnect.WalletConnectStore @@ -29,6 +30,7 @@ internal class DefaultWcSessionsManager( private val legacyStore: WalletConnectSessionsRepository, private val getWallets: GetWalletsUseCase, private val dispatchers: CoroutineDispatcherProvider, + private val associateNetworks: AssociateNetworksDelegate, private val scope: CoroutineScope, ) : WcSessionsManager, WcSdkObserver { @@ -76,10 +78,12 @@ internal class DefaultWcSessionsManager( ?: return@withContext null val sdkSession = WalletKit.getActiveSessionByTopic(topic) ?: return@withContext null val wallet = storedSession.wallet + val networks = associateNetworks.associate(wallet, sdkSession.namespaces) WcSession( wallet = wallet, sdkModel = WcSdkSessionConverter.convert(sdkSession), securityStatus = storedSession.securityStatus, + networks = networks, ) } @@ -114,7 +118,7 @@ internal class DefaultWcSessionsManager( return mustSaveInNewStore.isNotEmpty() } - private fun associate( + private suspend fun associate( inSdk: List, inStore: Set, wallets: List, @@ -122,7 +126,13 @@ internal class DefaultWcSessionsManager( val wcSessions = inStore.mapNotNull { session -> val wallet = wallets.find { it.walletId == session.walletId } ?: return@mapNotNull null val sdkSession = inSdk.find { it.topic == session.topic } ?: return@mapNotNull null - WcSession(wallet = wallet, sdkModel = WcSdkSessionConverter.convert(sdkSession), session.securityStatus) + val networks = associateNetworks.associate(wallet, sdkSession.namespaces) + WcSession( + wallet = wallet, + sdkModel = WcSdkSessionConverter.convert(sdkSession), + securityStatus = session.securityStatus, + networks = networks, + ) } return wcSessions } diff --git a/data/wallet-connect/src/test/kotlin/com/tangem/domain/walletconnect/DefaultWcPairUseCaseTest.kt b/data/wallet-connect/src/test/kotlin/com/tangem/domain/walletconnect/DefaultWcPairUseCaseTest.kt index 9b0b650724..6a86ee96f2 100644 --- a/data/wallet-connect/src/test/kotlin/com/tangem/domain/walletconnect/DefaultWcPairUseCaseTest.kt +++ b/data/wallet-connect/src/test/kotlin/com/tangem/domain/walletconnect/DefaultWcPairUseCaseTest.kt @@ -92,6 +92,7 @@ internal class DefaultWcPairUseCaseTest { wallet = sessionForApprove.wallet, sdkModel = WcSdkSessionConverter.convert(this), securityStatus = CheckDAppResult.SAFE, + networks = setOf(), ) private fun useCaseFactory() = DefaultWcPairUseCase( diff --git a/domain/wallet-connect/models/src/main/java/com/tangem/domain/walletconnect/model/WcSession.kt b/domain/wallet-connect/models/src/main/java/com/tangem/domain/walletconnect/model/WcSession.kt index d3707d52f6..a2b1449d45 100644 --- a/domain/wallet-connect/models/src/main/java/com/tangem/domain/walletconnect/model/WcSession.kt +++ b/domain/wallet-connect/models/src/main/java/com/tangem/domain/walletconnect/model/WcSession.kt @@ -1,11 +1,13 @@ package com.tangem.domain.walletconnect.model import com.domain.blockaid.models.dapp.CheckDAppResult +import com.tangem.domain.tokens.model.Network import com.tangem.domain.walletconnect.model.sdkcopy.WcSdkSession import com.tangem.domain.wallets.models.UserWallet data class WcSession( val wallet: UserWallet, + val networks: Set, val sdkModel: WcSdkSession, val securityStatus: CheckDAppResult, ) \ No newline at end of file diff --git a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/model/WcConnectedAppInfoModel.kt b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/model/WcConnectedAppInfoModel.kt index 6126b5f404..a751963826 100644 --- a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/model/WcConnectedAppInfoModel.kt +++ b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/model/WcConnectedAppInfoModel.kt @@ -6,6 +6,7 @@ import com.tangem.core.decompose.di.ModelScoped import com.tangem.core.decompose.model.Model import com.tangem.core.decompose.model.ParamsContainer import com.tangem.core.decompose.ui.UiMessageSender +import com.tangem.core.ui.extensions.iconResId import com.tangem.core.ui.extensions.stringReference import com.tangem.core.ui.message.SnackbarMessage import com.tangem.domain.walletconnect.model.WcSession @@ -13,9 +14,10 @@ import com.tangem.domain.walletconnect.usecase.WcSessionsUseCase import com.tangem.domain.walletconnect.usecase.disconnect.WcDisconnectUseCase import com.tangem.features.walletconnect.connections.components.WcConnectedAppInfoComponent import com.tangem.features.walletconnect.connections.entity.WcConnectedAppInfoUM +import com.tangem.features.walletconnect.connections.entity.WcNetworkInfoItem import com.tangem.features.walletconnect.connections.entity.WcPrimaryButtonConfig import com.tangem.utils.coroutines.CoroutineDispatcherProvider -import kotlinx.collections.immutable.persistentListOf +import kotlinx.collections.immutable.toImmutableList import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.update @@ -56,7 +58,15 @@ internal class WcConnectedAppInfoModel @Inject constructor( isVerified = session.securityStatus == CheckDAppResult.SAFE, appSubtitle = session.sdkModel.appMetaData.description, walletName = session.wallet.name, - networks = persistentListOf(), // TODO(wc): Nikolai & Doston: Where to find networks??? + networks = session.networks + .map { + WcNetworkInfoItem( + id = it.id.value, + icon = it.iconResId, + name = it.name, + symbol = it.currencySymbol, + ) + }.toImmutableList(), disconnectButtonConfig = WcPrimaryButtonConfig( showProgress = false, enabled = true, From a493d19d1a5253b244514336dfcb89fdd8fddc14 Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 7 May 2025 14:39:16 +0400 Subject: [PATCH 009/165] Updated on 2026-08-14 --- core/res/src/main/res/values-de/strings.xml | 33 ++- core/res/src/main/res/values-ja/strings.xml | 17 +- core/res/src/main/res/values/strings.xml | 4 +- .../src/main/res/drawable/ic_fee_new_24.xml | 21 ++ .../main/res/drawable/ic_user_square_24.xml | 17 ++ .../routing/DefaultWcRoutingComponent.kt | 6 +- .../components/WcSignTransactionComponent.kt | 29 +-- .../WcSignTransactionContainerComponent.kt | 123 +++++++++++ .../WcSignTransactionRequestInfoComponent.kt | 24 +++ .../transaction/entity/WcSignTransactionUM.kt | 22 +- .../model/WcSignTransactionModel.kt | 18 +- .../routes/WcSignTransactionRoutes.kt | 15 ++ .../common/TransactionRequestInfoContent.kt | 203 +++++++++++++++--- .../transaction/ui/common/WcAddressItem.kt | 44 ++++ .../transaction/ui/common/WcNetworkFeeItem.kt | 67 ++++++ .../transaction/ui/common/WcNetworkItem.kt | 2 +- .../transaction/ui/common/WcWalletItem.kt | 2 +- .../sign/WcSignTransactionModalBottomSheet.kt | 154 ------------- ...cSignTransactionModalBottomSheetContent.kt | 191 ++++++++++++---- .../transaction/utils/WalletAddressUtils.kt | 7 + .../utils/WcSignTransactionUtils.kt | 87 ++++++-- 21 files changed, 800 insertions(+), 286 deletions(-) create mode 100644 core/ui/src/main/res/drawable/ic_fee_new_24.xml create mode 100644 core/ui/src/main/res/drawable/ic_user_square_24.xml create mode 100644 features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/components/WcSignTransactionContainerComponent.kt create mode 100644 features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/components/WcSignTransactionRequestInfoComponent.kt create mode 100644 features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/routes/WcSignTransactionRoutes.kt create mode 100644 features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/ui/common/WcAddressItem.kt create mode 100644 features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/ui/common/WcNetworkFeeItem.kt delete mode 100644 features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/ui/sign/WcSignTransactionModalBottomSheet.kt create mode 100644 features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/utils/WalletAddressUtils.kt diff --git a/core/res/src/main/res/values-de/strings.xml b/core/res/src/main/res/values-de/strings.xml index aabccb9ff7..d3c9f55037 100644 --- a/core/res/src/main/res/values-de/strings.xml +++ b/core/res/src/main/res/values-de/strings.xml @@ -278,7 +278,7 @@ Kann nicht gegen %s ausgetauscht werden Bereitgestellt von Status - Tangem bietet Token-Swaps über Drittanbieter gemäß den jeweiligen Bedingungen des jeweiligen Anbieters an. + Anbieter erleichtern Transaktionen Anbieter Es ist ein Fehler aufgetreten. Code: %s Fehler %1$s. Der ausgewählte Anbieter kann die angegebene Transaktion nicht verarbeiten. Bitte runde den Wert auf %2$s auf oder änder diesen. @@ -319,11 +319,11 @@ Verifizierung erforderlich Wartet auf Transaktions-Hash Liste aller Token, die deiner Wallet hinzugefügt wurden - Beste Preise werden abgerufen … + Aktuelle Preise werden abgerufen ... Variabler Zinssatz Durch die Nutzung der Swap-Funktion erklärst du dich mit den folgenden Bedingungen des Anbieters einverstanden %s Durch die Nutzung der Swap-Funktionalität erklärst du dich mit des Anbieters %1$s und %2$s einverstanden - Weitere Anbieter werden bald folgen.\nBleib dabei! + Weitere Anbieter folgen in Kürze Anbieter Bester Preis Verfügbar bis zu %s @@ -419,6 +419,7 @@ Dieses Asset wird derzeit in der Wallet nicht unterstützt. Dieses Asset ist für dieses Wallet nicht verfügbar Hinzufügen + APY %s Verfügbare Netzwerke Mein Portfolio Markt @@ -448,6 +449,9 @@ Top-Gewinner Top-Verlierer Beliebt + Staking ist der einfachste Weg, um Belohnungen für Deine Kryptowährung zu erhalten. %s + Mehr anzeigen + Verdiene bis zu %s APY Über %s %d börse @@ -530,6 +534,13 @@ Basisinformationen Netzwerk Vertragsadresse + Kontrollabschnitt + Kontrollabschnitt + Kontrollabschnitt + Kontrollabschnitt + Kontrollabschnitt + Kontrollabschnitt + Kontrollabschnitt Letzter Verkaufspreis Raritätsetikett Seltenheitsgrad @@ -1051,6 +1062,8 @@ Validierer: %s Minimum %s Der Mindesttransaktionsbetrag beträgt %1$s. + Die Netzwerkgebühren für beliebte Token im Tron-Netzwerk können höher sein. Das Staking von TRX kann helfen, die Transaktionskosten zu senken. + Spare bei den Tron-Netzwerkgebühren Versuche es erneut Du hast dieselbe Karte oder Ring gescannt. Um ein Zwillings-Wallet zu erstellen, musst du die Karte oder Ring mit der Nummer %d scannen. Du hast die falsche Doppelkarte oder Ring gescannt. Bitte versuche eine andere Karte oder Ring @@ -1237,6 +1250,7 @@ Zeitüberschreitungsfehler. Bitte versuche es später erneut. WalletConnect konnte nicht hergestellt werden Diese Domäne kann nicht verifiziert werden. Überprüfe die Anfrage sorgfältig und bestätige diese dann. + Geh zurück zu Deinem Browser und stell erneut eine Verbindung über WalletConnect her. WalletConnect-Sitzung wurde getrennt Fehlercode: %s. Wenn das Problem weiterhin besteht, wende Dich bitte an unseren Support. Wir haben einen unbekannten Fehler festgestellt. @@ -1244,14 +1258,22 @@ Nicht unterstützte Netzwerke Tangem unterstützt ein erforderliches Netzwerk um %s. Verifizierte Domain + Falsche Karte oder falscher Ring in der Tangem-App ausgewählt Wir haben eine Art Problem + Verbinden Netzwerk Netzwerke Wallet + Verbundene Netzwerke + Anzeige des Kontostands und der Aktivitäten in Deiner Wallet Signiere die Transaktionen ohne eine Vorankündigung Genehmigung für Transaktionen anfordern Wird nicht in der Lage sein, + Möchte + Verbindungsanfrage Verbindungen + Inhalt + Daten kopieren Alle trennen Text über die Trennung aller dApps Alle dApps trennen @@ -1262,6 +1284,11 @@ Keine Sitzungen Diese Domain wird von mehreren Sicherheitsanbietern als unsicher eingestuft. Verlasse diese umgehend, um Dein Vermögen zu schützen. Bekanntes Sicherheitsrisiko + Anfrage von + Art der Signatur + Transaktionsanfrage + Transaktionsanfrage + Wallet verbinden Verwerfen Du hast eine unterbrochene Sicherung. Möchtest du diese fortsetzen? Ja, fortsetzen diff --git a/core/res/src/main/res/values-ja/strings.xml b/core/res/src/main/res/values-ja/strings.xml index 31ae60add2..06e907a6d2 100644 --- a/core/res/src/main/res/values-ja/strings.xml +++ b/core/res/src/main/res/values-ja/strings.xml @@ -527,6 +527,13 @@ 基本情報 チェーン コントラクトアドレス + スタブ + スタブ + スタブ + スタブ + スタブ + スタブ + スタブ 最終販売価格 レアリティ・ラベル レアリティ・ランク @@ -1044,7 +1051,7 @@ バリデーター: %s 最小%s 最小取引金額は%1$sです。 - Tronネットワークの人気トークンの手数料は高めです。TRXをステーキングすると、より安く、あるいは無料で取引できます。 + Tronネットワークの人気トークンの手数料は高い可能性があります。TRXをステーキングすると、取引コストを削減できる可能性があります。 Tronネットワーク手数料を節約 もう一度やり直してください 同じカードをスキャンしました。ツインウォレットを作成するには、番号%dのカードをスキャンする必要があります。 @@ -1237,6 +1244,7 @@ 検証済みドメイン Tangemアプリで誤ったカードまたはリングが選択されました 問題が起きています + アドレス 接続する ネットワーク ネットワーク @@ -1249,6 +1257,8 @@ したい 接続リクエスト 接続 + 内容 + データをコピー すべての接続を解除する すべてのdAppsの接続解除に関するテキスト すべてのdAppを接続解除する @@ -1259,6 +1269,11 @@ セッションなし このドメインは複数のセキュリティプロバイダーから安全でないとの警告を受けています。あなたの資産を守るため、直ちにアクセスを中止してください。 既知のセキュリティリスク + リクエスト元 + 署名タイプ + 宛先 + 取引リクエスト + 取引リクエスト ウォレットコネクト 破棄 バックアップが中断されました。再開しますか? diff --git a/core/res/src/main/res/values/strings.xml b/core/res/src/main/res/values/strings.xml index b4b174998e..2081101d9d 100644 --- a/core/res/src/main/res/values/strings.xml +++ b/core/res/src/main/res/values/strings.xml @@ -1063,7 +1063,7 @@ validator: %s Minimum %s The minimum transaction amount is %1$s. - Tron network fees for popular tokens are higher. Stake some TRX for cheaper or free transactions. + Tron network fees for popular tokens can be higher. Staking TRX may help reduce transaction costs. Save on Tron network fees Try again You\'ve scanned the same card. To create a twin wallet you need to scan the card with number %d @@ -1305,6 +1305,7 @@ Verified domain Wrong card or ring selected in Tangem App We\'ve got some kind of problem + Address Connect Network Networks @@ -1331,6 +1332,7 @@ Known security risk Request from Signature Type + To Transaction request Transaction request Wallet connect diff --git a/core/ui/src/main/res/drawable/ic_fee_new_24.xml b/core/ui/src/main/res/drawable/ic_fee_new_24.xml new file mode 100644 index 0000000000..a644c488cf --- /dev/null +++ b/core/ui/src/main/res/drawable/ic_fee_new_24.xml @@ -0,0 +1,21 @@ + + + + + + diff --git a/core/ui/src/main/res/drawable/ic_user_square_24.xml b/core/ui/src/main/res/drawable/ic_user_square_24.xml new file mode 100644 index 0000000000..655ae700f5 --- /dev/null +++ b/core/ui/src/main/res/drawable/ic_user_square_24.xml @@ -0,0 +1,17 @@ + + + + + diff --git a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/routing/DefaultWcRoutingComponent.kt b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/routing/DefaultWcRoutingComponent.kt index 6a424d83be..45ccb328ab 100644 --- a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/routing/DefaultWcRoutingComponent.kt +++ b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/routing/DefaultWcRoutingComponent.kt @@ -16,7 +16,7 @@ import com.tangem.core.decompose.model.getOrCreateModel import com.tangem.core.ui.decompose.ComposableContentComponent import com.tangem.features.walletconnect.components.WcRoutingComponent import com.tangem.features.walletconnect.connections.components.WcAppInfoContainerComponent -import com.tangem.features.walletconnect.transaction.components.WcSignTransactionComponent +import com.tangem.features.walletconnect.transaction.components.WcSignTransactionContainerComponent import dagger.assisted.Assisted import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject @@ -55,9 +55,9 @@ internal class DefaultWcRoutingComponent @AssistedInject constructor( router = model.innerRouter, ) return when (config) { - is WcInnerRoute.SignMessage -> WcSignTransactionComponent( + is WcInnerRoute.SignMessage -> WcSignTransactionContainerComponent( childContext, - params = WcSignTransactionComponent.Params(config.rawRequest), + params = WcSignTransactionContainerComponent.Params(config.rawRequest), ) is WcInnerRoute.Pair -> WcAppInfoContainerComponent( childContext, diff --git a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/components/WcSignTransactionComponent.kt b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/components/WcSignTransactionComponent.kt index 9ec9f43b8f..9ffe0a28d5 100644 --- a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/components/WcSignTransactionComponent.kt +++ b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/components/WcSignTransactionComponent.kt @@ -1,37 +1,28 @@ package com.tangem.features.walletconnect.transaction.components import androidx.compose.runtime.Composable -import androidx.compose.runtime.getValue import androidx.compose.ui.Modifier import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.tangem.core.decompose.context.AppComponentContext -import com.tangem.core.decompose.model.getOrCreateModel -import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig import com.tangem.core.ui.decompose.ComposableContentComponent -import com.tangem.domain.walletconnect.model.sdkcopy.WcSdkSessionRequest import com.tangem.features.walletconnect.transaction.model.WcSignTransactionModel -import com.tangem.features.walletconnect.transaction.ui.sign.WcSignTransactionModalBottomSheet +import com.tangem.features.walletconnect.transaction.ui.sign.WcSignTransactionModalBottomSheetContent internal class WcSignTransactionComponent( - appComponentContext: AppComponentContext, - params: Params, + private val appComponentContext: AppComponentContext, + private val model: WcSignTransactionModel, + private val transactionInfoOnClick: () -> Unit, ) : AppComponentContext by appComponentContext, ComposableContentComponent { - private val model: WcSignTransactionModel = getOrCreateModel(params) - @Composable override fun Content(modifier: Modifier) { - val content by model.uiState.collectAsStateWithLifecycle() - content?.let { - WcSignTransactionModalBottomSheet( - config = TangemBottomSheetConfig( - isShown = true, - onDismissRequest = it.actions.onDismiss, - content = it, - ), + val content = model.uiState.collectAsStateWithLifecycle().value + + if (content != null) { + WcSignTransactionModalBottomSheetContent( + state = content, + onClickTransactionRequest = transactionInfoOnClick, ) } } - - data class Params(val rawRequest: WcSdkSessionRequest) } \ No newline at end of file diff --git a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/components/WcSignTransactionContainerComponent.kt b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/components/WcSignTransactionContainerComponent.kt new file mode 100644 index 0000000000..7bfc3e7738 --- /dev/null +++ b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/components/WcSignTransactionContainerComponent.kt @@ -0,0 +1,123 @@ +package com.tangem.features.walletconnect.transaction.components + +import androidx.compose.animation.animateContentSize +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.ui.Modifier +import com.arkivanov.decompose.ComponentContext +import com.arkivanov.decompose.extensions.compose.stack.Children +import com.arkivanov.decompose.extensions.compose.subscribeAsState +import com.arkivanov.decompose.router.stack.StackNavigation +import com.arkivanov.decompose.router.stack.childStack +import com.arkivanov.decompose.router.stack.navigate +import com.tangem.core.decompose.context.AppComponentContext +import com.tangem.core.decompose.context.childByContext +import com.tangem.core.decompose.model.getOrCreateModel +import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig +import com.tangem.core.ui.components.bottomsheets.modal.TangemModalBottomSheet +import com.tangem.core.ui.components.bottomsheets.modal.TangemModalBottomSheetTitle +import com.tangem.core.ui.decompose.ComposableContentComponent +import com.tangem.core.ui.extensions.resourceReference +import com.tangem.core.ui.res.TangemTheme +import com.tangem.domain.walletconnect.model.sdkcopy.WcSdkSessionRequest +import com.tangem.features.walletconnect.impl.R +import com.tangem.features.walletconnect.transaction.model.WcSignTransactionModel +import com.tangem.features.walletconnect.transaction.routes.WcSignTransactionRoutes + +internal class WcSignTransactionContainerComponent( + private val appComponentContext: AppComponentContext, + params: Params, +) : AppComponentContext by appComponentContext, ComposableContentComponent { + + private val model: WcSignTransactionModel = getOrCreateModel(params = params) + private val contentNavigation = StackNavigation() + private val contentStack = childStack( + source = contentNavigation, + serializer = WcSignTransactionRoutes.serializer(), + initialConfiguration = WcSignTransactionRoutes.Transaction, + childFactory = ::screenChild, + ) + + private fun dismiss() { + model.dismiss() + } + + @Composable + override fun Content(modifier: Modifier) { + val content by contentStack.subscribeAsState() + TangemModalBottomSheet( + config = TangemBottomSheetConfig( + isShown = true, + onDismissRequest = ::dismiss, + content = content.active.configuration, + ), + containerColor = TangemTheme.colors.background.tertiary, + title = { config -> Title(route = config) }, + content = { + Box( + modifier = Modifier + .fillMaxWidth() + .animateContentSize(), + ) { + Children(stack = content) { child -> + child.instance.Content(modifier = Modifier) + } + } + }, + ) + } + + @Composable + private fun Title(route: WcSignTransactionRoutes) { + TangemModalBottomSheetTitle( + title = resourceReference(R.string.wc_wallet_connect), + startIconRes = when (route) { + is WcSignTransactionRoutes.Transaction -> null + else -> R.drawable.ic_back_24 + }, + onStartClick = when (route) { + is WcSignTransactionRoutes.Transaction -> null + else -> ::toTransaction + }, + endIconRes = when (route) { + is WcSignTransactionRoutes.Transaction -> R.drawable.ic_close_24 + else -> null + }, + onEndClick = when (route) { + is WcSignTransactionRoutes.Transaction -> ::dismiss + else -> null + }, + ) + } + + private fun toTransactionRequestInfo() { + contentNavigation.navigate { + listOf(WcSignTransactionRoutes.TransactionRequestInfo) + } + } + + private fun toTransaction() { + contentNavigation.navigate { + listOf(WcSignTransactionRoutes.Transaction) + } + } + + private fun screenChild( + config: WcSignTransactionRoutes, + componentContext: ComponentContext, + ): ComposableContentComponent = when (config) { + is WcSignTransactionRoutes.Transaction -> WcSignTransactionComponent( + appComponentContext = childByContext(componentContext), + model = model, + transactionInfoOnClick = ::toTransactionRequestInfo, + ) + is WcSignTransactionRoutes.TransactionRequestInfo -> WcSignTransactionRequestInfoComponent( + appComponentContext = childByContext(componentContext), + model = model, + ) + } + + data class Params(val rawRequest: WcSdkSessionRequest) +} \ No newline at end of file diff --git a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/components/WcSignTransactionRequestInfoComponent.kt b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/components/WcSignTransactionRequestInfoComponent.kt new file mode 100644 index 0000000000..7563b8a646 --- /dev/null +++ b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/components/WcSignTransactionRequestInfoComponent.kt @@ -0,0 +1,24 @@ +package com.tangem.features.walletconnect.transaction.components + +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import com.tangem.core.decompose.context.AppComponentContext +import com.tangem.core.ui.decompose.ComposableContentComponent +import com.tangem.features.walletconnect.transaction.model.WcSignTransactionModel +import com.tangem.features.walletconnect.transaction.ui.common.TransactionRequestInfoContent + +internal class WcSignTransactionRequestInfoComponent( + private val appComponentContext: AppComponentContext, + private val model: WcSignTransactionModel, +) : AppComponentContext by appComponentContext, ComposableContentComponent { + + @Composable + override fun Content(modifier: Modifier) { + val content = model.uiState.collectAsStateWithLifecycle().value + + if (content != null) { + TransactionRequestInfoContent(content) + } + } +} \ No newline at end of file diff --git a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/entity/WcSignTransactionUM.kt b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/entity/WcSignTransactionUM.kt index 3f796ce6b1..77379d44e1 100644 --- a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/entity/WcSignTransactionUM.kt +++ b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/entity/WcSignTransactionUM.kt @@ -8,19 +8,10 @@ import kotlinx.collections.immutable.ImmutableList @Immutable internal data class WcSignTransactionUM( - @DrawableRes val startIconRes: Int, - @DrawableRes val endIconRes: Int, - @DrawableRes val transactionIconRes: Int, val actions: WcTransactionActionsUM, - val state: State = State.TRANSACTION, val transaction: WcTransactionUM, val transactionRequestInfo: WcTransactionRequestInfoUM, -) : TangemBottomSheetConfigContent { - - enum class State { - TRANSACTION, TRANSACTION_REQUEST_INFO - } -} +) : TangemBottomSheetConfigContent @Immutable internal data class WcTransactionUM( @@ -30,26 +21,31 @@ internal data class WcTransactionUM( val appSubtitle: String, val walletName: String, val networkInfo: WcNetworkInfoUM, + val addressText: String? = null, + val networkFee: String? = null, val isLoading: Boolean = false, ) @Immutable internal data class WcTransactionRequestInfoUM( + val blocks: ImmutableList, +) + +@Immutable +internal data class WcTransactionRequestBlockUM( val info: ImmutableList, ) @Immutable internal data class WcTransactionRequestInfoItemUM( val title: TextReference, - val description: String, + val description: String = "", ) @Immutable internal data class WcTransactionActionsUM( - val transactionRequestOnClick: () -> Unit, val onDismiss: () -> Unit, val onSign: () -> Unit, - val onBack: () -> Unit, val onCopy: () -> Unit, ) diff --git a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/model/WcSignTransactionModel.kt b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/model/WcSignTransactionModel.kt index e5b5c90b30..ed6ea36064 100644 --- a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/model/WcSignTransactionModel.kt +++ b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/model/WcSignTransactionModel.kt @@ -10,7 +10,7 @@ import com.tangem.domain.walletconnect.WcRequestUseCaseFactory import com.tangem.domain.walletconnect.usecase.method.WcSignState import com.tangem.domain.walletconnect.usecase.method.WcSignStep import com.tangem.domain.walletconnect.usecase.method.WcSignUseCase -import com.tangem.features.walletconnect.transaction.components.WcSignTransactionComponent +import com.tangem.features.walletconnect.transaction.components.WcSignTransactionContainerComponent import com.tangem.features.walletconnect.transaction.entity.WcSignTransactionUM import com.tangem.features.walletconnect.transaction.entity.WcTransactionActionsUM import com.tangem.features.walletconnect.transaction.utils.toUM @@ -35,7 +35,7 @@ internal class WcSignTransactionModel @Inject constructor( private val _uiState = MutableStateFlow(null) val uiState: StateFlow = _uiState - private val params = paramsContainer.require() + private val params = paramsContainer.require() init { modelScope.launch { @@ -47,10 +47,8 @@ internal class WcSignTransactionModel @Inject constructor( signState = signState, actions = WcTransactionActionsUM( onDismiss = { cancel(useCase) }, - onBack = ::showTransactionState, onSign = useCase::sign, onCopy = { copyData(useCase.rawSdkRequest.request.params) }, - transactionRequestOnClick = ::showTransactionRequestState, ), ) _uiState.emit(signTransactionUM) @@ -59,6 +57,10 @@ internal class WcSignTransactionModel @Inject constructor( } } + fun dismiss() { + _uiState.value?.actions?.onDismiss?.invoke() ?: router.pop() + } + private fun signingIsDone(signState: WcSignState<*>): Boolean { (signState.domainStep as? WcSignStep.Result)?.result?.let { router.pop() @@ -75,12 +77,4 @@ internal class WcSignTransactionModel @Inject constructor( private fun copyData(text: String) { clipboardManager.setText(text = text, isSensitive = true) } - - private fun showTransactionRequestState() { - _uiState.value = _uiState.value?.copy(state = WcSignTransactionUM.State.TRANSACTION_REQUEST_INFO) - } - - private fun showTransactionState() { - _uiState.value = _uiState.value?.copy(state = WcSignTransactionUM.State.TRANSACTION) - } } \ No newline at end of file diff --git a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/routes/WcSignTransactionRoutes.kt b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/routes/WcSignTransactionRoutes.kt new file mode 100644 index 0000000000..6c5c1d9fa8 --- /dev/null +++ b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/routes/WcSignTransactionRoutes.kt @@ -0,0 +1,15 @@ +package com.tangem.features.walletconnect.transaction.routes + +import androidx.compose.runtime.Immutable +import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfigContent +import kotlinx.serialization.Serializable + +@Serializable +@Immutable +internal sealed class WcSignTransactionRoutes : TangemBottomSheetConfigContent { + @Serializable + data object Transaction : WcSignTransactionRoutes() + + @Serializable + data object TransactionRequestInfo : WcSignTransactionRoutes() +} \ No newline at end of file diff --git a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/ui/common/TransactionRequestInfoContent.kt b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/ui/common/TransactionRequestInfoContent.kt index d7c61d3af8..382da28e98 100644 --- a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/ui/common/TransactionRequestInfoContent.kt +++ b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/ui/common/TransactionRequestInfoContent.kt @@ -1,53 +1,94 @@ package com.tangem.features.walletconnect.transaction.ui.common +import android.content.res.Configuration import androidx.compose.foundation.background import androidx.compose.foundation.layout.* +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip +import androidx.compose.ui.platform.LocalConfiguration +import androidx.compose.ui.tooling.preview.Devices +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.tooling.preview.PreviewParameter +import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider import androidx.compose.ui.unit.dp import com.tangem.core.ui.components.SecondaryButtonIconEnd +import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig +import com.tangem.core.ui.components.bottomsheets.modal.TangemModalBottomSheet +import com.tangem.core.ui.components.bottomsheets.modal.TangemModalBottomSheetTitle import com.tangem.core.ui.extensions.resolveReference +import com.tangem.core.ui.extensions.resourceReference import com.tangem.core.ui.extensions.stringResourceSafe import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.res.TangemThemePreview import com.tangem.features.walletconnect.impl.R +import com.tangem.features.walletconnect.transaction.entity.* +import com.tangem.features.walletconnect.transaction.entity.WcNetworkInfoUM import com.tangem.features.walletconnect.transaction.entity.WcSignTransactionUM +import com.tangem.features.walletconnect.transaction.entity.WcTransactionActionsUM +import com.tangem.features.walletconnect.transaction.entity.WcTransactionRequestInfoUM +import com.tangem.features.walletconnect.transaction.entity.WcTransactionUM +import kotlinx.collections.immutable.persistentListOf + +private const val MIN_HEIGHT_SCREEN_PERCENT = 0.35f +private const val MAX_HEIGHT_SCREEN_PERCENT = 0.75f @Composable internal fun TransactionRequestInfoContent(state: WcSignTransactionUM) { + val screenHeight = LocalConfiguration.current.screenHeightDp + val minHeight = (screenHeight * MIN_HEIGHT_SCREEN_PERCENT).dp + val maxHeight = (screenHeight * MAX_HEIGHT_SCREEN_PERCENT).dp + Box( modifier = Modifier .fillMaxWidth() - .heightIn(min = 310.dp) + .heightIn(min = minHeight, max = maxHeight) .padding(horizontal = TangemTheme.dimens.spacing16), ) { - Column( + LazyColumn( modifier = Modifier - .fillMaxWidth() - .clip(RoundedCornerShape(TangemTheme.dimens.radius16)) - .background(TangemTheme.colors.background.action) - .padding( - start = TangemTheme.dimens.spacing16, - end = TangemTheme.dimens.spacing16, - bottom = TangemTheme.dimens.spacing20, - ), + .fillMaxWidth(), + contentPadding = PaddingValues(top = TangemTheme.dimens.spacing8, bottom = TangemTheme.dimens.spacing70), ) { - state.transactionRequestInfo.info.forEach { item -> - Text( - text = item.title.resolveReference(), - modifier = Modifier.padding(top = TangemTheme.dimens.spacing20), - style = TangemTheme.typography.subtitle2, - color = TangemTheme.colors.text.tertiary, - ) - Text( - text = item.description, - modifier = Modifier.padding(top = TangemTheme.dimens.spacing4), - style = TangemTheme.typography.body2, - color = TangemTheme.colors.text.primary1, - ) + items(state.transactionRequestInfo.blocks) { block -> + Column( + modifier = Modifier + .fillMaxWidth() + .clip(RoundedCornerShape(TangemTheme.dimens.radius16)) + .background(TangemTheme.colors.background.action) + .padding( + start = TangemTheme.dimens.spacing16, + end = TangemTheme.dimens.spacing16, + top = TangemTheme.dimens.spacing20, + ), + ) { + block.info.forEach { item -> + Text( + text = item.title.resolveReference(), + style = TangemTheme.typography.subtitle2, + color = TangemTheme.colors.text.tertiary, + ) + if (item.description.isNotEmpty()) { + Text( + text = item.description, + modifier = Modifier.padding( + top = TangemTheme.dimens.spacing4, + bottom = TangemTheme.dimens.spacing20, + ), + style = TangemTheme.typography.body2, + color = TangemTheme.colors.text.primary1, + ) + } else { + Spacer(modifier = Modifier.size(TangemTheme.dimens.size12)) + } + } + } + Spacer(modifier = Modifier.size(TangemTheme.dimens.size20)) } } @@ -61,4 +102,118 @@ internal fun TransactionRequestInfoContent(state: WcSignTransactionUM) { iconResId = R.drawable.ic_copy_24, ) } -} \ No newline at end of file +} + +@Composable +@Preview(showBackground = true, device = Devices.PIXEL_7_PRO) +@Preview(showBackground = true, device = Devices.PIXEL_7_PRO, uiMode = Configuration.UI_MODE_NIGHT_YES) +private fun WcSignTransactionRequestInfoBottomSheetPreview( + @PreviewParameter(WcSignTransactionStateProvider::class) state: WcSignTransactionUM, +) { + TangemThemePreview { + TangemModalBottomSheet( + config = TangemBottomSheetConfig( + isShown = true, + onDismissRequest = {}, + content = state, + ), + containerColor = TangemTheme.colors.background.tertiary, + title = { + TangemModalBottomSheetTitle( + title = resourceReference(R.string.wc_transaction_request_title), + endIconRes = null, + onEndClick = {}, + startIconRes = R.drawable.ic_back_24, + onStartClick = {}, + ) + }, + content = { + TransactionRequestInfoContent(state = state) + }, + ) + } +} + +private class WcSignTransactionStateProvider : CollectionPreviewParameterProvider( + listOf( + WcSignTransactionUM( + transaction = WcTransactionUM( + appName = "React App", + appIcon = "", + isVerified = true, + appSubtitle = "react-app.walletconnect.com", + walletName = "Tangem 2.0", + networkInfo = WcNetworkInfoUM(name = "Ethereum", iconRes = R.drawable.img_eth_22), + ), + transactionRequestInfo = WcTransactionRequestInfoUM( + blocks = persistentListOf( + WcTransactionRequestBlockUM( + persistentListOf( + WcTransactionRequestInfoItemUM( + title = resourceReference(R.string.wc_signature_type), + description = "personal_sign", + ), + WcTransactionRequestInfoItemUM( + title = resourceReference(R.string.wc_contents), + description = "Hello! My name is John Dow. test@tange.com", + ), + ), + ), + ), + ), + actions = WcTransactionActionsUM( + onDismiss = {}, + onSign = {}, + onCopy = {}, + ), + ), + WcSignTransactionUM( + transaction = WcTransactionUM( + appName = "React App", + appIcon = "", + isVerified = true, + appSubtitle = "react-app.walletconnect.com", + walletName = "Tangem 2.0", + networkInfo = WcNetworkInfoUM(name = "Ethereum", iconRes = R.drawable.img_eth_22), + addressText = "0x345FF...34FA", + networkFee = "~ 0.22 $", + ), + transactionRequestInfo = WcTransactionRequestInfoUM( + blocks = persistentListOf( + WcTransactionRequestBlockUM( + persistentListOf( + WcTransactionRequestInfoItemUM( + title = resourceReference(R.string.wc_signature_type), + description = "personal_sign", + ), + WcTransactionRequestInfoItemUM( + title = resourceReference(R.string.wc_contents), + description = "Hello! My name is John Dow. test@tange.com", + ), + ), + ), + WcTransactionRequestBlockUM( + persistentListOf( + WcTransactionRequestInfoItemUM( + title = resourceReference(R.string.wc_transaction_info_to_title), + ), + WcTransactionRequestInfoItemUM( + title = resourceReference(R.string.settings_wallet_name_title), + description = "Bob", + ), + WcTransactionRequestInfoItemUM( + title = resourceReference(R.string.wc_common_wallet), + description = "0xCD2a3d9F938E13CD947Ec05AbC7FE734Df8DD826", + ), + ), + ), + ), + ), + actions = WcTransactionActionsUM( + onDismiss = {}, + onSign = {}, + onCopy = {}, + ), + ), + ), +) \ No newline at end of file diff --git a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/ui/common/WcAddressItem.kt b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/ui/common/WcAddressItem.kt new file mode 100644 index 0000000000..8e9febc9b2 --- /dev/null +++ b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/ui/common/WcAddressItem.kt @@ -0,0 +1,44 @@ +package com.tangem.features.walletconnect.transaction.ui.common + +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.material3.Icon +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.text.style.TextAlign +import com.tangem.core.ui.extensions.stringResourceSafe +import com.tangem.core.ui.res.TangemTheme +import com.tangem.features.walletconnect.impl.R + +@Composable +internal fun WcAddressItem(addressText: String, modifier: Modifier = Modifier) { + Row(modifier = modifier, verticalAlignment = Alignment.CenterVertically) { + Icon( + modifier = Modifier.size(TangemTheme.dimens.size24), + painter = painterResource(R.drawable.ic_user_square_24), + contentDescription = null, + tint = TangemTheme.colors.icon.accent, + ) + Text( + modifier = Modifier + .padding(start = TangemTheme.dimens.spacing8) + .weight(1f), + text = stringResourceSafe(R.string.wc_common_address), + style = TangemTheme.typography.body1, + color = TangemTheme.colors.text.primary1, + ) + Text( + modifier = Modifier + .padding(start = TangemTheme.dimens.spacing16) + .weight(1f), + text = addressText, + textAlign = TextAlign.End, + style = TangemTheme.typography.body1, + color = TangemTheme.colors.text.tertiary, + ) + } +} \ No newline at end of file diff --git a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/ui/common/WcNetworkFeeItem.kt b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/ui/common/WcNetworkFeeItem.kt new file mode 100644 index 0000000000..bb4c1867c5 --- /dev/null +++ b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/ui/common/WcNetworkFeeItem.kt @@ -0,0 +1,67 @@ +package com.tangem.features.walletconnect.transaction.ui.common + +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.material3.Icon +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.res.painterResource +import com.tangem.core.ui.extensions.stringResourceSafe +import com.tangem.core.ui.res.TangemTheme +import com.tangem.features.walletconnect.impl.R + +@Composable +internal fun WcNetworkFeeItem(networkFeeText: String, modifier: Modifier = Modifier) { + Row( + modifier = modifier, + verticalAlignment = Alignment.CenterVertically, + ) { + Icon( + modifier = Modifier.size(TangemTheme.dimens.size24), + painter = painterResource(R.drawable.ic_fee_new_24), + contentDescription = null, + tint = TangemTheme.colors.icon.accent, + ) + + Text( + modifier = Modifier.padding(start = TangemTheme.dimens.spacing8), + text = stringResourceSafe(R.string.common_network_fee_title), + style = TangemTheme.typography.body1, + color = TangemTheme.colors.text.primary1, + ) + + Icon( + modifier = Modifier + .padding(start = TangemTheme.dimens.spacing6) + .size(TangemTheme.dimens.size16), + painter = painterResource(id = R.drawable.ic_information_24), + tint = TangemTheme.colors.icon.informative, + contentDescription = null, + ) + + Spacer(modifier = Modifier.weight(1f)) + + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.End, + ) { + Text( + text = networkFeeText, + style = TangemTheme.typography.body1, + color = TangemTheme.colors.text.tertiary, + ) + Spacer(modifier = Modifier.width(TangemTheme.dimens.spacing6)) + Icon( + modifier = Modifier + .clip(CircleShape) + .size(width = TangemTheme.dimens.size18, height = TangemTheme.dimens.size24), + painter = painterResource(id = R.drawable.ic_select_18_24), + tint = TangemTheme.colors.icon.informative, + contentDescription = null, + ) + } + } +} \ No newline at end of file diff --git a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/ui/common/WcNetworkItem.kt b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/ui/common/WcNetworkItem.kt index 1f337688aa..58c62d0366 100644 --- a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/ui/common/WcNetworkItem.kt +++ b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/ui/common/WcNetworkItem.kt @@ -30,7 +30,7 @@ internal fun WcNetworkItem(networkInfo: WcNetworkInfoUM, modifier: Modifier = Mo ) Text( modifier = Modifier - .padding(start = TangemTheme.dimens.spacing4) + .padding(start = TangemTheme.dimens.spacing8) .weight(1f), text = stringResourceSafe(R.string.wc_common_network), style = TangemTheme.typography.body1, diff --git a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/ui/common/WcWalletItem.kt b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/ui/common/WcWalletItem.kt index 63633a22c1..18babacc67 100644 --- a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/ui/common/WcWalletItem.kt +++ b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/ui/common/WcWalletItem.kt @@ -25,7 +25,7 @@ internal fun WcWalletItem(walletName: String, modifier: Modifier = Modifier) { ) Text( modifier = Modifier - .padding(start = TangemTheme.dimens.spacing4) + .padding(start = TangemTheme.dimens.spacing8) .weight(1f), text = stringResourceSafe(R.string.manage_tokens_network_selector_wallet), style = TangemTheme.typography.body1, diff --git a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/ui/sign/WcSignTransactionModalBottomSheet.kt b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/ui/sign/WcSignTransactionModalBottomSheet.kt deleted file mode 100644 index 665eb497b3..0000000000 --- a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/ui/sign/WcSignTransactionModalBottomSheet.kt +++ /dev/null @@ -1,154 +0,0 @@ -package com.tangem.features.walletconnect.transaction.ui.sign - -import android.content.res.Configuration -import androidx.compose.animation.animateContentSize -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.runtime.Composable -import androidx.compose.ui.Modifier -import androidx.compose.ui.tooling.preview.Devices -import androidx.compose.ui.tooling.preview.Preview -import androidx.compose.ui.tooling.preview.PreviewParameter -import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider -import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig -import com.tangem.core.ui.components.bottomsheets.modal.TangemModalBottomSheet -import com.tangem.core.ui.components.bottomsheets.modal.TangemModalBottomSheetTitle -import com.tangem.core.ui.extensions.resourceReference -import com.tangem.core.ui.res.TangemTheme -import com.tangem.core.ui.res.TangemThemePreview -import com.tangem.features.walletconnect.impl.R -import com.tangem.features.walletconnect.transaction.entity.* -import com.tangem.features.walletconnect.transaction.ui.common.TransactionRequestInfoContent -import kotlinx.collections.immutable.persistentListOf - -@Composable -internal fun WcSignTransactionModalBottomSheet(config: TangemBottomSheetConfig) { - TangemModalBottomSheet( - config = config, - containerColor = TangemTheme.colors.background.tertiary, - title = { state -> - TangemModalBottomSheetTitle( - title = when (state.state) { - WcSignTransactionUM.State.TRANSACTION -> { - resourceReference(R.string.wallet_connect_title) - } - WcSignTransactionUM.State.TRANSACTION_REQUEST_INFO -> { - resourceReference(R.string.wc_transaction_request_title) - } - }, - endIconRes = state.endIconRes.takeIf { - state.state == WcSignTransactionUM.State.TRANSACTION - }, - onEndClick = state.actions.onDismiss, - startIconRes = state.startIconRes.takeIf { - state.state == WcSignTransactionUM.State.TRANSACTION_REQUEST_INFO - }, - onStartClick = state.actions.onBack, - ) - }, - content = { state -> - Box( - modifier = Modifier - .fillMaxWidth() - .animateContentSize(), - ) { - when (state.state) { - WcSignTransactionUM.State.TRANSACTION -> { - WcSignTransactionModalBottomSheetContent(state) - } - WcSignTransactionUM.State.TRANSACTION_REQUEST_INFO -> { - TransactionRequestInfoContent(state) - } - } - } - }, - ) -} - -@Composable -@Preview(showBackground = true, device = Devices.PIXEL_7_PRO) -@Preview(showBackground = true, device = Devices.PIXEL_7_PRO, uiMode = Configuration.UI_MODE_NIGHT_YES) -private fun WcSignTransactionBottomSheetPreview( - @PreviewParameter(WcSignTransactionStateProvider::class) state: WcSignTransactionUM, -) { - TangemThemePreview { - WcSignTransactionModalBottomSheet( - config = TangemBottomSheetConfig( - isShown = true, - onDismissRequest = {}, - content = state, - ), - ) - } -} - -private class WcSignTransactionStateProvider : CollectionPreviewParameterProvider( - listOf( - WcSignTransactionUM( - startIconRes = R.drawable.ic_back_24, - endIconRes = R.drawable.ic_close_24, - transactionIconRes = R.drawable.ic_doc_new_24, - state = WcSignTransactionUM.State.TRANSACTION, - transaction = WcTransactionUM( - appName = "React App", - appIcon = "", - isVerified = true, - appSubtitle = "react-app.walletconnect.com", - walletName = "Tangem 2.0", - networkInfo = WcNetworkInfoUM(name = "Ethereum", iconRes = R.drawable.img_eth_22), - ), - transactionRequestInfo = WcTransactionRequestInfoUM( - persistentListOf( - WcTransactionRequestInfoItemUM( - title = resourceReference(R.string.wc_signature_type), - description = "personal_sign", - ), - WcTransactionRequestInfoItemUM( - title = resourceReference(R.string.wc_contents), - description = "Hello! My name is John Dow. test@tange.com", - ), - ), - ), - actions = WcTransactionActionsUM( - onDismiss = {}, - onBack = {}, - onSign = {}, - onCopy = {}, - transactionRequestOnClick = {}, - ), - ), - WcSignTransactionUM( - startIconRes = R.drawable.ic_back_24, - endIconRes = R.drawable.ic_close_24, - transactionIconRes = R.drawable.ic_doc_new_24, - state = WcSignTransactionUM.State.TRANSACTION_REQUEST_INFO, - transaction = WcTransactionUM( - appName = "React App", - appIcon = "", - isVerified = true, - appSubtitle = "react-app.walletconnect.com", - walletName = "Tangem 2.0", - networkInfo = WcNetworkInfoUM(name = "Ethereum", iconRes = R.drawable.img_eth_22), - ), - transactionRequestInfo = WcTransactionRequestInfoUM( - persistentListOf( - WcTransactionRequestInfoItemUM( - title = resourceReference(R.string.wc_signature_type), - description = "personal_sign", - ), - WcTransactionRequestInfoItemUM( - title = resourceReference(R.string.wc_contents), - description = "Hello! My name is John Dow. test@tange.com", - ), - ), - ), - actions = WcTransactionActionsUM( - onDismiss = {}, - onBack = {}, - onSign = {}, - onCopy = {}, - transactionRequestOnClick = {}, - ), - ), - ), -) \ No newline at end of file diff --git a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/ui/sign/WcSignTransactionModalBottomSheetContent.kt b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/ui/sign/WcSignTransactionModalBottomSheetContent.kt index 7548486b13..398d71924e 100644 --- a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/ui/sign/WcSignTransactionModalBottomSheetContent.kt +++ b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/ui/sign/WcSignTransactionModalBottomSheetContent.kt @@ -1,5 +1,6 @@ package com.tangem.features.walletconnect.transaction.ui.sign +import android.content.res.Configuration import androidx.compose.animation.animateContentSize import androidx.compose.foundation.background import androidx.compose.foundation.clickable @@ -11,18 +12,33 @@ import androidx.compose.material3.HorizontalDivider import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip +import androidx.compose.ui.tooling.preview.Devices +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.tooling.preview.PreviewParameter +import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp +import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig +import com.tangem.core.ui.components.bottomsheets.modal.TangemModalBottomSheet +import com.tangem.core.ui.components.bottomsheets.modal.TangemModalBottomSheetTitle +import com.tangem.core.ui.extensions.resourceReference import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.res.TangemThemePreview import com.tangem.features.walletconnect.connections.ui.WcAppInfoItem -import com.tangem.features.walletconnect.transaction.entity.WcSignTransactionUM +import com.tangem.features.walletconnect.impl.R +import com.tangem.features.walletconnect.transaction.entity.* import com.tangem.features.walletconnect.transaction.ui.common.* -import com.tangem.features.walletconnect.transaction.ui.common.WcNetworkItem -import com.tangem.features.walletconnect.transaction.ui.common.WcTransactionRequestButtons -import com.tangem.features.walletconnect.transaction.ui.common.WcTransactionRequestItem -import com.tangem.features.walletconnect.transaction.ui.common.WcWalletItem +import kotlinx.collections.immutable.persistentListOf @Composable -internal fun WcSignTransactionModalBottomSheetContent(state: WcSignTransactionUM) { - Column(modifier = Modifier.padding(horizontal = TangemTheme.dimens.spacing16)) { +internal fun WcSignTransactionModalBottomSheetContent( + state: WcSignTransactionUM, + onClickTransactionRequest: () -> Unit, +) { + Column( + modifier = Modifier + .padding(horizontal = TangemTheme.dimens.spacing16), + ) { Column( modifier = Modifier .clip(RoundedCornerShape(TangemTheme.dimens.radius14)) @@ -37,47 +53,17 @@ internal fun WcSignTransactionModalBottomSheetContent(state: WcSignTransactionUM subtitle = state.transaction.appSubtitle, isVerified = state.transaction.isVerified, ) - HorizontalDivider( - thickness = TangemTheme.dimens.size1, - color = TangemTheme.colors.stroke.primary, - ) + DividerWithPadding(start = 0.dp, end = 0.dp) WcTransactionRequestItem( - iconRes = state.transactionIconRes, + iconRes = R.drawable.ic_doc_new_24, modifier = Modifier .fillMaxWidth() - .clickable { state.actions.transactionRequestOnClick() } + .clickable { onClickTransactionRequest() } .padding(TangemTheme.dimens.spacing12), ) } Column(modifier = Modifier.padding(top = TangemTheme.dimens.spacing16)) { - Column( - modifier = Modifier - .clip(RoundedCornerShape(TangemTheme.dimens.radius14)) - .background(color = TangemTheme.colors.background.action) - .fillMaxWidth() - .animateContentSize(), - ) { - val itemsModifier = Modifier - .fillMaxWidth() - .padding(TangemTheme.dimens.spacing12) - - HorizontalDivider( - thickness = TangemTheme.dimens.size1, - color = TangemTheme.colors.stroke.primary, - ) - WcWalletItem( - modifier = itemsModifier, - walletName = state.transaction.walletName, - ) - HorizontalDivider( - thickness = TangemTheme.dimens.size1, - color = TangemTheme.colors.stroke.primary, - ) - WcNetworkItem( - modifier = itemsModifier, - networkInfo = state.transaction.networkInfo, - ) - } + WcSignTransactionItems(state) WcTransactionRequestButtons( modifier = Modifier.padding(vertical = TangemTheme.dimens.spacing16), onDismiss = state.actions.onDismiss, @@ -86,4 +72,125 @@ internal fun WcSignTransactionModalBottomSheetContent(state: WcSignTransactionUM ) } } -} \ No newline at end of file +} + +@Composable +private fun WcSignTransactionItems(state: WcSignTransactionUM) { + Column( + modifier = Modifier + .clip(RoundedCornerShape(TangemTheme.dimens.radius14)) + .background(color = TangemTheme.colors.background.action) + .fillMaxWidth() + .animateContentSize(), + ) { + val itemsModifier = Modifier + .fillMaxWidth() + .padding(TangemTheme.dimens.spacing12) + + DividerWithPadding(start = 0.dp, end = 0.dp) + WcWalletItem( + modifier = itemsModifier, + walletName = state.transaction.walletName, + ) + DividerWithPadding(start = TangemTheme.dimens.spacing40, end = TangemTheme.dimens.spacing12) + WcNetworkItem( + modifier = itemsModifier, + networkInfo = state.transaction.networkInfo, + ) + if (!state.transaction.addressText.isNullOrEmpty()) { + DividerWithPadding(start = TangemTheme.dimens.spacing40, end = TangemTheme.dimens.spacing12) + WcAddressItem( + modifier = itemsModifier, + addressText = state.transaction.addressText, + ) + } + if (!state.transaction.networkFee.isNullOrEmpty()) { + DividerWithPadding(start = TangemTheme.dimens.spacing40, end = TangemTheme.dimens.spacing12) + WcNetworkFeeItem( + modifier = itemsModifier, + networkFeeText = state.transaction.networkFee, + ) + } + } +} + +@Composable +private fun DividerWithPadding(start: Dp, end: Dp) { + HorizontalDivider( + modifier = Modifier.padding( + start = start, + end = end, + ), + thickness = TangemTheme.dimens.size1, + color = TangemTheme.colors.stroke.primary, + ) +} + +@Composable +@Preview(showBackground = true, device = Devices.PIXEL_7_PRO) +@Preview(showBackground = true, device = Devices.PIXEL_7_PRO, uiMode = Configuration.UI_MODE_NIGHT_YES) +private fun WcSignTransactionBottomSheetPreview( + @PreviewParameter(WcSignTransactionStateProvider::class) state: WcSignTransactionUM, +) { + TangemThemePreview { + TangemModalBottomSheet( + config = TangemBottomSheetConfig( + isShown = true, + onDismissRequest = {}, + content = state, + ), + containerColor = TangemTheme.colors.background.tertiary, + title = { + TangemModalBottomSheetTitle( + title = resourceReference(R.string.wallet_connect_title), + endIconRes = R.drawable.ic_close_24, + onEndClick = {}, + startIconRes = null, + onStartClick = {}, + ) + }, + content = { + WcSignTransactionModalBottomSheetContent(state = state, onClickTransactionRequest = {}) + }, + ) + } +} + +private class WcSignTransactionStateProvider : CollectionPreviewParameterProvider( + listOf( + WcSignTransactionUM( + transaction = WcTransactionUM( + appName = "React App", + appIcon = "", + isVerified = true, + appSubtitle = "react-app.walletconnect.com", + walletName = "Tangem 2.0", + networkInfo = WcNetworkInfoUM(name = "Ethereum", iconRes = R.drawable.img_eth_22), + ), + transactionRequestInfo = WcTransactionRequestInfoUM(persistentListOf()), + actions = WcTransactionActionsUM( + onDismiss = {}, + onSign = {}, + onCopy = {}, + ), + ), + WcSignTransactionUM( + transaction = WcTransactionUM( + appName = "React App", + appIcon = "", + isVerified = true, + appSubtitle = "react-app.walletconnect.com", + walletName = "Tangem 2.0", + networkInfo = WcNetworkInfoUM(name = "Ethereum", iconRes = R.drawable.img_eth_22), + addressText = "0x345FF...34FA", + networkFee = "~ 0.22 $", + ), + transactionRequestInfo = WcTransactionRequestInfoUM(persistentListOf()), + actions = WcTransactionActionsUM( + onDismiss = {}, + onSign = {}, + onCopy = {}, + ), + ), + ), +) \ No newline at end of file diff --git a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/utils/WalletAddressUtils.kt b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/utils/WalletAddressUtils.kt new file mode 100644 index 0000000000..a51a5d82f7 --- /dev/null +++ b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/utils/WalletAddressUtils.kt @@ -0,0 +1,7 @@ +package com.tangem.features.walletconnect.transaction.utils + +const val ADDRESS_FIRST_PART_LENGTH = 7 +const val ADDRESS_SECOND_PART_LENGTH = 4 + +internal fun String.toShortAddressText() = + "${take(ADDRESS_FIRST_PART_LENGTH)}...${takeLast(ADDRESS_SECOND_PART_LENGTH)}" \ No newline at end of file diff --git a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/utils/WcSignTransactionUtils.kt b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/utils/WcSignTransactionUtils.kt index 5eba6dcc9f..0442435782 100644 --- a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/utils/WcSignTransactionUtils.kt +++ b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/utils/WcSignTransactionUtils.kt @@ -3,6 +3,9 @@ package com.tangem.features.walletconnect.transaction.utils import com.domain.blockaid.models.dapp.CheckDAppResult import com.tangem.core.ui.extensions.getActiveIconRes import com.tangem.core.ui.extensions.resourceReference +import com.tangem.domain.walletconnect.model.WcEthMethod +import com.tangem.domain.walletconnect.model.WcSolanaMethod +import com.tangem.domain.walletconnect.model.sdkcopy.WcSdkSessionRequest import com.tangem.domain.walletconnect.usecase.method.WcMessageSignUseCase import com.tangem.domain.walletconnect.usecase.method.WcSignState import com.tangem.domain.walletconnect.usecase.method.WcSignStep @@ -11,33 +14,84 @@ import com.tangem.features.walletconnect.impl.R import com.tangem.features.walletconnect.transaction.entity.WcNetworkInfoUM import com.tangem.features.walletconnect.transaction.entity.WcSignTransactionUM import com.tangem.features.walletconnect.transaction.entity.WcTransactionActionsUM +import com.tangem.features.walletconnect.transaction.entity.WcTransactionRequestBlockUM import com.tangem.features.walletconnect.transaction.entity.WcTransactionRequestInfoItemUM import com.tangem.features.walletconnect.transaction.entity.WcTransactionRequestInfoUM import com.tangem.features.walletconnect.transaction.entity.WcTransactionUM import kotlinx.collections.immutable.persistentListOf +import kotlinx.collections.immutable.toImmutableList internal fun WcSignUseCase.toUM(signState: WcSignState<*>, actions: WcTransactionActionsUM): WcSignTransactionUM? { return when (this) { is WcMessageSignUseCase -> { - ethMessageSignToUM( - signState = signState, - signModel = signState.signModel as WcMessageSignUseCase.SignModel, - actions = actions, - ) + when (method) { + is WcEthMethod.SignTypedData -> signTypedDataToUM( + signState = signState, + signModel = signState.signModel as WcMessageSignUseCase.SignModel, + actions = actions, + ) + is WcEthMethod.MessageSign, is WcSolanaMethod.SignMessage -> messageSignToUM( + signState = signState, + signModel = signState.signModel as WcMessageSignUseCase.SignModel, + actions = actions, + ) + else -> null + } } else -> null } } -private fun WcMessageSignUseCase.ethMessageSignToUM( +private fun WcMessageSignUseCase.signTypedDataToUM( + signState: WcSignState<*>, + signModel: WcMessageSignUseCase.SignModel, + actions: WcTransactionActionsUM, +) = WcSignTransactionUM( + actions = actions, + transaction = WcTransactionUM( + appName = session.sdkModel.appMetaData.name, + appIcon = session.sdkModel.appMetaData.url, + isVerified = session.securityStatus == CheckDAppResult.SAFE, + appSubtitle = session.sdkModel.appMetaData.description, + walletName = session.wallet.name, + networkInfo = WcNetworkInfoUM( + name = network.name, + iconRes = getActiveIconRes(network.id.value), + ), + addressText = walletAddress.toShortAddressText(), + isLoading = signState.domainStep == WcSignStep.Signing, + ), + transactionRequestInfo = WcTransactionRequestInfoUM( + buildList { + add(createInfoBlockUM(rawSdkRequest, signModel)) + (method as? WcEthMethod.SignTypedData)?.params?.message?.to?.let { to -> + add( + WcTransactionRequestBlockUM( + persistentListOf( + WcTransactionRequestInfoItemUM( + title = resourceReference(R.string.wc_transaction_info_to_title), + ), + WcTransactionRequestInfoItemUM( + title = resourceReference(R.string.settings_wallet_name_title), + description = to.name, + ), + WcTransactionRequestInfoItemUM( + title = resourceReference(R.string.wc_common_wallet), + description = to.wallet, + ), + ), + ), + ) + } + }.toImmutableList(), + ), +) + +private fun WcMessageSignUseCase.messageSignToUM( signState: WcSignState<*>, signModel: WcMessageSignUseCase.SignModel, actions: WcTransactionActionsUM, ) = WcSignTransactionUM( - startIconRes = R.drawable.ic_back_24, - endIconRes = R.drawable.ic_close_24, - transactionIconRes = R.drawable.ic_doc_new_24, - state = WcSignTransactionUM.State.TRANSACTION, actions = actions, transaction = WcTransactionUM( appName = session.sdkModel.appMetaData.name, @@ -52,6 +106,15 @@ private fun WcMessageSignUseCase.ethMessageSignToUM( isLoading = signState.domainStep == WcSignStep.Signing, ), transactionRequestInfo = WcTransactionRequestInfoUM( + persistentListOf(createInfoBlockUM(rawSdkRequest, signModel)), + ), +) + +private fun createInfoBlockUM( + rawSdkRequest: WcSdkSessionRequest, + signModel: WcMessageSignUseCase.SignModel, +): WcTransactionRequestBlockUM { + return WcTransactionRequestBlockUM( persistentListOf( WcTransactionRequestInfoItemUM( title = resourceReference(R.string.wc_signature_type), @@ -62,5 +125,5 @@ private fun WcMessageSignUseCase.ethMessageSignToUM( description = signModel.humanMsg, ), ), - ), -) \ No newline at end of file + ) +} \ No newline at end of file From 9cda183a5b868fd4321b6e006e569570f8c55993 Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 30 Apr 2025 19:53:58 +0500 Subject: [PATCH 010/165] Updated on 2026-08-14 --- .../tap/di/domain/TokensDomainModule.kt | 24 +--- .../GetCryptoCurrencyStatusSyncUseCase.kt | 30 ----- .../tokens/GetCurrencyStatusUpdatesUseCase.kt | 62 ---------- ...=> GetMultiCryptoCurrencyStatusUseCase.kt} | 12 +- .../GetPrimaryCurrencyStatusUpdatesUseCase.kt | 42 ------- .../GetSingleCryptoCurrencyStatusUseCase.kt | 106 ++++++++++++++++++ .../collections/model/NFTCollectionsModel.kt | 4 - .../topup/model/OnboardingNoteTopUpModel.kt | 11 +- .../v2/twin/impl/model/OnboardingTwinModel.kt | 28 ++--- .../onramp/hottokens/model/HotCryptoModel.kt | 9 +- .../features/send/v2/send/model/SendModel.kt | 10 +- .../send/v2/sendnft/model/NFTSendModel.kt | 6 +- .../send/impl/presentation/model/SendModel.kt | 6 +- .../impl/presentation/model/StakingModel.kt | 4 +- .../feature/swap/domain/SwapInteractorImpl.kt | 6 +- .../swap/domain/di/SwapDomainModule.kt | 6 +- .../tangem/feature/swap/model/SwapModel.kt | 18 ++- .../tokendetails/model/TokenDetailsModel.kt | 7 +- .../txhistory/model/TxHistoryModel.kt | 6 +- .../intents/WalletContentClickIntents.kt | 6 +- .../WalletCurrencyActionsClickIntents.kt | 6 +- .../domain/GetSingleWalletWarningsFactory.kt | 6 +- .../presentation/wallet/domain/UseCaseExt.kt | 10 +- .../implementors/SingleWalletContentLoader.kt | 12 +- .../SingleWalletContentLoaderFactory.kt | 6 +- .../subscribers/PrimaryCurrencySubscriber.kt | 6 +- .../SingleWalletButtonsSubscriber.kt | 6 +- .../SingleWalletExpressStatusesSubscriber.kt | 6 +- .../wallet/subscribers/TxHistorySubscriber.kt | 6 +- 29 files changed, 216 insertions(+), 251 deletions(-) delete mode 100644 domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetCryptoCurrencyStatusSyncUseCase.kt delete mode 100644 domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetCurrencyStatusUpdatesUseCase.kt rename domain/tokens/src/main/kotlin/com/tangem/domain/tokens/{GetCryptoCurrencyStatusesSyncUseCase.kt => GetMultiCryptoCurrencyStatusUseCase.kt} (50%) delete mode 100644 domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetPrimaryCurrencyStatusUpdatesUseCase.kt create mode 100644 domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetSingleCryptoCurrencyStatusUseCase.kt diff --git a/app/src/main/java/com/tangem/tap/di/domain/TokensDomainModule.kt b/app/src/main/java/com/tangem/tap/di/domain/TokensDomainModule.kt index 5b56c4dc11..d6e0a771ad 100644 --- a/app/src/main/java/com/tangem/tap/di/domain/TokensDomainModule.kt +++ b/app/src/main/java/com/tangem/tap/di/domain/TokensDomainModule.kt @@ -117,8 +117,8 @@ internal object TokensDomainModule { fun provideGetCurrencyUseCase( baseCurrencyStatusOperations: BaseCurrencyStatusOperations, dispatchers: CoroutineDispatcherProvider, - ): GetCurrencyStatusUpdatesUseCase { - return GetCurrencyStatusUpdatesUseCase( + ): GetSingleCryptoCurrencyStatusUseCase { + return GetSingleCryptoCurrencyStatusUseCase( currencyStatusOperations = baseCurrencyStatusOperations, dispatchers = dispatchers, ) @@ -158,18 +158,6 @@ internal object TokensDomainModule { ) } - @Provides - @Singleton - fun provideGetPrimaryCurrencyUseCase( - currencyStatusOperations: BaseCurrencyStatusOperations, - dispatchers: CoroutineDispatcherProvider, - ): GetPrimaryCurrencyStatusUpdatesUseCase { - return GetPrimaryCurrencyStatusUpdatesUseCase( - currencyStatusOperations = currencyStatusOperations, - dispatchers = dispatchers, - ) - } - @Provides @Singleton fun provideFetchCurrencyStatusUseCase( @@ -214,14 +202,6 @@ internal object TokensDomainModule { ) } - @Provides - @Singleton - fun providesGetCryptoCurrencyStatusSyncUseCase( - currencyStatusOperations: BaseCurrencyStatusOperations, - ): GetCryptoCurrencyStatusSyncUseCase { - return GetCryptoCurrencyStatusSyncUseCase(currencyStatusOperations) - } - @Provides @Singleton fun provideGetCryptoCurrencyUseCase(currenciesRepository: CurrenciesRepository): GetCryptoCurrencyUseCase { diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetCryptoCurrencyStatusSyncUseCase.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetCryptoCurrencyStatusSyncUseCase.kt deleted file mode 100644 index 20354b45c4..0000000000 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetCryptoCurrencyStatusSyncUseCase.kt +++ /dev/null @@ -1,30 +0,0 @@ -package com.tangem.domain.tokens - -import arrow.core.Either -import com.tangem.domain.tokens.error.CurrencyStatusError -import com.tangem.domain.tokens.error.mapper.mapToCurrencyError -import com.tangem.domain.tokens.model.CryptoCurrency -import com.tangem.domain.tokens.model.CryptoCurrencyStatus -import com.tangem.domain.tokens.operations.BaseCurrencyStatusOperations -import com.tangem.domain.wallets.models.UserWalletId - -class GetCryptoCurrencyStatusSyncUseCase( - private val currencyStatusOperations: BaseCurrencyStatusOperations, -) { - - // multi-currency - suspend operator fun invoke( - userWalletId: UserWalletId, - cryptoCurrencyId: CryptoCurrency.ID, - isSingleWalletWithTokens: Boolean = false, - ): Either { - return currencyStatusOperations.getCurrencyStatusSync(userWalletId, cryptoCurrencyId, isSingleWalletWithTokens) - .mapLeft { error -> error.mapToCurrencyError() } - } - - // single-currency - suspend operator fun invoke(userWalletId: UserWalletId): Either { - return currencyStatusOperations.getPrimaryCurrencyStatusSync(userWalletId) - .mapLeft { error -> error.mapToCurrencyError() } - } -} \ No newline at end of file diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetCurrencyStatusUpdatesUseCase.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetCurrencyStatusUpdatesUseCase.kt deleted file mode 100644 index 0595b091fe..0000000000 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetCurrencyStatusUpdatesUseCase.kt +++ /dev/null @@ -1,62 +0,0 @@ -package com.tangem.domain.tokens - -import arrow.core.Either -import com.tangem.domain.tokens.error.CurrencyStatusError -import com.tangem.domain.tokens.error.mapper.mapToCurrencyError -import com.tangem.domain.tokens.model.CryptoCurrency -import com.tangem.domain.tokens.model.CryptoCurrencyStatus -import com.tangem.domain.tokens.operations.BaseCurrencyStatusOperations -import com.tangem.domain.tokens.operations.CurrenciesStatusesOperations -import com.tangem.domain.wallets.models.UserWalletId -import com.tangem.utils.coroutines.CoroutineDispatcherProvider -import kotlinx.coroutines.flow.* - -/** - * Use case for fetching the status of a cryptocurrency associated with a user wallet. - * - */ -class GetCurrencyStatusUpdatesUseCase( - private val currencyStatusOperations: BaseCurrencyStatusOperations, - private val dispatchers: CoroutineDispatcherProvider, -) { - - /** - * Invokes the use case. - * - * @param userWalletId The unique identifier of the user's wallet. - * @param currencyId The unique identifier of the cryptocurrency. - * @param isSingleWalletWithTokens Indicates whether the user wallet contains only one token on card (old cards) - * @return A [Flow] emitting either a [CurrencyStatusError] or a [CryptoCurrencyStatus], indicating the result of the fetch operation. - */ - operator fun invoke( - userWalletId: UserWalletId, - currencyId: CryptoCurrency.ID, - isSingleWalletWithTokens: Boolean, - ): Flow> { - return flow { - emitAll( - getCurrencyStatus( - userWalletId = userWalletId, - currencyId = currencyId, - isSingleWalletWithTokens = isSingleWalletWithTokens, - ), - ) - }.flowOn(dispatchers.io) - } - - private suspend fun getCurrencyStatus( - userWalletId: UserWalletId, - currencyId: CryptoCurrency.ID, - isSingleWalletWithTokens: Boolean, - ): Flow> { - val currencyFlow = currencyStatusOperations.getCurrencyStatusFlow( - userWalletId = userWalletId, - currencyId = currencyId, - isSingleWalletWithTokens = isSingleWalletWithTokens, - ) - - return currencyFlow.map { maybeCurrency -> - maybeCurrency.mapLeft(CurrenciesStatusesOperations.Error::mapToCurrencyError) - } - } -} \ No newline at end of file diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetCryptoCurrencyStatusesSyncUseCase.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetMultiCryptoCurrencyStatusUseCase.kt similarity index 50% rename from domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetCryptoCurrencyStatusesSyncUseCase.kt rename to domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetMultiCryptoCurrencyStatusUseCase.kt index 590cb6caa0..56b50913c1 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetCryptoCurrencyStatusesSyncUseCase.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetMultiCryptoCurrencyStatusUseCase.kt @@ -1,17 +1,25 @@ package com.tangem.domain.tokens import arrow.core.Either +import com.tangem.domain.tokens.error.CurrencyStatusError import com.tangem.domain.tokens.error.TokenListError import com.tangem.domain.tokens.error.mapper.mapToTokenListError import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.domain.tokens.operations.BaseCurrencyStatusOperations import com.tangem.domain.wallets.models.UserWalletId +import kotlinx.coroutines.flow.Flow -class GetCryptoCurrencyStatusesSyncUseCase( +class GetMultiCryptoCurrencyStatusUseCase( private val currencyStatusOperations: BaseCurrencyStatusOperations, ) { - suspend operator fun invoke(userWalletId: UserWalletId): Either> { + /** + * Returns synchronously list of cryptocurrency statuses for Multi-Currency wallet + * + * @param userWalletId The unique identifier of the user's wallet. + * @return A [Flow] emitting either a [CurrencyStatusError] or a [CryptoCurrencyStatus], indicating the result of the fetch operation. + */ + suspend fun invokeMultiWalletSync(userWalletId: UserWalletId): Either> { return currencyStatusOperations.getCurrenciesStatusesSync(userWalletId) .mapLeft { error -> error.mapToTokenListError() } } diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetPrimaryCurrencyStatusUpdatesUseCase.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetPrimaryCurrencyStatusUpdatesUseCase.kt deleted file mode 100644 index 000411bff6..0000000000 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetPrimaryCurrencyStatusUpdatesUseCase.kt +++ /dev/null @@ -1,42 +0,0 @@ -package com.tangem.domain.tokens - -import arrow.core.Either -import com.tangem.domain.tokens.error.CurrencyStatusError -import com.tangem.domain.tokens.error.mapper.mapToCurrencyError -import com.tangem.domain.tokens.model.CryptoCurrencyStatus -import com.tangem.domain.tokens.operations.BaseCurrencyStatusOperations -import com.tangem.domain.tokens.operations.CurrenciesStatusesOperations -import com.tangem.domain.wallets.models.UserWalletId -import com.tangem.utils.coroutines.CoroutineDispatcherProvider -import kotlinx.coroutines.flow.* - -/** - * Use case for fetching the status of the primary cryptocurrency associated with a user wallet. - * - * @property dispatchers Provides coroutine dispatchers. - */ -class GetPrimaryCurrencyStatusUpdatesUseCase( - private val dispatchers: CoroutineDispatcherProvider, - private val currencyStatusOperations: BaseCurrencyStatusOperations, -) { - - /** - * Invokes the use case. - * - * @param userWalletId The unique identifier of the user's wallet. - * @return A [Flow] emitting either a [CurrencyStatusError] or a [CryptoCurrencyStatus], indicating the result of the fetch operation. - */ - operator fun invoke(userWalletId: UserWalletId): Flow> { - return flow { - emitAll(getPrimaryCurrency(userWalletId)) - }.flowOn(dispatchers.io) - } - - private suspend fun getPrimaryCurrency( - userWalletId: UserWalletId, - ): Flow> { - return currencyStatusOperations.getPrimaryCurrencyStatusFlow(userWalletId).map { maybeCurrency -> - maybeCurrency.mapLeft(CurrenciesStatusesOperations.Error::mapToCurrencyError) - } - } -} \ No newline at end of file diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetSingleCryptoCurrencyStatusUseCase.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetSingleCryptoCurrencyStatusUseCase.kt new file mode 100644 index 0000000000..928221f31b --- /dev/null +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetSingleCryptoCurrencyStatusUseCase.kt @@ -0,0 +1,106 @@ +package com.tangem.domain.tokens + +import arrow.core.Either +import com.tangem.domain.tokens.error.CurrencyStatusError +import com.tangem.domain.tokens.error.mapper.mapToCurrencyError +import com.tangem.domain.tokens.model.CryptoCurrency +import com.tangem.domain.tokens.model.CryptoCurrencyStatus +import com.tangem.domain.tokens.operations.BaseCurrencyStatusOperations +import com.tangem.domain.tokens.operations.CurrenciesStatusesOperations +import com.tangem.domain.wallets.models.UserWalletId +import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import kotlinx.coroutines.flow.* + +/** + * Use case for fetching the status of a cryptocurrency associated with a user wallet. + * + */ +class GetSingleCryptoCurrencyStatusUseCase( + private val currencyStatusOperations: BaseCurrencyStatusOperations, + private val dispatchers: CoroutineDispatcherProvider, +) { + + /** + * Returns cryptocurrency status flow for Multi-Currency wallet + * + * @param userWalletId The unique identifier of the user's wallet. + * @param currencyId The unique identifier of the cryptocurrency. + * @param isSingleWalletWithTokens Indicates whether the user wallet contains only one token on card (old cards) + * @return A [Flow] emitting either a [CurrencyStatusError] or a [CryptoCurrencyStatus], indicating the result of the fetch operation. + */ + fun invokeMultiWallet( + userWalletId: UserWalletId, + currencyId: CryptoCurrency.ID, + isSingleWalletWithTokens: Boolean, + ): Flow> { + return flow { + emitAll( + getCurrencyStatus( + userWalletId = userWalletId, + currencyId = currencyId, + isSingleWalletWithTokens = isSingleWalletWithTokens, + ), + ) + }.flowOn(dispatchers.io) + } + + /** + * Returns cryptocurrency status flow for primary currency for Single-Currency wallet + * + * @param userWalletId The unique identifier of the user's wallet. + * @return A [Flow] emitting either a [CurrencyStatusError] or a [CryptoCurrencyStatus], indicating the result of the fetch operation. + */ + fun invokeSingleWallet(userWalletId: UserWalletId): Flow> { + return flow { + emitAll( + currencyStatusOperations.getPrimaryCurrencyStatusFlow(userWalletId).map { maybeCurrency -> + maybeCurrency.mapLeft(CurrenciesStatusesOperations.Error::mapToCurrencyError) + }, + ) + }.flowOn(dispatchers.io) + } + + /** + * Returns synchronously cryptocurrency status for Multi-Currency wallet + * + * @param userWalletId The unique identifier of the user's wallet. + * @param cryptoCurrencyId The unique identifier of the cryptocurrency. + * @param isSingleWalletWithTokens Indicates whether the user wallet contains only one token on card (old cards) + * @return A [Flow] emitting either a [CurrencyStatusError] or a [CryptoCurrencyStatus], indicating the result of the fetch operation. + */ + suspend fun invokeMultiWalletSync( + userWalletId: UserWalletId, + cryptoCurrencyId: CryptoCurrency.ID, + isSingleWalletWithTokens: Boolean = false, + ): Either { + return currencyStatusOperations.getCurrencyStatusSync(userWalletId, cryptoCurrencyId, isSingleWalletWithTokens) + .mapLeft { error -> error.mapToCurrencyError() } + } + + /** + * Returns synchronously cryptocurrency status for primary currency for Single-Currency wallet + * + * @param userWalletId The unique identifier of the user's wallet. + * @return A [Flow] emitting either a [CurrencyStatusError] or a [CryptoCurrencyStatus], indicating the result of the fetch operation. + */ + suspend fun invokeSingleWalletSync(userWalletId: UserWalletId): Either { + return currencyStatusOperations.getPrimaryCurrencyStatusSync(userWalletId) + .mapLeft { error -> error.mapToCurrencyError() } + } + + private suspend fun getCurrencyStatus( + userWalletId: UserWalletId, + currencyId: CryptoCurrency.ID, + isSingleWalletWithTokens: Boolean, + ): Flow> { + val currencyFlow = currencyStatusOperations.getCurrencyStatusFlow( + userWalletId = userWalletId, + currencyId = currencyId, + isSingleWalletWithTokens = isSingleWalletWithTokens, + ) + + return currencyFlow.map { maybeCurrency -> + maybeCurrency.mapLeft(CurrenciesStatusesOperations.Error::mapToCurrencyError) + } + } +} \ No newline at end of file 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 72b200907a..1b83ed39e4 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 @@ -15,10 +15,6 @@ import com.tangem.features.nft.collections.NFTCollectionsComponent import com.tangem.features.nft.collections.entity.NFTCollectionsStateUM import com.tangem.features.nft.collections.entity.NFTCollectionsUM import com.tangem.features.nft.collections.entity.transformer.* -import com.tangem.features.nft.collections.entity.transformer.ChangeCollectionExpandedStateTransformer -import com.tangem.features.nft.collections.entity.transformer.ToggleSearchBarTransformer -import com.tangem.features.nft.collections.entity.transformer.UpdateDataStateTransformer -import com.tangem.features.nft.collections.entity.transformer.UpdateSearchQueryTransformer import com.tangem.features.nft.impl.R import com.tangem.utils.coroutines.CoroutineDispatcherProvider import kotlinx.coroutines.flow.* diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/note/impl/child/topup/model/OnboardingNoteTopUpModel.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/note/impl/child/topup/model/OnboardingNoteTopUpModel.kt index bdac9d90ca..7330ae8a68 100644 --- a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/note/impl/child/topup/model/OnboardingNoteTopUpModel.kt +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/note/impl/child/topup/model/OnboardingNoteTopUpModel.kt @@ -16,7 +16,7 @@ import com.tangem.domain.exchange.RampStateManager import com.tangem.domain.models.scan.ScanResponse import com.tangem.domain.onramp.GetLegacyTopUpUrlUseCase import com.tangem.domain.tokens.FetchCurrencyStatusUseCase -import com.tangem.domain.tokens.GetPrimaryCurrencyStatusUpdatesUseCase +import com.tangem.domain.tokens.GetSingleCryptoCurrencyStatusUseCase import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.domain.tokens.model.Network import com.tangem.domain.tokens.model.NetworkAddress @@ -39,7 +39,7 @@ import javax.inject.Inject internal class OnboardingNoteTopUpModel @Inject constructor( paramsContainer: ParamsContainer, override val dispatchers: CoroutineDispatcherProvider, - private val getPrimaryCurrencyStatusUpdatesUseCase: GetPrimaryCurrencyStatusUpdatesUseCase, + private val getSingleCryptoCurrencyStatusUseCase: GetSingleCryptoCurrencyStatusUseCase, private val fetchCurrencyStatusUseCase: FetchCurrencyStatusUseCase, private val userWalletBuilderFactory: UserWalletBuilder.Factory, private val getLegacyTopUpUrlUseCase: GetLegacyTopUpUrlUseCase, @@ -137,8 +137,11 @@ internal class OnboardingNoteTopUpModel @Inject constructor( private fun observeCryptoCurrencyStatus() { val userWalletId = userWallet?.walletId ?: return - getPrimaryCurrencyStatusUpdatesUseCase(userWalletId = userWalletId).map { it.getOrNull() }.filterNotNull() - .onEach(::applyCryptoCurrencyStatusToState).launchIn(modelScope) + getSingleCryptoCurrencyStatusUseCase.invokeSingleWallet(userWalletId = userWalletId) + .map { it.getOrNull() } + .filterNotNull() + .onEach(::applyCryptoCurrencyStatusToState) + .launchIn(modelScope) } private fun applyCryptoCurrencyStatusToState(status: CryptoCurrencyStatus) { diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/twin/impl/model/OnboardingTwinModel.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/twin/impl/model/OnboardingTwinModel.kt index 1219a5763f..a36e14fec8 100644 --- a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/twin/impl/model/OnboardingTwinModel.kt +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/twin/impl/model/OnboardingTwinModel.kt @@ -7,18 +7,18 @@ import com.tangem.common.core.TangemError import com.tangem.common.core.TangemSdkError import com.tangem.common.extensions.hexToBytes import com.tangem.common.extensions.toHexString +import com.tangem.common.ui.bottomsheet.receive.TokenReceiveBottomSheetConfig +import com.tangem.core.analytics.Analytics import com.tangem.core.analytics.api.AnalyticsEventHandler import com.tangem.core.analytics.models.AnalyticsParam import com.tangem.core.decompose.di.ModelScoped import com.tangem.core.decompose.model.Model import com.tangem.core.decompose.model.ParamsContainer import com.tangem.core.decompose.ui.UiMessageSender -import com.tangem.core.navigation.url.UrlOpener -import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig -import com.tangem.common.ui.bottomsheet.receive.TokenReceiveBottomSheetConfig -import com.tangem.core.analytics.Analytics import com.tangem.core.navigation.share.ShareManager +import com.tangem.core.navigation.url.UrlOpener import com.tangem.core.ui.clipboard.ClipboardManager +import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig import com.tangem.core.ui.extensions.resourceReference import com.tangem.core.ui.extensions.toWrappedList import com.tangem.core.ui.format.bigdecimal.crypto @@ -34,7 +34,7 @@ import com.tangem.domain.models.scan.ScanResponse import com.tangem.domain.onboarding.SaveTwinsOnboardingShownUseCase import com.tangem.domain.onramp.GetLegacyTopUpUrlUseCase import com.tangem.domain.tokens.FetchCurrencyStatusUseCase -import com.tangem.domain.tokens.GetPrimaryCurrencyStatusUpdatesUseCase +import com.tangem.domain.tokens.GetSingleCryptoCurrencyStatusUseCase import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.domain.tokens.model.Network import com.tangem.domain.tokens.model.analytics.TokenReceiveAnalyticsEvent @@ -77,7 +77,7 @@ internal class OnboardingTwinModel @Inject constructor( private val tangemSdkManager: TangemSdkManager, private val issuersConfigStorage: IssuersConfigStorage, private val cardRepository: CardRepository, - private val getPrimaryCurrencyStatusUpdatesUseCase: GetPrimaryCurrencyStatusUpdatesUseCase, + private val getSingleCryptoCurrencyStatusUseCase: GetSingleCryptoCurrencyStatusUseCase, private val fetchCurrencyStatusUseCase: FetchCurrencyStatusUseCase, private val getLegacyTopUpUrlUseCase: GetLegacyTopUpUrlUseCase, private val urlOpener: UrlOpener, @@ -337,16 +337,16 @@ internal class OnboardingTwinModel @Inject constructor( setLoading(false) } - val cryptoCurrencyStatus = - getPrimaryCurrencyStatusUpdatesUseCase.invoke(userWallet.walletId).firstOrNull()?.getOrNull() - ?: run { - setLoading(false) - Timber.e("Unable to get currency status") - return@coroutineScope - } + val cryptoCurrencyStatus = getSingleCryptoCurrencyStatusUseCase.invokeSingleWallet(userWallet.walletId) + .firstOrNull()?.getOrNull() + ?: run { + setLoading(false) + Timber.e("Unable to get currency status") + return@coroutineScope + } launch { - getPrimaryCurrencyStatusUpdatesUseCase.invoke(userWallet.walletId) + getSingleCryptoCurrencyStatusUseCase.invokeSingleWallet(userWallet.walletId) .collect { it.onRight { status -> applyCryptoCurrencyStatusToState(status) diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/hottokens/model/HotCryptoModel.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/hottokens/model/HotCryptoModel.kt index be622c09d1..cb7ccfc863 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/hottokens/model/HotCryptoModel.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/hottokens/model/HotCryptoModel.kt @@ -14,7 +14,7 @@ import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.onramp.GetHotCryptoUseCase import com.tangem.domain.onramp.model.HotCryptoCurrency -import com.tangem.domain.tokens.GetCryptoCurrencyStatusSyncUseCase +import com.tangem.domain.tokens.GetSingleCryptoCurrencyStatusUseCase import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.features.onramp.hottokens.HotCryptoComponent import com.tangem.features.onramp.hottokens.converter.HotTokenItemStateConverter @@ -45,7 +45,7 @@ internal class HotCryptoModel @Inject constructor( paramsContainer: ParamsContainer, getHotCryptoUseCase: GetHotCryptoUseCase, getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase, - private val getCryptoCurrencyStatusSyncUseCase: GetCryptoCurrencyStatusSyncUseCase, + private val getSingleCryptoCurrencyStatusUseCase: GetSingleCryptoCurrencyStatusUseCase, override val dispatchers: CoroutineDispatcherProvider, ) : Model() { @@ -103,7 +103,10 @@ internal class HotCryptoModel @Inject constructor( private fun onSuccessAdding(id: CryptoCurrency.ID) { modelScope.launch { - getCryptoCurrencyStatusSyncUseCase(userWalletId = params.userWalletId, cryptoCurrencyId = id) + getSingleCryptoCurrencyStatusUseCase.invokeMultiWalletSync( + userWalletId = params.userWalletId, + cryptoCurrencyId = id, + ) .onRight { bottomSheetNavigation.dismiss() params.onTokenClick(it) 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 250e790576..3d162a7bc2 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 @@ -23,9 +23,8 @@ import com.tangem.domain.feedback.models.FeedbackEmailType import com.tangem.domain.qrscanning.models.SourceType import com.tangem.domain.qrscanning.usecases.ListenToQrScanningUseCase import com.tangem.domain.qrscanning.usecases.ParseQrCodeUseCase -import com.tangem.domain.tokens.GetCurrencyStatusUpdatesUseCase +import com.tangem.domain.tokens.GetSingleCryptoCurrencyStatusUseCase import com.tangem.domain.tokens.GetFeePaidCryptoCurrencyStatusSyncUseCase -import com.tangem.domain.tokens.GetPrimaryCurrencyStatusUpdatesUseCase import com.tangem.domain.tokens.error.CurrencyStatusError import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.domain.transaction.error.GetFeeError @@ -72,8 +71,7 @@ internal class SendModel @Inject constructor( override val dispatchers: CoroutineDispatcherProvider, private val router: Router, private val getUserWalletUseCase: GetUserWalletUseCase, - private val getCurrencyStatusUpdatesUseCase: GetCurrencyStatusUpdatesUseCase, - private val getPrimaryCurrencyStatusUpdatesUseCase: GetPrimaryCurrencyStatusUpdatesUseCase, + private val getSingleCryptoCurrencyStatusUseCase: GetSingleCryptoCurrencyStatusUseCase, private val getFeePaidCryptoCurrencyStatusSyncUseCase: GetFeePaidCryptoCurrencyStatusSyncUseCase, private val getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase, private val listenToQrScanningUseCase: ListenToQrScanningUseCase, @@ -244,13 +242,13 @@ internal class SendModel @Inject constructor( isMultiCurrency: Boolean, ): Flow> { return if (isMultiCurrency) { - getCurrencyStatusUpdatesUseCase( + getSingleCryptoCurrencyStatusUseCase.invokeMultiWallet( userWalletId = userWalletId, currencyId = cryptoCurrency.id, isSingleWalletWithTokens = isSingleWalletWithToken, ) } else { - getPrimaryCurrencyStatusUpdatesUseCase(userWalletId = userWalletId) + getSingleCryptoCurrencyStatusUseCase.invokeSingleWallet(userWalletId = userWalletId) } } 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 4e496e2810..ecd6c008dd 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 @@ -19,7 +19,7 @@ import com.tangem.domain.feedback.SendFeedbackEmailUseCase import com.tangem.domain.feedback.models.BlockchainErrorInfo import com.tangem.domain.feedback.models.FeedbackEmailType import com.tangem.domain.tokens.GetCryptoCurrenciesUseCase -import com.tangem.domain.tokens.GetCurrencyStatusUpdatesUseCase +import com.tangem.domain.tokens.GetSingleCryptoCurrencyStatusUseCase import com.tangem.domain.tokens.GetFeePaidCryptoCurrencyStatusSyncUseCase import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.domain.tokens.model.CryptoCurrencyStatus @@ -60,7 +60,7 @@ internal class NFTSendModel @Inject constructor( private val getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase, private val getUserWalletUseCase: GetUserWalletUseCase, private val getCryptoCurrenciesUseCase: GetCryptoCurrenciesUseCase, - private val getCurrencyStatusUpdatesUseCase: GetCurrencyStatusUpdatesUseCase, + private val getSingleCryptoCurrencyStatusUseCase: GetSingleCryptoCurrencyStatusUseCase, private val getFeePaidCryptoCurrencyStatusSyncUseCase: GetFeePaidCryptoCurrencyStatusSyncUseCase, private val createNFTTransferTransactionUseCase: CreateNFTTransferTransactionUseCase, private val getFeeUseCase: GetFeeUseCase, @@ -183,7 +183,7 @@ internal class NFTSendModel @Inject constructor( } private fun getCurrenciesStatusUpdates(isSingleWalletWithToken: Boolean) { - getCurrencyStatusUpdatesUseCase( + getSingleCryptoCurrencyStatusUseCase.invokeMultiWallet( userWalletId = userWalletId, currencyId = cryptoCurrency.id, isSingleWalletWithTokens = isSingleWalletWithToken, diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/model/SendModel.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/model/SendModel.kt index 1aadbe4b88..02f0478ef5 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/model/SendModel.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/model/SendModel.kt @@ -84,7 +84,7 @@ import kotlin.properties.Delegates internal class SendModel @Inject constructor( override val dispatchers: CoroutineDispatcherProvider, private val getUserWalletUseCase: GetUserWalletUseCase, - private val getCryptoCurrencyStatusSyncUseCase: GetCryptoCurrencyStatusSyncUseCase, + private val getSingleCryptoCurrencyStatusUseCase: GetSingleCryptoCurrencyStatusUseCase, private val getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase, private val getWalletsUseCase: GetWalletsUseCase, private val getFeePaidCryptoCurrencyStatusSyncUseCase: GetFeePaidCryptoCurrencyStatusSyncUseCase, @@ -325,13 +325,13 @@ internal class SendModel @Inject constructor( isMultiCurrency: Boolean, ): Either { return if (isMultiCurrency) { - getCryptoCurrencyStatusSyncUseCase( + getSingleCryptoCurrencyStatusUseCase.invokeMultiWalletSync( userWalletId = userWalletId, cryptoCurrencyId = cryptoCurrency.id, isSingleWalletWithTokens = isSingleWalletWithToken, ) } else { - getCryptoCurrencyStatusSyncUseCase(userWalletId = userWalletId) + getSingleCryptoCurrencyStatusUseCase.invokeSingleWalletSync(userWalletId = userWalletId) } } 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 d122cd48bf..c8eb096676 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 @@ -93,7 +93,7 @@ internal class StakingModel @Inject constructor( private val stateController: StakingStateController, override val dispatchers: CoroutineDispatcherProvider, private val getBalanceHidingSettingsUseCase: GetBalanceHidingSettingsUseCase, - private val getCurrencyStatusUpdatesUseCase: GetCurrencyStatusUpdatesUseCase, + private val getSingleCryptoCurrencyStatusUseCase: GetSingleCryptoCurrencyStatusUseCase, private val getFeePaidCryptoCurrencyStatusSyncUseCase: GetFeePaidCryptoCurrencyStatusSyncUseCase, private val getMinimumTransactionAmountSyncUseCase: GetMinimumTransactionAmountSyncUseCase, private val getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase, @@ -920,7 +920,7 @@ internal class StakingModel @Inject constructor( ) }, ) - getCurrencyStatusUpdatesUseCase( + getSingleCryptoCurrencyStatusUseCase.invokeMultiWallet( userWalletId = userWalletId, currencyId = cryptoCurrencyId, isSingleWalletWithTokens = false, diff --git a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt index 6d119df85d..1e45b523de 100644 --- a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt +++ b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt @@ -18,7 +18,7 @@ import com.tangem.domain.appcurrency.repository.AppCurrencyRepository import com.tangem.domain.demo.IsDemoCardUseCase import com.tangem.domain.quotes.QuotesRepositoryV2 import com.tangem.domain.tokens.FetchCurrencyStatusUseCase -import com.tangem.domain.tokens.GetCryptoCurrencyStatusesSyncUseCase +import com.tangem.domain.tokens.GetMultiCryptoCurrencyStatusUseCase import com.tangem.domain.tokens.GetCurrencyCheckUseCase import com.tangem.domain.tokens.TokensFeatureToggles import com.tangem.domain.tokens.model.* @@ -54,7 +54,7 @@ internal class SwapInteractorImpl @AssistedInject constructor( private val userWalletManager: UserWalletManager, private val repository: SwapRepository, private val allowPermissionsHandler: AllowPermissionsHandler, - private val getMultiCryptoCurrencyStatusUseCase: GetCryptoCurrencyStatusesSyncUseCase, + private val getMultiCryptoCurrencyStatusUseCase: GetMultiCryptoCurrencyStatusUseCase, private val fetchCurrencyStatusUseCase: FetchCurrencyStatusUseCase, private val sendTransactionUseCase: SendTransactionUseCase, private val createTransactionUseCase: CreateTransactionUseCase, @@ -92,7 +92,7 @@ internal class SwapInteractorImpl @AssistedInject constructor( } override suspend fun getTokensDataState(currency: CryptoCurrency): TokensDataStateExpress { - val walletCurrencyStatuses = getMultiCryptoCurrencyStatusUseCase(userWalletId) + val walletCurrencyStatuses = getMultiCryptoCurrencyStatusUseCase.invokeMultiWalletSync(userWalletId) .getOrElse { emptyList() } val walletCurrencyStatusesExceptInitial = walletCurrencyStatuses diff --git a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/di/SwapDomainModule.kt b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/di/SwapDomainModule.kt index 610aa556a8..7a7e89069d 100644 --- a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/di/SwapDomainModule.kt +++ b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/di/SwapDomainModule.kt @@ -1,6 +1,6 @@ package com.tangem.feature.swap.domain.di -import com.tangem.domain.tokens.GetCryptoCurrencyStatusesSyncUseCase +import com.tangem.domain.tokens.GetMultiCryptoCurrencyStatusUseCase import com.tangem.domain.tokens.operations.BaseCurrencyStatusOperations import com.tangem.feature.swap.domain.* import dagger.Module @@ -28,8 +28,8 @@ internal class SwapDomainModule { @Singleton fun providesGetCryptoCurrencyStatusUseCase( currencyStatusOperations: BaseCurrencyStatusOperations, - ): GetCryptoCurrencyStatusesSyncUseCase { - return GetCryptoCurrencyStatusesSyncUseCase(currencyStatusOperations) + ): GetMultiCryptoCurrencyStatusUseCase { + return GetMultiCryptoCurrencyStatusUseCase(currencyStatusOperations) } @Provides 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 235b3002ef..2c54085052 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 @@ -34,7 +34,10 @@ import com.tangem.domain.promo.models.StoryContentIds import com.tangem.domain.settings.usercountry.GetUserCountryUseCase import com.tangem.domain.settings.usercountry.models.UserCountry import com.tangem.domain.settings.usercountry.models.needApplyFCARestrictions -import com.tangem.domain.tokens.* +import com.tangem.domain.tokens.GetFeePaidCryptoCurrencyStatusSyncUseCase +import com.tangem.domain.tokens.GetMinimumTransactionAmountSyncUseCase +import com.tangem.domain.tokens.GetSingleCryptoCurrencyStatusUseCase +import com.tangem.domain.tokens.UpdateDelayedNetworkStatusUseCase import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.domain.tokens.model.Network @@ -86,9 +89,8 @@ internal class SwapModel @Inject constructor( private val analyticsEventHandler: AnalyticsEventHandler, private val analyticsErrorEventHandler: AnalyticsErrorHandler, private val getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase, - private val getCryptoCurrencyStatusUseCase: GetCryptoCurrencyStatusSyncUseCase, private val updateDelayedCurrencyStatusUseCase: UpdateDelayedNetworkStatusUseCase, - private val getCurrencyStatusUpdatesUseCase: GetCurrencyStatusUpdatesUseCase, + private val getSingleCryptoCurrencyStatusUseCase: GetSingleCryptoCurrencyStatusUseCase, private val getFeePaidCryptoCurrencyStatusSyncUseCase: GetFeePaidCryptoCurrencyStatusSyncUseCase, private val getUserWalletUseCase: GetUserWalletUseCase, private val getCardInfoUseCase: GetCardInfoUseCase, @@ -176,8 +178,12 @@ internal class SwapModel @Inject constructor( } modelScope.launch(dispatchers.io) { - val fromStatus = getCryptoCurrencyStatusUseCase(userWalletId, initialCurrencyFrom.id).getOrNull() - val toStatus = initialCurrencyTo?.let { getCryptoCurrencyStatusUseCase(userWalletId, it.id).getOrNull() } + val fromStatus = + getSingleCryptoCurrencyStatusUseCase.invokeMultiWalletSync(userWalletId, initialCurrencyFrom.id) + .getOrNull() + val toStatus = initialCurrencyTo?.let { + getSingleCryptoCurrencyStatusUseCase.invokeMultiWalletSync(userWalletId, it.id).getOrNull() + } val wallet = getUserWalletUseCase(userWalletId).getOrNull() if (fromStatus == null || wallet == null) { @@ -861,7 +867,7 @@ internal class SwapModel @Inject constructor( ) { Timber.d("Subscribe to ${coin.id} balance updates") - getCurrencyStatusUpdatesUseCase( + getSingleCryptoCurrencyStatusUseCase.invokeMultiWallet( userWalletId = userWalletId, currencyId = coin.id, isSingleWalletWithTokens = false, 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 19f0df36c5..669086fb61 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 @@ -91,7 +91,7 @@ import javax.inject.Inject @ModelScoped internal class TokenDetailsModel @Inject constructor( override val dispatchers: CoroutineDispatcherProvider, - private val getCurrencyStatusUpdatesUseCase: GetCurrencyStatusUpdatesUseCase, + private val getSingleCryptoCurrencyStatusUseCase: GetSingleCryptoCurrencyStatusUseCase, private val getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase, private val fetchCurrencyStatusUseCase: FetchCurrencyStatusUseCase, private val txHistoryItemsCountUseCase: GetTxHistoryItemsCountUseCase, @@ -117,7 +117,6 @@ internal class TokenDetailsModel @Inject constructor( private val analyticsEventsHandler: AnalyticsEventHandler, private val vibratorHapticManager: VibratorHapticManager, private val clipboardManager: ClipboardManager, - private val getCryptoCurrencySyncUseCase: GetCryptoCurrencyStatusSyncUseCase, private val onrampFeatureToggles: OnrampFeatureToggles, private val shareManager: ShareManager, @GlobalUiMessageSender private val uiMessageSender: UiMessageSender, @@ -219,7 +218,7 @@ 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 = getCryptoCurrencySyncUseCase.invoke( + val currentCryptoCurrencyStatus = getSingleCryptoCurrencyStatusUseCase.invokeMultiWalletSync( userWalletId = userWalletId, cryptoCurrencyId = cryptoCurrency.id, isSingleWalletWithTokens = false, @@ -300,7 +299,7 @@ internal class TokenDetailsModel @Inject constructor( private fun subscribeOnCurrencyStatusUpdates() { modelScope.launch(dispatchers.main) { - getCurrencyStatusUpdatesUseCase( + getSingleCryptoCurrencyStatusUseCase.invokeMultiWallet( userWalletId = userWalletId, currencyId = cryptoCurrency.id, isSingleWalletWithTokens = userWallet.scanResponse.cardTypesResolver.isSingleWalletWithToken(), diff --git a/features/txhistory/impl/src/main/kotlin/com/tangem/features/txhistory/model/TxHistoryModel.kt b/features/txhistory/impl/src/main/kotlin/com/tangem/features/txhistory/model/TxHistoryModel.kt index fc7568fafc..13948922c0 100644 --- a/features/txhistory/impl/src/main/kotlin/com/tangem/features/txhistory/model/TxHistoryModel.kt +++ b/features/txhistory/impl/src/main/kotlin/com/tangem/features/txhistory/model/TxHistoryModel.kt @@ -8,7 +8,7 @@ import com.tangem.core.decompose.model.ParamsContainer import com.tangem.core.navigation.url.UrlOpener import com.tangem.domain.balancehiding.GetBalanceHidingSettingsUseCase import com.tangem.domain.common.util.cardTypesResolver -import com.tangem.domain.tokens.GetCurrencyStatusUpdatesUseCase +import com.tangem.domain.tokens.GetSingleCryptoCurrencyStatusUseCase import com.tangem.domain.tokens.error.CurrencyStatusError import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.domain.txhistory.models.TxHistoryStateError @@ -39,7 +39,7 @@ internal class TxHistoryModel @Inject constructor( override val dispatchers: CoroutineDispatcherProvider, private val getBalanceHidingSettingsUseCase: GetBalanceHidingSettingsUseCase, private val txHistoryItemsCountUseCase: GetTxHistoryItemsCountUseCase, - private val getCurrencyStatusUpdatesUseCase: GetCurrencyStatusUpdatesUseCase, + private val getSingleCryptoCurrencyStatusUseCase: GetSingleCryptoCurrencyStatusUseCase, private val getUserWalletUseCase: GetUserWalletUseCase, private val getExplorerTransactionUrlUseCase: GetExplorerTransactionUrlUseCase, private val urlOpener: UrlOpener, @@ -165,7 +165,7 @@ internal class TxHistoryModel @Inject constructor( val userWallet: UserWallet = requireNotNull(getUserWalletUseCase(params.userWalletId).getOrNull()) { "User wallet not found" } - getCurrencyStatusUpdatesUseCase( + getSingleCryptoCurrencyStatusUseCase.invokeMultiWallet( userWalletId = params.userWalletId, currencyId = params.currency.id, isSingleWalletWithTokens = userWallet.scanResponse.cardTypesResolver.isSingleWalletWithToken(), diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/intents/WalletContentClickIntents.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/intents/WalletContentClickIntents.kt index f9790aaa18..4695f54f1a 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/intents/WalletContentClickIntents.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/intents/WalletContentClickIntents.kt @@ -8,7 +8,7 @@ import com.tangem.domain.nft.analytics.NFTAnalyticsEvent import com.tangem.domain.redux.ReduxStateHolder import com.tangem.domain.settings.ShouldShowMarketsTooltipUseCase import com.tangem.domain.tokens.GetCryptoCurrencyActionsUseCase -import com.tangem.domain.tokens.GetPrimaryCurrencyStatusUpdatesUseCase +import com.tangem.domain.tokens.GetSingleCryptoCurrencyStatusUseCase import com.tangem.domain.tokens.TokensAction import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.domain.tokens.model.TokenActionsState @@ -67,7 +67,7 @@ internal class WalletContentClickIntentsImplementor @Inject constructor( private val walletWarningsClickIntents: WalletWarningsClickIntentsImplementor, private val onrampStatusFactory: OnrampStatusFactory, private val getUserWalletUseCase: GetUserWalletUseCase, - private val getPrimaryCurrencyStatusUpdatesUseCase: GetPrimaryCurrencyStatusUpdatesUseCase, + private val getSingleCryptoCurrencyStatusUseCase: GetSingleCryptoCurrencyStatusUseCase, private val getCryptoCurrencyActionsUseCase: GetCryptoCurrencyActionsUseCase, private val getExplorerTransactionUrlUseCase: GetExplorerTransactionUrlUseCase, private val shouldShowMarketsTooltipUseCase: ShouldShowMarketsTooltipUseCase, @@ -162,7 +162,7 @@ internal class WalletContentClickIntentsImplementor @Inject constructor( override fun onTransactionClick(txHash: String) { modelScope.launch(dispatchers.main) { - val currency = getPrimaryCurrencyStatusUpdatesUseCase.unwrap( + val currency = getSingleCryptoCurrencyStatusUseCase.unwrap( userWalletId = stateHolder.getSelectedWalletId(), ) ?.currency diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/intents/WalletCurrencyActionsClickIntents.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/intents/WalletCurrencyActionsClickIntents.kt index be96aa27a4..c4d18c7023 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/intents/WalletCurrencyActionsClickIntents.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/intents/WalletCurrencyActionsClickIntents.kt @@ -34,7 +34,7 @@ import com.tangem.domain.promo.GetStoryContentUseCase import com.tangem.domain.promo.models.StoryContentIds import com.tangem.domain.redux.ReduxStateHolder import com.tangem.domain.staking.model.stakekit.Yield -import com.tangem.domain.tokens.GetPrimaryCurrencyStatusUpdatesUseCase +import com.tangem.domain.tokens.GetSingleCryptoCurrencyStatusUseCase import com.tangem.domain.tokens.IsCryptoCurrencyCoinCouldHideUseCase import com.tangem.domain.tokens.RemoveCurrencyUseCase import com.tangem.domain.tokens.legacy.TradeCryptoAction @@ -110,7 +110,7 @@ internal class WalletCurrencyActionsClickIntentsImplementor @Inject constructor( private val getSelectedWalletSyncUseCase: GetSelectedWalletSyncUseCase, private val walletManagersFacade: WalletManagersFacade, private val isDemoCardUseCase: IsDemoCardUseCase, - private val getPrimaryCurrencyStatusUpdatesUseCase: GetPrimaryCurrencyStatusUpdatesUseCase, + private val getSingleCryptoCurrencyStatusUseCase: GetSingleCryptoCurrencyStatusUseCase, private val isCryptoCurrencyCoinCouldHide: IsCryptoCurrencyCoinCouldHideUseCase, private val removeCurrencyUseCase: RemoveCurrencyUseCase, private val getExploreUrlUseCase: GetExploreUrlUseCase, @@ -489,7 +489,7 @@ internal class WalletCurrencyActionsClickIntentsImplementor @Inject constructor( val userWalletId = stateHolder.getSelectedWalletId() modelScope.launch(dispatchers.main) { - val currencyStatus = getPrimaryCurrencyStatusUpdatesUseCase.unwrap(userWalletId) ?: return@launch + val currencyStatus = getSingleCryptoCurrencyStatusUseCase.unwrap(userWalletId) ?: return@launch when (val addresses = currencyStatus.value.networkAddress) { is NetworkAddress.Selectable -> { diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/GetSingleWalletWarningsFactory.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/GetSingleWalletWarningsFactory.kt index b59d4bd433..e8b3b2aacb 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/GetSingleWalletWarningsFactory.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/GetSingleWalletWarningsFactory.kt @@ -7,7 +7,7 @@ import com.tangem.domain.common.util.cardTypesResolver import com.tangem.domain.demo.IsDemoCardUseCase import com.tangem.domain.models.StatusSource import com.tangem.domain.settings.IsReadyToShowRateAppUseCase -import com.tangem.domain.tokens.GetPrimaryCurrencyStatusUpdatesUseCase +import com.tangem.domain.tokens.GetSingleCryptoCurrencyStatusUseCase import com.tangem.domain.tokens.error.CurrencyStatusError import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.domain.wallets.models.UserWallet @@ -22,7 +22,7 @@ import javax.inject.Inject @ModelScoped internal class GetSingleWalletWarningsFactory @Inject constructor( - private val getPrimaryCurrencyStatusUpdatesUseCase: GetPrimaryCurrencyStatusUpdatesUseCase, + private val getSingleCryptoCurrencyStatusUseCase: GetSingleCryptoCurrencyStatusUseCase, private val isDemoCardUseCase: IsDemoCardUseCase, private val isReadyToShowRateAppUseCase: IsReadyToShowRateAppUseCase, private val isNeedToBackupUseCase: IsNeedToBackupUseCase, @@ -36,7 +36,7 @@ internal class GetSingleWalletWarningsFactory @Inject constructor( val cardTypesResolver = userWallet.scanResponse.cardTypesResolver return combine( - flow = getPrimaryCurrencyStatusUpdatesUseCase(userWallet.walletId), + flow = getSingleCryptoCurrencyStatusUseCase.invokeSingleWallet(userWallet.walletId), flow2 = isReadyToShowRateAppUseCase().conflate(), flow3 = isNeedToBackupUseCase(userWallet.walletId).conflate(), flow4 = getWalletsUseCase().conflate(), diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/UseCaseExt.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/UseCaseExt.kt index 1ab1765968..d21c569563 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/UseCaseExt.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/UseCaseExt.kt @@ -1,7 +1,7 @@ package com.tangem.feature.wallet.presentation.wallet.domain import arrow.core.Either -import com.tangem.domain.tokens.GetPrimaryCurrencyStatusUpdatesUseCase +import com.tangem.domain.tokens.GetSingleCryptoCurrencyStatusUseCase import com.tangem.domain.tokens.error.CurrencyStatusError import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.domain.wallets.models.UserWallet @@ -20,8 +20,8 @@ internal fun GetSelectedWalletSyncUseCase.unwrap(): UserWallet? { ) } -internal suspend fun GetPrimaryCurrencyStatusUpdatesUseCase.unwrap(userWalletId: UserWalletId): CryptoCurrencyStatus? { - return this(userWalletId) +internal suspend fun GetSingleCryptoCurrencyStatusUseCase.unwrap(userWalletId: UserWalletId): CryptoCurrencyStatus? { + return invokeSingleWallet(userWalletId) .conflate() .distinctUntilChanged() .filter(Either::isRight) @@ -35,11 +35,11 @@ internal suspend fun GetPrimaryCurrencyStatusUpdatesUseCase.unwrap(userWalletId: ) } -internal suspend fun GetPrimaryCurrencyStatusUpdatesUseCase.collectLatest( +internal suspend fun GetSingleCryptoCurrencyStatusUseCase.collectLatest( userWalletId: UserWalletId, onRight: suspend (CryptoCurrencyStatus) -> Unit, ) { - this(userWalletId = userWalletId) + invokeSingleWallet(userWalletId = userWalletId) .conflate() .distinctUntilChanged() .collectLatest { maybeStatus -> diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/loaders/implementors/SingleWalletContentLoader.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/loaders/implementors/SingleWalletContentLoader.kt index e4167b9c20..81601a2e31 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/loaders/implementors/SingleWalletContentLoader.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/loaders/implementors/SingleWalletContentLoader.kt @@ -6,7 +6,7 @@ import com.tangem.domain.onramp.GetOnrampTransactionsUseCase import com.tangem.domain.onramp.OnrampRemoveTransactionUseCase import com.tangem.domain.settings.SetWalletWithFundsFoundUseCase import com.tangem.domain.tokens.GetCryptoCurrencyActionsUseCase -import com.tangem.domain.tokens.GetPrimaryCurrencyStatusUpdatesUseCase +import com.tangem.domain.tokens.GetSingleCryptoCurrencyStatusUseCase import com.tangem.domain.txhistory.usecase.GetTxHistoryItemsCountUseCase import com.tangem.domain.txhistory.usecase.GetTxHistoryItemsUseCase import com.tangem.domain.wallets.models.UserWallet @@ -29,7 +29,7 @@ internal class SingleWalletContentLoader( private val clickIntents: WalletClickIntents, private val isRefresh: Boolean, private val stateHolder: WalletStateController, - private val getPrimaryCurrencyStatusUpdatesUseCase: GetPrimaryCurrencyStatusUpdatesUseCase, + private val getSingleCryptoCurrencyStatusUseCase: GetSingleCryptoCurrencyStatusUseCase, private val getCryptoCurrencyActionsUseCase: GetCryptoCurrencyActionsUseCase, private val getSingleWalletWarningsFactory: GetSingleWalletWarningsFactory, private val setWalletWithFundsFoundUseCase: SetWalletWithFundsFoundUseCase, @@ -48,7 +48,7 @@ internal class SingleWalletContentLoader( PrimaryCurrencySubscriber( userWallet = userWallet, stateHolder = stateHolder, - getPrimaryCurrencyStatusUpdatesUseCase = getPrimaryCurrencyStatusUpdatesUseCase, + getSingleCryptoCurrencyStatusUseCase = getSingleCryptoCurrencyStatusUseCase, setWalletWithFundsFoundUseCase = setWalletWithFundsFoundUseCase, getSelectedAppCurrencyUseCase = getSelectedAppCurrencyUseCase, analyticsEventHandler = analyticsEventHandler, @@ -57,7 +57,7 @@ internal class SingleWalletContentLoader( userWallet = userWallet, stateHolder = stateHolder, clickIntents = clickIntents, - getPrimaryCurrencyStatusUpdatesUseCase = getPrimaryCurrencyStatusUpdatesUseCase, + getSingleCryptoCurrencyStatusUseCase = getSingleCryptoCurrencyStatusUseCase, getCryptoCurrencyActionsUseCase = getCryptoCurrencyActionsUseCase, ), SingleWalletNotificationsSubscriber( @@ -78,7 +78,7 @@ internal class SingleWalletContentLoader( clickIntents = clickIntents, getSelectedAppCurrencyUseCase = getSelectedAppCurrencyUseCase, analyticsEventHandler = analyticsEventHandler, - getPrimaryCurrencyStatusUpdatesUseCase = getPrimaryCurrencyStatusUpdatesUseCase, + getSingleCryptoCurrencyStatusUseCase = getSingleCryptoCurrencyStatusUseCase, getOnrampTransactionsUseCase = getOnrampTransactionsUseCase, onrampRemoveTransactionUseCase = onrampRemoveTransactionUseCase, ), @@ -87,7 +87,7 @@ internal class SingleWalletContentLoader( isRefresh = isRefresh, stateHolder = stateHolder, clickIntents = clickIntents, - getPrimaryCurrencyStatusUpdatesUseCase = getPrimaryCurrencyStatusUpdatesUseCase, + getSingleCryptoCurrencyStatusUseCase = getSingleCryptoCurrencyStatusUseCase, txHistoryItemsCountUseCase = txHistoryItemsCountUseCase, txHistoryItemsUseCase = txHistoryItemsUseCase, ), diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/loaders/implementors/SingleWalletContentLoaderFactory.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/loaders/implementors/SingleWalletContentLoaderFactory.kt index 6e8e164e92..22f8884acd 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/loaders/implementors/SingleWalletContentLoaderFactory.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/loaders/implementors/SingleWalletContentLoaderFactory.kt @@ -7,7 +7,7 @@ import com.tangem.domain.onramp.GetOnrampTransactionsUseCase import com.tangem.domain.onramp.OnrampRemoveTransactionUseCase import com.tangem.domain.settings.SetWalletWithFundsFoundUseCase import com.tangem.domain.tokens.GetCryptoCurrencyActionsUseCase -import com.tangem.domain.tokens.GetPrimaryCurrencyStatusUpdatesUseCase +import com.tangem.domain.tokens.GetSingleCryptoCurrencyStatusUseCase import com.tangem.domain.txhistory.usecase.GetTxHistoryItemsCountUseCase import com.tangem.domain.txhistory.usecase.GetTxHistoryItemsUseCase import com.tangem.domain.wallets.models.UserWallet @@ -22,7 +22,7 @@ import javax.inject.Inject @Suppress("LongParameterList") internal class SingleWalletContentLoaderFactory @Inject constructor( private val stateHolder: WalletStateController, - private val getPrimaryCurrencyStatusUpdatesUseCase: GetPrimaryCurrencyStatusUpdatesUseCase, + private val getSingleCryptoCurrencyStatusUseCase: GetSingleCryptoCurrencyStatusUseCase, private val getCryptoCurrencyActionsUseCase: GetCryptoCurrencyActionsUseCase, private val getSingleWalletWarningsFactory: GetSingleWalletWarningsFactory, private val setWalletWithFundsFoundUseCase: SetWalletWithFundsFoundUseCase, @@ -42,7 +42,7 @@ internal class SingleWalletContentLoaderFactory @Inject constructor( clickIntents = clickIntents, isRefresh = isRefresh, stateHolder = stateHolder, - getPrimaryCurrencyStatusUpdatesUseCase = getPrimaryCurrencyStatusUpdatesUseCase, + getSingleCryptoCurrencyStatusUseCase = getSingleCryptoCurrencyStatusUseCase, getCryptoCurrencyActionsUseCase = getCryptoCurrencyActionsUseCase, getSingleWalletWarningsFactory = getSingleWalletWarningsFactory, setWalletWithFundsFoundUseCase = setWalletWithFundsFoundUseCase, diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/PrimaryCurrencySubscriber.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/PrimaryCurrencySubscriber.kt index 855300bc2c..c5fd60f86a 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/PrimaryCurrencySubscriber.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/PrimaryCurrencySubscriber.kt @@ -8,7 +8,7 @@ import com.tangem.core.analytics.models.AnalyticsParam import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.settings.SetWalletWithFundsFoundUseCase -import com.tangem.domain.tokens.GetPrimaryCurrencyStatusUpdatesUseCase +import com.tangem.domain.tokens.GetSingleCryptoCurrencyStatusUseCase import com.tangem.domain.tokens.error.CurrencyStatusError import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.domain.wallets.models.UserWallet @@ -23,7 +23,7 @@ import java.math.BigDecimal internal class PrimaryCurrencySubscriber( private val userWallet: UserWallet, private val stateHolder: WalletStateController, - private val getPrimaryCurrencyStatusUpdatesUseCase: GetPrimaryCurrencyStatusUpdatesUseCase, + private val getSingleCryptoCurrencyStatusUseCase: GetSingleCryptoCurrencyStatusUseCase, private val setWalletWithFundsFoundUseCase: SetWalletWithFundsFoundUseCase, private val getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase, private val analyticsEventHandler: AnalyticsEventHandler, @@ -33,7 +33,7 @@ internal class PrimaryCurrencySubscriber( coroutineScope: CoroutineScope, ): Flow, AppCurrency>> { return combine( - flow = getPrimaryCurrencyStatusUpdatesUseCase(userWallet.walletId) + flow = getSingleCryptoCurrencyStatusUseCase.invokeSingleWallet(userWallet.walletId) .conflate() .distinctUntilChanged(), flow2 = getSelectedAppCurrencyUseCase() diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/SingleWalletButtonsSubscriber.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/SingleWalletButtonsSubscriber.kt index 3404216570..2066f30975 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/SingleWalletButtonsSubscriber.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/SingleWalletButtonsSubscriber.kt @@ -1,7 +1,7 @@ package com.tangem.feature.wallet.presentation.wallet.subscribers import com.tangem.domain.tokens.GetCryptoCurrencyActionsUseCase -import com.tangem.domain.tokens.GetPrimaryCurrencyStatusUpdatesUseCase +import com.tangem.domain.tokens.GetSingleCryptoCurrencyStatusUseCase import com.tangem.domain.tokens.model.TokenActionsState import com.tangem.domain.wallets.models.UserWallet import com.tangem.feature.wallet.child.wallet.model.intents.WalletClickIntents @@ -15,13 +15,13 @@ internal class SingleWalletButtonsSubscriber( private val userWallet: UserWallet, private val stateHolder: WalletStateController, private val clickIntents: WalletClickIntents, - private val getPrimaryCurrencyStatusUpdatesUseCase: GetPrimaryCurrencyStatusUpdatesUseCase, + private val getSingleCryptoCurrencyStatusUseCase: GetSingleCryptoCurrencyStatusUseCase, private val getCryptoCurrencyActionsUseCase: GetCryptoCurrencyActionsUseCase, ) : WalletSubscriber() { override fun create(coroutineScope: CoroutineScope): Flow { return channelFlow { - getPrimaryCurrencyStatusUpdatesUseCase.collectLatest(userWalletId = userWallet.walletId) { status -> + getSingleCryptoCurrencyStatusUseCase.collectLatest(userWalletId = userWallet.walletId) { status -> getCryptoCurrencyActionsUseCase(userWallet = userWallet, cryptoCurrencyStatus = status) .conflate() .distinctUntilChanged() diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/SingleWalletExpressStatusesSubscriber.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/SingleWalletExpressStatusesSubscriber.kt index fd21c385d1..8f46c558d4 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/SingleWalletExpressStatusesSubscriber.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/SingleWalletExpressStatusesSubscriber.kt @@ -8,7 +8,7 @@ import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.onramp.GetOnrampTransactionsUseCase import com.tangem.domain.onramp.OnrampRemoveTransactionUseCase import com.tangem.domain.onramp.model.cache.OnrampTransaction -import com.tangem.domain.tokens.GetPrimaryCurrencyStatusUpdatesUseCase +import com.tangem.domain.tokens.GetSingleCryptoCurrencyStatusUseCase import com.tangem.domain.tokens.error.CurrencyStatusError import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.domain.wallets.models.UserWallet @@ -26,7 +26,7 @@ internal class SingleWalletExpressStatusesSubscriber( private val clickIntents: WalletClickIntents, private val analyticsEventHandler: AnalyticsEventHandler, private val getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase, - private val getPrimaryCurrencyStatusUpdatesUseCase: GetPrimaryCurrencyStatusUpdatesUseCase, + private val getSingleCryptoCurrencyStatusUseCase: GetSingleCryptoCurrencyStatusUseCase, private val getOnrampTransactionsUseCase: GetOnrampTransactionsUseCase, private val onrampRemoveTransactionUseCase: OnrampRemoveTransactionUseCase, ) : WalletSubscriber() { @@ -35,7 +35,7 @@ internal class SingleWalletExpressStatusesSubscriber( coroutineScope: CoroutineScope, ): Flow, AppCurrency>> { return combine( - flow = getPrimaryCurrencyStatusUpdatesUseCase(userWalletId = userWallet.walletId) + flow = getSingleCryptoCurrencyStatusUseCase.invokeSingleWallet(userWalletId = userWallet.walletId) .conflate() .distinctUntilChanged(), flow2 = getSelectedAppCurrencyUseCase() diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/TxHistorySubscriber.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/TxHistorySubscriber.kt index ad67739b99..3058397289 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/TxHistorySubscriber.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/TxHistorySubscriber.kt @@ -5,7 +5,7 @@ import androidx.paging.cachedIn import androidx.paging.map import arrow.core.Either import com.tangem.domain.common.util.cardTypesResolver -import com.tangem.domain.tokens.GetPrimaryCurrencyStatusUpdatesUseCase +import com.tangem.domain.tokens.GetSingleCryptoCurrencyStatusUseCase import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.domain.txhistory.models.TxHistoryItem import com.tangem.domain.txhistory.models.TxHistoryListError @@ -35,14 +35,14 @@ internal class TxHistorySubscriber( private val isRefresh: Boolean, private val stateHolder: WalletStateController, private val clickIntents: WalletClickIntents, - private val getPrimaryCurrencyStatusUpdatesUseCase: GetPrimaryCurrencyStatusUpdatesUseCase, + private val getSingleCryptoCurrencyStatusUseCase: GetSingleCryptoCurrencyStatusUseCase, private val txHistoryItemsCountUseCase: GetTxHistoryItemsCountUseCase, private val txHistoryItemsUseCase: GetTxHistoryItemsUseCase, ) : WalletSubscriber() { override fun create(coroutineScope: CoroutineScope): Flow> { return flow { - getPrimaryCurrencyStatusUpdatesUseCase.collectLatest(userWalletId = userWallet.walletId) { status -> + getSingleCryptoCurrencyStatusUseCase.collectLatest(userWalletId = userWallet.walletId) { status -> val maybeTxHistoryItemCount = txHistoryItemsCountUseCase( userWalletId = userWallet.walletId, currency = status.currency, From 03686da34d60e0c78000c65e6a83bd7a6052777a Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 7 May 2025 22:47:07 +0400 Subject: [PATCH 011/165] Updated on 2026-08-14 --- .../components/WcSignTransactionComponent.kt | 2 +- .../WcSignTransactionRequestInfoComponent.kt | 2 +- .../transaction/entity/WcSignTransactionUM.kt | 56 ----- .../entity/approve/WcApproveTransactionUM.kt | 35 +++- ...ndAllowanceUM.kt => WcSpendAllowanceUM.kt} | 2 +- .../entity/common/WcNetworkInfoUM.kt | 10 + .../entity/common/WcTransactionActionsUM.kt | 10 + .../common/WcTransactionAppInfoContentUM.kt | 8 + .../common/WcTransactionRequestInfoUM.kt | 4 +- .../entity/common/WcTransactionUM.kt | 36 ---- .../entity/sign/WcSignTransactionUM.kt | 24 ++- .../model/WcSignTransactionModel.kt | 6 +- .../WcApproveTransactionModalBottomSheet.kt | 194 +++++++++++------- ...lowanceItem.kt => WcSpendAllowanceItem.kt} | 4 +- .../common/TransactionRequestInfoContent.kt | 125 +++++------ ...RequestFromItem.kt => WcSmallTitleItem.kt} | 0 .../ui/common/WcTransactionRequestButtons.kt | 4 +- .../common/WcTransactionRequestInfoContent.kt | 90 -------- ...cSignTransactionModalBottomSheetContent.kt | 84 ++++---- .../WcTransactionModalBottomSheetContent.kt | 119 ----------- .../utils/WcSignTransactionUtils.kt | 44 ++-- 21 files changed, 308 insertions(+), 551 deletions(-) delete mode 100644 features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/entity/WcSignTransactionUM.kt rename features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/entity/approve/{SpendAllowanceUM.kt => WcSpendAllowanceUM.kt} (76%) create mode 100644 features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/entity/common/WcNetworkInfoUM.kt create mode 100644 features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/entity/common/WcTransactionActionsUM.kt create mode 100644 features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/entity/common/WcTransactionAppInfoContentUM.kt delete mode 100644 features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/entity/common/WcTransactionUM.kt rename features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/ui/approve/{SpendAllowanceItem.kt => WcSpendAllowanceItem.kt} (95%) rename features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/ui/common/{WcRequestFromItem.kt => WcSmallTitleItem.kt} (100%) delete mode 100644 features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/ui/common/WcTransactionRequestInfoContent.kt delete mode 100644 features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/ui/sign/WcTransactionModalBottomSheetContent.kt diff --git a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/components/WcSignTransactionComponent.kt b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/components/WcSignTransactionComponent.kt index 9ffe0a28d5..8f18c4327c 100644 --- a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/components/WcSignTransactionComponent.kt +++ b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/components/WcSignTransactionComponent.kt @@ -20,7 +20,7 @@ internal class WcSignTransactionComponent( if (content != null) { WcSignTransactionModalBottomSheetContent( - state = content, + state = content.transaction, onClickTransactionRequest = transactionInfoOnClick, ) } diff --git a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/components/WcSignTransactionRequestInfoComponent.kt b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/components/WcSignTransactionRequestInfoComponent.kt index 7563b8a646..d78b56b14a 100644 --- a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/components/WcSignTransactionRequestInfoComponent.kt +++ b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/components/WcSignTransactionRequestInfoComponent.kt @@ -18,7 +18,7 @@ internal class WcSignTransactionRequestInfoComponent( val content = model.uiState.collectAsStateWithLifecycle().value if (content != null) { - TransactionRequestInfoContent(content) + TransactionRequestInfoContent(content.transactionRequestInfo) } } } \ No newline at end of file diff --git a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/entity/WcSignTransactionUM.kt b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/entity/WcSignTransactionUM.kt deleted file mode 100644 index 77379d44e1..0000000000 --- a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/entity/WcSignTransactionUM.kt +++ /dev/null @@ -1,56 +0,0 @@ -package com.tangem.features.walletconnect.transaction.entity - -import androidx.annotation.DrawableRes -import androidx.compose.runtime.Immutable -import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfigContent -import com.tangem.core.ui.extensions.TextReference -import kotlinx.collections.immutable.ImmutableList - -@Immutable -internal data class WcSignTransactionUM( - val actions: WcTransactionActionsUM, - val transaction: WcTransactionUM, - val transactionRequestInfo: WcTransactionRequestInfoUM, -) : TangemBottomSheetConfigContent - -@Immutable -internal data class WcTransactionUM( - val appName: String, - val appIcon: String, - val isVerified: Boolean, - val appSubtitle: String, - val walletName: String, - val networkInfo: WcNetworkInfoUM, - val addressText: String? = null, - val networkFee: String? = null, - val isLoading: Boolean = false, -) - -@Immutable -internal data class WcTransactionRequestInfoUM( - val blocks: ImmutableList, -) - -@Immutable -internal data class WcTransactionRequestBlockUM( - val info: ImmutableList, -) - -@Immutable -internal data class WcTransactionRequestInfoItemUM( - val title: TextReference, - val description: String = "", -) - -@Immutable -internal data class WcTransactionActionsUM( - val onDismiss: () -> Unit, - val onSign: () -> Unit, - val onCopy: () -> Unit, -) - -@Immutable -internal data class WcNetworkInfoUM( - val name: String, - @DrawableRes val iconRes: Int, -) \ No newline at end of file diff --git a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/entity/approve/WcApproveTransactionUM.kt b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/entity/approve/WcApproveTransactionUM.kt index cdf8da7c02..f899662751 100644 --- a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/entity/approve/WcApproveTransactionUM.kt +++ b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/entity/approve/WcApproveTransactionUM.kt @@ -1,20 +1,35 @@ package com.tangem.features.walletconnect.transaction.entity.approve +import androidx.annotation.DrawableRes import androidx.compose.runtime.Immutable import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfigContent -import com.tangem.features.walletconnect.transaction.entity.common.WcTransactionActionsUM +import com.tangem.features.walletconnect.transaction.entity.common.WcNetworkInfoUM +import com.tangem.features.walletconnect.transaction.entity.common.WcTransactionAppInfoContentUM import com.tangem.features.walletconnect.transaction.entity.common.WcTransactionRequestInfoUM -import com.tangem.features.walletconnect.transaction.entity.common.WcTransactionUM @Immutable internal data class WcApproveTransactionUM( - val actions: WcTransactionActionsUM, - val state: State = State.TRANSACTION, - val transaction: WcTransactionUM, + val transaction: WcApproveTransactionItemUM, val transactionRequestInfo: WcTransactionRequestInfoUM, -) : TangemBottomSheetConfigContent { + val customAllowance: WcCustomAllowanceUM, +) - enum class State { - TRANSACTION, CUSTOM_ALLOWANCE, TRANSACTION_REQUEST_INFO - } -} \ No newline at end of file +@Immutable +internal data class WcApproveTransactionItemUM( + val onDismiss: () -> Unit, + val onSend: () -> Unit, + val appInfo: WcTransactionAppInfoContentUM, + val spendAllowance: WcSpendAllowanceUM? = null, + val walletName: String, + val networkInfo: WcNetworkInfoUM, + val networkFee: String? = null, + val isLoading: Boolean = false, +) : TangemBottomSheetConfigContent + +@Immutable +internal data class WcCustomAllowanceUM( + @DrawableRes val networkIconRes: Int, + val tokenIconUrl: String, + val amountText: String, + val isUnlimited: Boolean, +) \ No newline at end of file diff --git a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/entity/approve/SpendAllowanceUM.kt b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/entity/approve/WcSpendAllowanceUM.kt similarity index 76% rename from features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/entity/approve/SpendAllowanceUM.kt rename to features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/entity/approve/WcSpendAllowanceUM.kt index 5a4c5a888c..d3609afa63 100644 --- a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/entity/approve/SpendAllowanceUM.kt +++ b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/entity/approve/WcSpendAllowanceUM.kt @@ -1,6 +1,6 @@ package com.tangem.features.walletconnect.transaction.entity.approve -internal data class SpendAllowanceUM( +internal data class WcSpendAllowanceUM( val amountText: String, val tokenImageUrl: String, ) \ No newline at end of file diff --git a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/entity/common/WcNetworkInfoUM.kt b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/entity/common/WcNetworkInfoUM.kt new file mode 100644 index 0000000000..e0706d7cd7 --- /dev/null +++ b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/entity/common/WcNetworkInfoUM.kt @@ -0,0 +1,10 @@ +package com.tangem.features.walletconnect.transaction.entity.common + +import androidx.annotation.DrawableRes +import androidx.compose.runtime.Immutable + +@Immutable +internal data class WcNetworkInfoUM( + val name: String, + @DrawableRes val iconRes: Int, +) \ No newline at end of file diff --git a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/entity/common/WcTransactionActionsUM.kt b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/entity/common/WcTransactionActionsUM.kt new file mode 100644 index 0000000000..58a4ac9b7a --- /dev/null +++ b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/entity/common/WcTransactionActionsUM.kt @@ -0,0 +1,10 @@ +package com.tangem.features.walletconnect.transaction.entity.common + +import androidx.compose.runtime.Immutable + +@Immutable +internal data class WcTransactionActionsUM( + val onDismiss: () -> Unit, + val onSign: () -> Unit, + val onCopy: () -> Unit, +) \ No newline at end of file diff --git a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/entity/common/WcTransactionAppInfoContentUM.kt b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/entity/common/WcTransactionAppInfoContentUM.kt new file mode 100644 index 0000000000..3d84687afa --- /dev/null +++ b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/entity/common/WcTransactionAppInfoContentUM.kt @@ -0,0 +1,8 @@ +package com.tangem.features.walletconnect.transaction.entity.common + +internal data class WcTransactionAppInfoContentUM( + val appName: String, + val appIcon: String, + val isVerified: Boolean, + val appSubtitle: String, +) \ No newline at end of file diff --git a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/entity/common/WcTransactionRequestInfoUM.kt b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/entity/common/WcTransactionRequestInfoUM.kt index dbd160ac11..946ace922b 100644 --- a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/entity/common/WcTransactionRequestInfoUM.kt +++ b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/entity/common/WcTransactionRequestInfoUM.kt @@ -1,13 +1,15 @@ package com.tangem.features.walletconnect.transaction.entity.common import androidx.compose.runtime.Immutable +import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfigContent import com.tangem.core.ui.extensions.TextReference import kotlinx.collections.immutable.ImmutableList @Immutable internal data class WcTransactionRequestInfoUM( val blocks: ImmutableList, -) + val onCopy: () -> Unit, +) : TangemBottomSheetConfigContent @Immutable internal data class WcTransactionRequestBlockUM( diff --git a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/entity/common/WcTransactionUM.kt b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/entity/common/WcTransactionUM.kt deleted file mode 100644 index ae92092db0..0000000000 --- a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/entity/common/WcTransactionUM.kt +++ /dev/null @@ -1,36 +0,0 @@ -package com.tangem.features.walletconnect.transaction.entity.common - -import androidx.annotation.DrawableRes -import androidx.compose.runtime.Immutable -import com.tangem.core.ui.extensions.TextReference -import com.tangem.features.walletconnect.transaction.entity.approve.SpendAllowanceUM - -@Immutable -internal data class WcTransactionUM( - val appName: String, - val appIcon: String, - val isVerified: Boolean, - val appSubtitle: String, - val walletName: String, - val networkInfo: WcNetworkInfoUM, - val activeButtonText: TextReference, - val addressText: String? = null, - val networkFee: String? = null, - val spendAllowance: SpendAllowanceUM? = null, - val isLoading: Boolean = false, -) - -@Immutable -internal data class WcTransactionActionsUM( - val transactionRequestOnClick: () -> Unit, - val onDismiss: () -> Unit, - val activeButtonOnClick: () -> Unit, - val onBack: () -> Unit, - val onCopy: () -> Unit, -) - -@Immutable -internal data class WcNetworkInfoUM( - val name: String, - @DrawableRes val iconRes: Int, -) \ No newline at end of file diff --git a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/entity/sign/WcSignTransactionUM.kt b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/entity/sign/WcSignTransactionUM.kt index 1a86205361..3487650bd4 100644 --- a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/entity/sign/WcSignTransactionUM.kt +++ b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/entity/sign/WcSignTransactionUM.kt @@ -2,19 +2,23 @@ package com.tangem.features.walletconnect.transaction.entity.sign import androidx.compose.runtime.Immutable import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfigContent -import com.tangem.features.walletconnect.transaction.entity.common.WcTransactionActionsUM +import com.tangem.features.walletconnect.transaction.entity.common.WcNetworkInfoUM +import com.tangem.features.walletconnect.transaction.entity.common.WcTransactionAppInfoContentUM import com.tangem.features.walletconnect.transaction.entity.common.WcTransactionRequestInfoUM -import com.tangem.features.walletconnect.transaction.entity.common.WcTransactionUM @Immutable internal data class WcSignTransactionUM( - val actions: WcTransactionActionsUM, - val state: State = State.TRANSACTION, - val transaction: WcTransactionUM, + val transaction: WcSignTransactionItemUM, val transactionRequestInfo: WcTransactionRequestInfoUM, -) : TangemBottomSheetConfigContent { +) - enum class State { - TRANSACTION, TRANSACTION_REQUEST_INFO - } -} \ No newline at end of file +@Immutable +internal data class WcSignTransactionItemUM( + val onDismiss: () -> Unit, + val onSign: () -> Unit, + val appInfo: WcTransactionAppInfoContentUM, + val walletName: String, + val networkInfo: WcNetworkInfoUM, + val addressText: String? = null, + val isLoading: Boolean = false, +) : TangemBottomSheetConfigContent \ No newline at end of file diff --git a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/model/WcSignTransactionModel.kt b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/model/WcSignTransactionModel.kt index ed6ea36064..f906dbd0de 100644 --- a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/model/WcSignTransactionModel.kt +++ b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/model/WcSignTransactionModel.kt @@ -11,8 +11,8 @@ import com.tangem.domain.walletconnect.usecase.method.WcSignState import com.tangem.domain.walletconnect.usecase.method.WcSignStep import com.tangem.domain.walletconnect.usecase.method.WcSignUseCase import com.tangem.features.walletconnect.transaction.components.WcSignTransactionContainerComponent -import com.tangem.features.walletconnect.transaction.entity.WcSignTransactionUM -import com.tangem.features.walletconnect.transaction.entity.WcTransactionActionsUM +import com.tangem.features.walletconnect.transaction.entity.common.WcTransactionActionsUM +import com.tangem.features.walletconnect.transaction.entity.sign.WcSignTransactionUM import com.tangem.features.walletconnect.transaction.utils.toUM import com.tangem.utils.coroutines.CoroutineDispatcherProvider import kotlinx.coroutines.flow.MutableStateFlow @@ -58,7 +58,7 @@ internal class WcSignTransactionModel @Inject constructor( } fun dismiss() { - _uiState.value?.actions?.onDismiss?.invoke() ?: router.pop() + _uiState.value?.transaction?.onDismiss?.invoke() ?: router.pop() } private fun signingIsDone(signState: WcSignState<*>): Boolean { diff --git a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/ui/approve/WcApproveTransactionModalBottomSheet.kt b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/ui/approve/WcApproveTransactionModalBottomSheet.kt index 37449a66ab..a64be58984 100644 --- a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/ui/approve/WcApproveTransactionModalBottomSheet.kt +++ b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/ui/approve/WcApproveTransactionModalBottomSheet.kt @@ -2,78 +2,125 @@ package com.tangem.features.walletconnect.transaction.ui.approve import android.content.res.Configuration import androidx.compose.animation.animateContentSize -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.HorizontalDivider import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip import androidx.compose.ui.tooling.preview.Devices import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig import com.tangem.core.ui.components.bottomsheets.modal.TangemModalBottomSheet import com.tangem.core.ui.components.bottomsheets.modal.TangemModalBottomSheetTitle import com.tangem.core.ui.extensions.resourceReference import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreview +import com.tangem.features.walletconnect.connections.ui.WcAppInfoItem import com.tangem.features.walletconnect.impl.R -import com.tangem.features.walletconnect.transaction.entity.approve.SpendAllowanceUM -import com.tangem.features.walletconnect.transaction.entity.approve.WcApproveTransactionUM +import com.tangem.features.walletconnect.transaction.entity.approve.WcApproveTransactionItemUM +import com.tangem.features.walletconnect.transaction.entity.approve.WcSpendAllowanceUM import com.tangem.features.walletconnect.transaction.entity.common.WcNetworkInfoUM -import com.tangem.features.walletconnect.transaction.entity.common.WcTransactionActionsUM -import com.tangem.features.walletconnect.transaction.entity.common.WcTransactionRequestInfoUM -import com.tangem.features.walletconnect.transaction.entity.common.WcTransactionUM -import com.tangem.features.walletconnect.transaction.ui.common.WcTransactionRequestInfoContent -import com.tangem.features.walletconnect.transaction.ui.sign.WcTransactionModalBottomSheetContent -import kotlinx.collections.immutable.persistentListOf +import com.tangem.features.walletconnect.transaction.entity.common.WcTransactionAppInfoContentUM +import com.tangem.features.walletconnect.transaction.ui.common.* @Composable -internal fun WcApproveTransactionModalBottomSheet(config: TangemBottomSheetConfig) { - TangemModalBottomSheet( - config = config, - containerColor = TangemTheme.colors.background.tertiary, - title = { state -> - TangemModalBottomSheetTitle( - title = when (state.state) { - WcApproveTransactionUM.State.TRANSACTION -> { - resourceReference(R.string.wallet_connect_title) - } - WcApproveTransactionUM.State.CUSTOM_ALLOWANCE -> { - resourceReference(R.string.wc_custom_allowance_title) - } - WcApproveTransactionUM.State.TRANSACTION_REQUEST_INFO -> { - resourceReference(R.string.wc_transaction_request_title) - } - }, - endIconRes = R.drawable.ic_close_24.takeIf { - state.state == WcApproveTransactionUM.State.TRANSACTION - }, - onEndClick = state.actions.onDismiss, - startIconRes = R.drawable.ic_back_24.takeIf { - state.state != WcApproveTransactionUM.State.TRANSACTION - }, - onStartClick = state.actions.onBack, +internal fun WcApproveTransactionModalBottomSheetContent( + state: WcApproveTransactionItemUM, + onClickTransactionRequest: () -> Unit, +) { + Column( + modifier = Modifier + .padding(horizontal = TangemTheme.dimens.spacing16), + ) { + Column( + modifier = Modifier + .clip(RoundedCornerShape(TangemTheme.dimens.radius14)) + .background(color = TangemTheme.colors.background.action) + .fillMaxWidth() + .animateContentSize(), + ) { + WcSmallTitleItem(R.string.wc_request_from) + WcAppInfoItem( + iconUrl = state.appInfo.appIcon, + title = state.appInfo.appName, + subtitle = state.appInfo.appSubtitle, + isVerified = state.appInfo.isVerified, ) - }, - content = { state -> - Box( + DividerWithPadding(start = 0.dp, end = 0.dp) + WcTransactionRequestItem( + iconRes = R.drawable.ic_doc_new_24, modifier = Modifier .fillMaxWidth() - .animateContentSize(), - ) { - when (state.state) { - WcApproveTransactionUM.State.TRANSACTION -> { - WcTransactionModalBottomSheetContent(state.transaction, state.actions) - } - WcApproveTransactionUM.State.CUSTOM_ALLOWANCE -> { - TODO("Will be done in the second part of the PR") - } - WcApproveTransactionUM.State.TRANSACTION_REQUEST_INFO -> { - WcTransactionRequestInfoContent(state.transactionRequestInfo, state.actions) - } - } + .clickable { onClickTransactionRequest() } + .padding(TangemTheme.dimens.spacing12), + ) + } + Column(modifier = Modifier.padding(top = TangemTheme.dimens.spacing16)) { + if (state.spendAllowance != null) { + WcSpendAllowanceItem(state.spendAllowance) + Spacer(Modifier.height(TangemTheme.dimens.spacing16)) } - }, + WcApproveTransactionItems(state) + WcTransactionRequestButtons( + modifier = Modifier.padding(vertical = TangemTheme.dimens.spacing16), + onDismiss = state.onDismiss, + onClickActiveButton = state.onSend, + activeButtonText = resourceReference(R.string.common_send), + isLoading = state.isLoading, + ) + } + } +} + +@Composable +private fun WcApproveTransactionItems(state: WcApproveTransactionItemUM) { + Column( + modifier = Modifier + .clip(RoundedCornerShape(TangemTheme.dimens.radius14)) + .background(color = TangemTheme.colors.background.action) + .fillMaxWidth() + .animateContentSize(), + ) { + val itemsModifier = Modifier + .fillMaxWidth() + .padding(TangemTheme.dimens.spacing12) + + DividerWithPadding(start = 0.dp, end = 0.dp) + WcWalletItem( + modifier = itemsModifier, + walletName = state.walletName, + ) + DividerWithPadding(start = TangemTheme.dimens.spacing40, end = TangemTheme.dimens.spacing12) + WcNetworkItem( + modifier = itemsModifier, + networkInfo = state.networkInfo, + ) + if (!state.networkFee.isNullOrEmpty()) { + DividerWithPadding(start = TangemTheme.dimens.spacing40, end = TangemTheme.dimens.spacing12) + WcNetworkFeeItem( + modifier = itemsModifier, + networkFeeText = state.networkFee, + ) + } + } +} + +@Composable +internal fun DividerWithPadding(start: Dp, end: Dp) { + HorizontalDivider( + modifier = Modifier.padding( + start = start, + end = end, + ), + thickness = TangemTheme.dimens.size1, + color = TangemTheme.colors.stroke.primary, ) } @@ -81,42 +128,47 @@ internal fun WcApproveTransactionModalBottomSheet(config: TangemBottomSheetConfi @Preview(showBackground = true, device = Devices.PIXEL_7_PRO) @Preview(showBackground = true, device = Devices.PIXEL_7_PRO, uiMode = Configuration.UI_MODE_NIGHT_YES) private fun WcApproveTransactionBottomSheetPreview( - @PreviewParameter(WcApproveTransactionStateProvider::class) state: WcApproveTransactionUM, + @PreviewParameter(WcApproveTransactionStateProvider::class) state: WcApproveTransactionItemUM, ) { TangemThemePreview { - WcApproveTransactionModalBottomSheet( + TangemModalBottomSheet( config = TangemBottomSheetConfig( isShown = true, onDismissRequest = {}, content = state, ), + containerColor = TangemTheme.colors.background.tertiary, + title = { + TangemModalBottomSheetTitle( + title = resourceReference(R.string.wallet_connect_title), + endIconRes = R.drawable.ic_close_24, + onEndClick = {}, + startIconRes = null, + onStartClick = {}, + ) + }, + content = { + WcApproveTransactionModalBottomSheetContent(state = state, onClickTransactionRequest = {}) + }, ) } } -private class WcApproveTransactionStateProvider : CollectionPreviewParameterProvider( +private class WcApproveTransactionStateProvider : CollectionPreviewParameterProvider( listOf( - WcApproveTransactionUM( - state = WcApproveTransactionUM.State.TRANSACTION, - transaction = WcTransactionUM( + WcApproveTransactionItemUM( + onDismiss = {}, + onSend = {}, + appInfo = WcTransactionAppInfoContentUM( appName = "React App", appIcon = "", isVerified = true, appSubtitle = "react-app.walletconnect.com", - walletName = "Tangem 2.0", - activeButtonText = resourceReference(R.string.common_send), - networkInfo = WcNetworkInfoUM(name = "Ethereum", iconRes = R.drawable.img_eth_22), - networkFee = "~ 0.22 $", - spendAllowance = SpendAllowanceUM(amountText = "Unlimited USDT", tokenImageUrl = ""), - ), - transactionRequestInfo = WcTransactionRequestInfoUM(persistentListOf()), - actions = WcTransactionActionsUM( - onDismiss = {}, - onBack = {}, - activeButtonOnClick = {}, - onCopy = {}, - transactionRequestOnClick = {}, ), + spendAllowance = WcSpendAllowanceUM(amountText = "Unlimited USDT", tokenImageUrl = ""), + walletName = "Tangem 2.0", + networkInfo = WcNetworkInfoUM(name = "Ethereum", iconRes = R.drawable.img_eth_22), + networkFee = "~ 0.22 $", ), ), ) \ No newline at end of file diff --git a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/ui/approve/SpendAllowanceItem.kt b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/ui/approve/WcSpendAllowanceItem.kt similarity index 95% rename from features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/ui/approve/SpendAllowanceItem.kt rename to features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/ui/approve/WcSpendAllowanceItem.kt index 8dc9381c4c..b6be7f15e0 100644 --- a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/ui/approve/SpendAllowanceItem.kt +++ b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/ui/approve/WcSpendAllowanceItem.kt @@ -15,11 +15,11 @@ import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import coil.compose.AsyncImage import com.tangem.core.ui.res.TangemTheme -import com.tangem.features.walletconnect.transaction.entity.approve.SpendAllowanceUM +import com.tangem.features.walletconnect.transaction.entity.approve.WcSpendAllowanceUM import com.tangem.features.walletconnect.transaction.ui.common.WcSmallTitleItem @Composable -internal fun SpendAllowanceItem(spendAllowance: SpendAllowanceUM, modifier: Modifier = Modifier) { +internal fun WcSpendAllowanceItem(spendAllowance: WcSpendAllowanceUM, modifier: Modifier = Modifier) { Column( modifier = modifier .clip(RoundedCornerShape(TangemTheme.dimens.radius14)) diff --git a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/ui/common/TransactionRequestInfoContent.kt b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/ui/common/TransactionRequestInfoContent.kt index 382da28e98..46051412fe 100644 --- a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/ui/common/TransactionRequestInfoContent.kt +++ b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/ui/common/TransactionRequestInfoContent.kt @@ -27,19 +27,16 @@ import com.tangem.core.ui.extensions.stringResourceSafe import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreview import com.tangem.features.walletconnect.impl.R -import com.tangem.features.walletconnect.transaction.entity.* -import com.tangem.features.walletconnect.transaction.entity.WcNetworkInfoUM -import com.tangem.features.walletconnect.transaction.entity.WcSignTransactionUM -import com.tangem.features.walletconnect.transaction.entity.WcTransactionActionsUM -import com.tangem.features.walletconnect.transaction.entity.WcTransactionRequestInfoUM -import com.tangem.features.walletconnect.transaction.entity.WcTransactionUM +import com.tangem.features.walletconnect.transaction.entity.common.WcTransactionRequestBlockUM +import com.tangem.features.walletconnect.transaction.entity.common.WcTransactionRequestInfoItemUM +import com.tangem.features.walletconnect.transaction.entity.common.WcTransactionRequestInfoUM import kotlinx.collections.immutable.persistentListOf private const val MIN_HEIGHT_SCREEN_PERCENT = 0.35f private const val MAX_HEIGHT_SCREEN_PERCENT = 0.75f @Composable -internal fun TransactionRequestInfoContent(state: WcSignTransactionUM) { +internal fun TransactionRequestInfoContent(state: WcTransactionRequestInfoUM) { val screenHeight = LocalConfiguration.current.screenHeightDp val minHeight = (screenHeight * MIN_HEIGHT_SCREEN_PERCENT).dp val maxHeight = (screenHeight * MAX_HEIGHT_SCREEN_PERCENT).dp @@ -55,7 +52,7 @@ internal fun TransactionRequestInfoContent(state: WcSignTransactionUM) { .fillMaxWidth(), contentPadding = PaddingValues(top = TangemTheme.dimens.spacing8, bottom = TangemTheme.dimens.spacing70), ) { - items(state.transactionRequestInfo.blocks) { block -> + items(state.blocks) { block -> Column( modifier = Modifier .fillMaxWidth() @@ -98,7 +95,7 @@ internal fun TransactionRequestInfoContent(state: WcSignTransactionUM) { .padding(bottom = TangemTheme.dimens.spacing20) .fillMaxWidth(), text = stringResourceSafe(R.string.wc_copy_data_button_text), - onClick = state.actions.onCopy, + onClick = state.onCopy, iconResId = R.drawable.ic_copy_24, ) } @@ -108,10 +105,10 @@ internal fun TransactionRequestInfoContent(state: WcSignTransactionUM) { @Preview(showBackground = true, device = Devices.PIXEL_7_PRO) @Preview(showBackground = true, device = Devices.PIXEL_7_PRO, uiMode = Configuration.UI_MODE_NIGHT_YES) private fun WcSignTransactionRequestInfoBottomSheetPreview( - @PreviewParameter(WcSignTransactionStateProvider::class) state: WcSignTransactionUM, + @PreviewParameter(WcSignTransactionStateProvider::class) state: WcTransactionRequestInfoUM, ) { TangemThemePreview { - TangemModalBottomSheet( + TangemModalBottomSheet( config = TangemBottomSheetConfig( isShown = true, onDismissRequest = {}, @@ -134,86 +131,56 @@ private fun WcSignTransactionRequestInfoBottomSheetPreview( } } -private class WcSignTransactionStateProvider : CollectionPreviewParameterProvider( +private class WcSignTransactionStateProvider : CollectionPreviewParameterProvider( listOf( - WcSignTransactionUM( - transaction = WcTransactionUM( - appName = "React App", - appIcon = "", - isVerified = true, - appSubtitle = "react-app.walletconnect.com", - walletName = "Tangem 2.0", - networkInfo = WcNetworkInfoUM(name = "Ethereum", iconRes = R.drawable.img_eth_22), - ), - transactionRequestInfo = WcTransactionRequestInfoUM( - blocks = persistentListOf( - WcTransactionRequestBlockUM( - persistentListOf( - WcTransactionRequestInfoItemUM( - title = resourceReference(R.string.wc_signature_type), - description = "personal_sign", - ), - WcTransactionRequestInfoItemUM( - title = resourceReference(R.string.wc_contents), - description = "Hello! My name is John Dow. test@tange.com", - ), + WcTransactionRequestInfoUM( + blocks = persistentListOf( + WcTransactionRequestBlockUM( + persistentListOf( + WcTransactionRequestInfoItemUM( + title = resourceReference(R.string.wc_signature_type), + description = "personal_sign", + ), + WcTransactionRequestInfoItemUM( + title = resourceReference(R.string.wc_contents), + description = "Hello! My name is John Dow. test@tange.com", ), ), ), ), - actions = WcTransactionActionsUM( - onDismiss = {}, - onSign = {}, - onCopy = {}, - ), + onCopy = {}, ), - WcSignTransactionUM( - transaction = WcTransactionUM( - appName = "React App", - appIcon = "", - isVerified = true, - appSubtitle = "react-app.walletconnect.com", - walletName = "Tangem 2.0", - networkInfo = WcNetworkInfoUM(name = "Ethereum", iconRes = R.drawable.img_eth_22), - addressText = "0x345FF...34FA", - networkFee = "~ 0.22 $", - ), - transactionRequestInfo = WcTransactionRequestInfoUM( - blocks = persistentListOf( - WcTransactionRequestBlockUM( - persistentListOf( - WcTransactionRequestInfoItemUM( - title = resourceReference(R.string.wc_signature_type), - description = "personal_sign", - ), - WcTransactionRequestInfoItemUM( - title = resourceReference(R.string.wc_contents), - description = "Hello! My name is John Dow. test@tange.com", - ), + WcTransactionRequestInfoUM( + blocks = persistentListOf( + WcTransactionRequestBlockUM( + persistentListOf( + WcTransactionRequestInfoItemUM( + title = resourceReference(R.string.wc_signature_type), + description = "personal_sign", + ), + WcTransactionRequestInfoItemUM( + title = resourceReference(R.string.wc_contents), + description = "Hello! My name is John Dow. test@tange.com", ), ), - WcTransactionRequestBlockUM( - persistentListOf( - WcTransactionRequestInfoItemUM( - title = resourceReference(R.string.wc_transaction_info_to_title), - ), - WcTransactionRequestInfoItemUM( - title = resourceReference(R.string.settings_wallet_name_title), - description = "Bob", - ), - WcTransactionRequestInfoItemUM( - title = resourceReference(R.string.wc_common_wallet), - description = "0xCD2a3d9F938E13CD947Ec05AbC7FE734Df8DD826", - ), + ), + WcTransactionRequestBlockUM( + persistentListOf( + WcTransactionRequestInfoItemUM( + title = resourceReference(R.string.wc_transaction_info_to_title), + ), + WcTransactionRequestInfoItemUM( + title = resourceReference(R.string.settings_wallet_name_title), + description = "Bob", + ), + WcTransactionRequestInfoItemUM( + title = resourceReference(R.string.wc_common_wallet), + description = "0xCD2a3d9F938E13CD947Ec05AbC7FE734Df8DD826", ), ), ), ), - actions = WcTransactionActionsUM( - onDismiss = {}, - onSign = {}, - onCopy = {}, - ), + onCopy = {}, ), ), ) \ No newline at end of file diff --git a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/ui/common/WcRequestFromItem.kt b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/ui/common/WcSmallTitleItem.kt similarity index 100% rename from features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/ui/common/WcRequestFromItem.kt rename to features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/ui/common/WcSmallTitleItem.kt diff --git a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/ui/common/WcTransactionRequestButtons.kt b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/ui/common/WcTransactionRequestButtons.kt index 650d30d63e..9d8ec8d0fb 100644 --- a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/ui/common/WcTransactionRequestButtons.kt +++ b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/ui/common/WcTransactionRequestButtons.kt @@ -18,7 +18,7 @@ internal fun WcTransactionRequestButtons( activeButtonText: TextReference, isLoading: Boolean, onDismiss: () -> Unit, - onSign: () -> Unit, + onClickActiveButton: () -> Unit, modifier: Modifier = Modifier, ) { Row(modifier = modifier, horizontalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing8)) { @@ -34,7 +34,7 @@ internal fun WcTransactionRequestButtons( .fillMaxWidth() .weight(1f), text = activeButtonText.resolveReference(), - onClick = onSign, + onClick = onClickActiveButton, iconResId = R.drawable.ic_tangem_24, showProgress = isLoading, ) diff --git a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/ui/common/WcTransactionRequestInfoContent.kt b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/ui/common/WcTransactionRequestInfoContent.kt deleted file mode 100644 index 736c6b0936..0000000000 --- a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/ui/common/WcTransactionRequestInfoContent.kt +++ /dev/null @@ -1,90 +0,0 @@ -package com.tangem.features.walletconnect.transaction.ui.common - -import androidx.compose.foundation.background -import androidx.compose.foundation.layout.* -import androidx.compose.foundation.lazy.LazyColumn -import androidx.compose.foundation.lazy.items -import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.clip -import androidx.compose.ui.platform.LocalConfiguration -import androidx.compose.ui.unit.dp -import com.tangem.core.ui.components.SecondaryButtonIconEnd -import com.tangem.core.ui.extensions.resolveReference -import com.tangem.core.ui.extensions.stringResourceSafe -import com.tangem.core.ui.res.TangemTheme -import com.tangem.features.walletconnect.impl.R -import com.tangem.features.walletconnect.transaction.entity.common.WcTransactionActionsUM -import com.tangem.features.walletconnect.transaction.entity.common.WcTransactionRequestInfoUM - -private const val MIN_HEIGHT_SCREEN_PERCENT = 0.35f -private const val MAX_HEIGHT_SCREEN_PERCENT = 0.75f - -@Composable -internal fun WcTransactionRequestInfoContent(info: WcTransactionRequestInfoUM, actions: WcTransactionActionsUM) { - val screenHeight = LocalConfiguration.current.screenHeightDp - val minHeight = (screenHeight * MIN_HEIGHT_SCREEN_PERCENT).dp - val maxHeight = (screenHeight * MAX_HEIGHT_SCREEN_PERCENT).dp - - Box( - modifier = Modifier - .fillMaxWidth() - .heightIn(min = minHeight, max = maxHeight) - .padding(horizontal = TangemTheme.dimens.spacing16), - ) { - LazyColumn( - modifier = Modifier - .fillMaxWidth(), - contentPadding = PaddingValues(top = TangemTheme.dimens.spacing8, bottom = TangemTheme.dimens.spacing70), - ) { - items(info.blocks) { block -> - Column( - modifier = Modifier - .fillMaxWidth() - .clip(RoundedCornerShape(TangemTheme.dimens.radius16)) - .background(TangemTheme.colors.background.action) - .padding( - start = TangemTheme.dimens.spacing16, - end = TangemTheme.dimens.spacing16, - top = TangemTheme.dimens.spacing20, - ), - ) { - block.info.forEach { item -> - Text( - text = item.title.resolveReference(), - style = TangemTheme.typography.subtitle2, - color = TangemTheme.colors.text.tertiary, - ) - if (item.description.isNotEmpty()) { - Text( - text = item.description, - modifier = Modifier.padding( - top = TangemTheme.dimens.spacing4, - bottom = TangemTheme.dimens.spacing20, - ), - style = TangemTheme.typography.body2, - color = TangemTheme.colors.text.primary1, - ) - } else { - Spacer(modifier = Modifier.size(TangemTheme.dimens.size12)) - } - } - } - Spacer(modifier = Modifier.size(TangemTheme.dimens.size20)) - } - } - - SecondaryButtonIconEnd( - modifier = Modifier - .align(Alignment.BottomCenter) - .padding(bottom = TangemTheme.dimens.spacing20) - .fillMaxWidth(), - text = stringResourceSafe(R.string.wc_copy_data_button_text), - onClick = actions.onCopy, - iconResId = R.drawable.ic_copy_24, - ) - } -} \ No newline at end of file diff --git a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/ui/sign/WcSignTransactionModalBottomSheetContent.kt b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/ui/sign/WcSignTransactionModalBottomSheetContent.kt index 398d71924e..838f9f4786 100644 --- a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/ui/sign/WcSignTransactionModalBottomSheetContent.kt +++ b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/ui/sign/WcSignTransactionModalBottomSheetContent.kt @@ -26,13 +26,14 @@ import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreview import com.tangem.features.walletconnect.connections.ui.WcAppInfoItem import com.tangem.features.walletconnect.impl.R -import com.tangem.features.walletconnect.transaction.entity.* +import com.tangem.features.walletconnect.transaction.entity.common.WcNetworkInfoUM +import com.tangem.features.walletconnect.transaction.entity.common.WcTransactionAppInfoContentUM +import com.tangem.features.walletconnect.transaction.entity.sign.WcSignTransactionItemUM import com.tangem.features.walletconnect.transaction.ui.common.* -import kotlinx.collections.immutable.persistentListOf @Composable internal fun WcSignTransactionModalBottomSheetContent( - state: WcSignTransactionUM, + state: WcSignTransactionItemUM, onClickTransactionRequest: () -> Unit, ) { Column( @@ -46,12 +47,12 @@ internal fun WcSignTransactionModalBottomSheetContent( .fillMaxWidth() .animateContentSize(), ) { - RequestFromItem() + WcSmallTitleItem(R.string.wc_request_from) WcAppInfoItem( - iconUrl = state.transaction.appIcon, - title = state.transaction.appName, - subtitle = state.transaction.appSubtitle, - isVerified = state.transaction.isVerified, + iconUrl = state.appInfo.appIcon, + title = state.appInfo.appName, + subtitle = state.appInfo.appSubtitle, + isVerified = state.appInfo.isVerified, ) DividerWithPadding(start = 0.dp, end = 0.dp) WcTransactionRequestItem( @@ -66,16 +67,17 @@ internal fun WcSignTransactionModalBottomSheetContent( WcSignTransactionItems(state) WcTransactionRequestButtons( modifier = Modifier.padding(vertical = TangemTheme.dimens.spacing16), - onDismiss = state.actions.onDismiss, - onSign = state.actions.onSign, - isLoading = state.transaction.isLoading, + onDismiss = state.onDismiss, + onClickActiveButton = state.onSign, + activeButtonText = resourceReference(R.string.common_sign), + isLoading = state.isLoading, ) } } } @Composable -private fun WcSignTransactionItems(state: WcSignTransactionUM) { +private fun WcSignTransactionItems(state: WcSignTransactionItemUM) { Column( modifier = Modifier .clip(RoundedCornerShape(TangemTheme.dimens.radius14)) @@ -90,32 +92,25 @@ private fun WcSignTransactionItems(state: WcSignTransactionUM) { DividerWithPadding(start = 0.dp, end = 0.dp) WcWalletItem( modifier = itemsModifier, - walletName = state.transaction.walletName, + walletName = state.walletName, ) DividerWithPadding(start = TangemTheme.dimens.spacing40, end = TangemTheme.dimens.spacing12) WcNetworkItem( modifier = itemsModifier, - networkInfo = state.transaction.networkInfo, + networkInfo = state.networkInfo, ) - if (!state.transaction.addressText.isNullOrEmpty()) { + if (!state.addressText.isNullOrEmpty()) { DividerWithPadding(start = TangemTheme.dimens.spacing40, end = TangemTheme.dimens.spacing12) WcAddressItem( modifier = itemsModifier, - addressText = state.transaction.addressText, - ) - } - if (!state.transaction.networkFee.isNullOrEmpty()) { - DividerWithPadding(start = TangemTheme.dimens.spacing40, end = TangemTheme.dimens.spacing12) - WcNetworkFeeItem( - modifier = itemsModifier, - networkFeeText = state.transaction.networkFee, + addressText = state.addressText, ) } } } @Composable -private fun DividerWithPadding(start: Dp, end: Dp) { +internal fun DividerWithPadding(start: Dp, end: Dp) { HorizontalDivider( modifier = Modifier.padding( start = start, @@ -130,10 +125,10 @@ private fun DividerWithPadding(start: Dp, end: Dp) { @Preview(showBackground = true, device = Devices.PIXEL_7_PRO) @Preview(showBackground = true, device = Devices.PIXEL_7_PRO, uiMode = Configuration.UI_MODE_NIGHT_YES) private fun WcSignTransactionBottomSheetPreview( - @PreviewParameter(WcSignTransactionStateProvider::class) state: WcSignTransactionUM, + @PreviewParameter(WcSignTransactionStateProvider::class) state: WcSignTransactionItemUM, ) { TangemThemePreview { - TangemModalBottomSheet( + TangemModalBottomSheet( config = TangemBottomSheetConfig( isShown = true, onDismissRequest = {}, @@ -156,41 +151,32 @@ private fun WcSignTransactionBottomSheetPreview( } } -private class WcSignTransactionStateProvider : CollectionPreviewParameterProvider( +private class WcSignTransactionStateProvider : CollectionPreviewParameterProvider( listOf( - WcSignTransactionUM( - transaction = WcTransactionUM( + WcSignTransactionItemUM( + onDismiss = {}, + onSign = {}, + appInfo = WcTransactionAppInfoContentUM( appName = "React App", appIcon = "", isVerified = true, appSubtitle = "react-app.walletconnect.com", - walletName = "Tangem 2.0", - networkInfo = WcNetworkInfoUM(name = "Ethereum", iconRes = R.drawable.img_eth_22), - ), - transactionRequestInfo = WcTransactionRequestInfoUM(persistentListOf()), - actions = WcTransactionActionsUM( - onDismiss = {}, - onSign = {}, - onCopy = {}, ), + walletName = "Tangem 2.0", + networkInfo = WcNetworkInfoUM(name = "Ethereum", iconRes = R.drawable.img_eth_22), ), - WcSignTransactionUM( - transaction = WcTransactionUM( + WcSignTransactionItemUM( + onDismiss = {}, + onSign = {}, + appInfo = WcTransactionAppInfoContentUM( appName = "React App", appIcon = "", isVerified = true, appSubtitle = "react-app.walletconnect.com", - walletName = "Tangem 2.0", - networkInfo = WcNetworkInfoUM(name = "Ethereum", iconRes = R.drawable.img_eth_22), - addressText = "0x345FF...34FA", - networkFee = "~ 0.22 $", - ), - transactionRequestInfo = WcTransactionRequestInfoUM(persistentListOf()), - actions = WcTransactionActionsUM( - onDismiss = {}, - onSign = {}, - onCopy = {}, ), + walletName = "Tangem 2.0", + addressText = "0x345FF...34FA", + networkInfo = WcNetworkInfoUM(name = "Ethereum", iconRes = R.drawable.img_eth_22), ), ), ) \ No newline at end of file diff --git a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/ui/sign/WcTransactionModalBottomSheetContent.kt b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/ui/sign/WcTransactionModalBottomSheetContent.kt deleted file mode 100644 index 84d7b4c90e..0000000000 --- a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/ui/sign/WcTransactionModalBottomSheetContent.kt +++ /dev/null @@ -1,119 +0,0 @@ -package com.tangem.features.walletconnect.transaction.ui.sign - -import androidx.compose.animation.animateContentSize -import androidx.compose.foundation.background -import androidx.compose.foundation.clickable -import androidx.compose.foundation.layout.* -import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.material3.HorizontalDivider -import androidx.compose.runtime.Composable -import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.clip -import androidx.compose.ui.unit.Dp -import androidx.compose.ui.unit.dp -import com.tangem.core.ui.res.TangemTheme -import com.tangem.features.walletconnect.connections.ui.WcAppInfoItem -import com.tangem.features.walletconnect.impl.R -import com.tangem.features.walletconnect.transaction.entity.common.WcTransactionActionsUM -import com.tangem.features.walletconnect.transaction.entity.common.WcTransactionUM -import com.tangem.features.walletconnect.transaction.ui.approve.SpendAllowanceItem -import com.tangem.features.walletconnect.transaction.ui.common.* -import com.tangem.features.walletconnect.transaction.ui.common.WcNetworkItem -import com.tangem.features.walletconnect.transaction.ui.common.WcTransactionRequestButtons -import com.tangem.features.walletconnect.transaction.ui.common.WcTransactionRequestItem -import com.tangem.features.walletconnect.transaction.ui.common.WcWalletItem - -@Composable -internal fun WcTransactionModalBottomSheetContent(transaction: WcTransactionUM, actions: WcTransactionActionsUM) { - Column(modifier = Modifier.padding(horizontal = TangemTheme.dimens.spacing16)) { - Column( - modifier = Modifier - .clip(RoundedCornerShape(TangemTheme.dimens.radius14)) - .background(color = TangemTheme.colors.background.action) - .fillMaxWidth() - .animateContentSize(), - ) { - WcSmallTitleItem(R.string.wc_request_from) - WcAppInfoItem( - iconUrl = transaction.appIcon, - title = transaction.appName, - subtitle = transaction.appSubtitle, - isVerified = transaction.isVerified, - ) - DividerWithPadding(start = 0.dp, end = 0.dp) - WcTransactionRequestItem( - iconRes = R.drawable.ic_doc_new_24, - modifier = Modifier - .fillMaxWidth() - .clickable { actions.transactionRequestOnClick() } - .padding(TangemTheme.dimens.spacing12), - ) - } - Column(modifier = Modifier.padding(top = TangemTheme.dimens.spacing16)) { - if (transaction.spendAllowance != null) { - SpendAllowanceItem(transaction.spendAllowance) - Spacer(Modifier.height(TangemTheme.dimens.spacing16)) - } - WcSignTransactionItems(transaction) - WcTransactionRequestButtons( - modifier = Modifier.padding(vertical = TangemTheme.dimens.spacing16), - onDismiss = actions.onDismiss, - onSign = actions.activeButtonOnClick, - activeButtonText = transaction.activeButtonText, - isLoading = transaction.isLoading, - ) - } - } -} - -@Composable -private fun WcSignTransactionItems(transaction: WcTransactionUM) { - Column( - modifier = Modifier - .clip(RoundedCornerShape(TangemTheme.dimens.radius14)) - .background(color = TangemTheme.colors.background.action) - .fillMaxWidth() - .animateContentSize(), - ) { - val itemsModifier = Modifier - .fillMaxWidth() - .padding(TangemTheme.dimens.spacing12) - - DividerWithPadding(start = 0.dp, end = 0.dp) - WcWalletItem( - modifier = itemsModifier, - walletName = transaction.walletName, - ) - DividerWithPadding(start = TangemTheme.dimens.spacing40, end = TangemTheme.dimens.spacing12) - WcNetworkItem( - modifier = itemsModifier, - networkInfo = transaction.networkInfo, - ) - if (!transaction.addressText.isNullOrEmpty()) { - DividerWithPadding(start = TangemTheme.dimens.spacing40, end = TangemTheme.dimens.spacing12) - WcAddressItem( - modifier = itemsModifier, - addressText = transaction.addressText, - ) - } - if (!transaction.networkFee.isNullOrEmpty()) { - DividerWithPadding(start = TangemTheme.dimens.spacing40, end = TangemTheme.dimens.spacing12) - WcNetworkFeeItem( - modifier = itemsModifier, - networkFeeText = transaction.networkFee, - ) - } - } -} - -@Composable -private fun DividerWithPadding(start: Dp, end: Dp) { - HorizontalDivider( - modifier = Modifier.padding( - start = start, - end = end, - ), - thickness = TangemTheme.dimens.size1, - color = TangemTheme.colors.stroke.primary, - ) -} \ No newline at end of file diff --git a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/utils/WcSignTransactionUtils.kt b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/utils/WcSignTransactionUtils.kt index 0442435782..986355b6cf 100644 --- a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/utils/WcSignTransactionUtils.kt +++ b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/utils/WcSignTransactionUtils.kt @@ -11,13 +11,9 @@ import com.tangem.domain.walletconnect.usecase.method.WcSignState import com.tangem.domain.walletconnect.usecase.method.WcSignStep import com.tangem.domain.walletconnect.usecase.method.WcSignUseCase import com.tangem.features.walletconnect.impl.R -import com.tangem.features.walletconnect.transaction.entity.WcNetworkInfoUM -import com.tangem.features.walletconnect.transaction.entity.WcSignTransactionUM -import com.tangem.features.walletconnect.transaction.entity.WcTransactionActionsUM -import com.tangem.features.walletconnect.transaction.entity.WcTransactionRequestBlockUM -import com.tangem.features.walletconnect.transaction.entity.WcTransactionRequestInfoItemUM -import com.tangem.features.walletconnect.transaction.entity.WcTransactionRequestInfoUM -import com.tangem.features.walletconnect.transaction.entity.WcTransactionUM +import com.tangem.features.walletconnect.transaction.entity.common.* +import com.tangem.features.walletconnect.transaction.entity.sign.WcSignTransactionItemUM +import com.tangem.features.walletconnect.transaction.entity.sign.WcSignTransactionUM import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.toImmutableList @@ -47,12 +43,15 @@ private fun WcMessageSignUseCase.signTypedDataToUM( signModel: WcMessageSignUseCase.SignModel, actions: WcTransactionActionsUM, ) = WcSignTransactionUM( - actions = actions, - transaction = WcTransactionUM( - appName = session.sdkModel.appMetaData.name, - appIcon = session.sdkModel.appMetaData.url, - isVerified = session.securityStatus == CheckDAppResult.SAFE, - appSubtitle = session.sdkModel.appMetaData.description, + transaction = WcSignTransactionItemUM( + onDismiss = actions.onDismiss, + onSign = actions.onSign, + appInfo = WcTransactionAppInfoContentUM( + appName = session.sdkModel.appMetaData.name, + appIcon = session.sdkModel.appMetaData.url, + isVerified = session.securityStatus == CheckDAppResult.SAFE, + appSubtitle = session.sdkModel.appMetaData.description, + ), walletName = session.wallet.name, networkInfo = WcNetworkInfoUM( name = network.name, @@ -62,7 +61,7 @@ private fun WcMessageSignUseCase.signTypedDataToUM( isLoading = signState.domainStep == WcSignStep.Signing, ), transactionRequestInfo = WcTransactionRequestInfoUM( - buildList { + blocks = buildList { add(createInfoBlockUM(rawSdkRequest, signModel)) (method as? WcEthMethod.SignTypedData)?.params?.message?.to?.let { to -> add( @@ -84,6 +83,7 @@ private fun WcMessageSignUseCase.signTypedDataToUM( ) } }.toImmutableList(), + onCopy = actions.onCopy, ), ) @@ -92,12 +92,15 @@ private fun WcMessageSignUseCase.messageSignToUM( signModel: WcMessageSignUseCase.SignModel, actions: WcTransactionActionsUM, ) = WcSignTransactionUM( - actions = actions, - transaction = WcTransactionUM( - appName = session.sdkModel.appMetaData.name, - appIcon = session.sdkModel.appMetaData.url, - isVerified = session.securityStatus == CheckDAppResult.SAFE, - appSubtitle = session.sdkModel.appMetaData.description, + transaction = WcSignTransactionItemUM( + onDismiss = actions.onDismiss, + onSign = actions.onSign, + appInfo = WcTransactionAppInfoContentUM( + appName = session.sdkModel.appMetaData.name, + appIcon = session.sdkModel.appMetaData.url, + isVerified = session.securityStatus == CheckDAppResult.SAFE, + appSubtitle = session.sdkModel.appMetaData.description, + ), walletName = session.wallet.name, networkInfo = WcNetworkInfoUM( name = network.name, @@ -107,6 +110,7 @@ private fun WcMessageSignUseCase.messageSignToUM( ), transactionRequestInfo = WcTransactionRequestInfoUM( persistentListOf(createInfoBlockUM(rawSdkRequest, signModel)), + onCopy = actions.onCopy, ), ) From a09cc82bb82d5e159c652851fe05a850fbc31e43 Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 7 May 2025 23:05:24 +0400 Subject: [PATCH 012/165] Updated on 2026-08-14 --- core/res/src/main/res/values-de/strings.xml | 22 ++++--- core/res/src/main/res/values-ja/strings.xml | 20 +++--- core/res/src/main/res/values-ru/strings.xml | 67 +++++++++++++++++++++ core/res/src/main/res/values/strings.xml | 26 ++++---- 4 files changed, 102 insertions(+), 33 deletions(-) diff --git a/core/res/src/main/res/values-de/strings.xml b/core/res/src/main/res/values-de/strings.xml index d3c9f55037..12d2744c43 100644 --- a/core/res/src/main/res/values-de/strings.xml +++ b/core/res/src/main/res/values-de/strings.xml @@ -534,13 +534,13 @@ Basisinformationen Netzwerk Vertragsadresse - Kontrollabschnitt - Kontrollabschnitt - Kontrollabschnitt - Kontrollabschnitt - Kontrollabschnitt - Kontrollabschnitt - Kontrollabschnitt + Chain ist die Blockchain, in der das NFT existiert. + Die Vertragsadresse ist eine eindeutige Kennung für den Smart Contract, der die Token auf der Blockchain verwaltet. + Eine Bezeichnung, die beschreibt, wie selten das NFT ist. Je niedriger der Wert, desto einzigartiger das NFT. + Die Position eines NFT im Seltenheitsranking unter anderen Token. Je höher der Rang, desto seltener und wertvoller ist das NFT. + Die Token-Adresse ist eine eindeutige Kennung für das Token in der Blockchain und ermöglicht die Verfolgung von Transaktionen und Eigentumsverhältnissen + Die Token-ID ist eine eindeutige Kennung, die jedem Token zugewiesen wird und es von anderen in der Sammlung unterscheidet. + Der Token-Standard definiert, um welche Art von Token es sich handelt und wie es mit verschiedenen Wallets und Plattformen funktioniert Letzter Verkaufspreis Raritätsetikett Seltenheitsgrad @@ -553,7 +553,7 @@ Verfügbar Netzwerk auswählen NFT erhalten - Du hast dieses Netzwerk noch nicht hinzugefügt. Um NFTs zu empfangen, füge es zum Hauptbildschirm hinzu! + Du hast dieses Netzwerk noch nicht hinzugefügt. Um NFTs zu erhalten, füge es Deinem Portfolio hinzu. Netzwerk nicht hinzugefügt Nicht hinzugefügt NFT senden @@ -1250,7 +1250,7 @@ Zeitüberschreitungsfehler. Bitte versuche es später erneut. WalletConnect konnte nicht hergestellt werden Diese Domäne kann nicht verifiziert werden. Überprüfe die Anfrage sorgfältig und bestätige diese dann. - Geh zurück zu Deinem Browser und stell erneut eine Verbindung über WalletConnect her. + Bitte kehre zu Deinem Browser zurück und stellen die Verbindung über WalletConnect erneut her. WalletConnect-Sitzung wurde getrennt Fehlercode: %s. Wenn das Problem weiterhin besteht, wende Dich bitte an unseren Support. Wir haben einen unbekannten Fehler festgestellt. @@ -1258,8 +1258,9 @@ Nicht unterstützte Netzwerke Tangem unterstützt ein erforderliches Netzwerk um %s. Verifizierte Domain - Falsche Karte oder falscher Ring in der Tangem-App ausgewählt + Falsche Karte oder falscher Ring in der App ausgewählt Wir haben eine Art Problem + Vertragsadresse Verbinden Netzwerk Netzwerke @@ -1286,6 +1287,7 @@ Bekanntes Sicherheitsrisiko Anfrage von Art der Signatur + An Transaktionsanfrage Transaktionsanfrage Wallet verbinden diff --git a/core/res/src/main/res/values-ja/strings.xml b/core/res/src/main/res/values-ja/strings.xml index 06e907a6d2..3dde89a4c9 100644 --- a/core/res/src/main/res/values-ja/strings.xml +++ b/core/res/src/main/res/values-ja/strings.xml @@ -527,13 +527,13 @@ 基本情報 チェーン コントラクトアドレス - スタブ - スタブ - スタブ - スタブ - スタブ - スタブ - スタブ + チェーンは、NFTが存在するブロックチェーンです。 + コントラクトアドレスは、ブロックチェーン上のトークンを管理するスマートコントラクトの識別子です。 + NFTの希少性を示すラベル。数値が低いほどユニークなNFTであることを示します。 + 他のトークンの中での希少性ランキングにおけるNFTの位置。ランクが高ければ高いほど、NFTの希少性と価値は高くなります。 + トークンアドレスは、ブロックチェーン上のトークンの識別子であり、取引と所有権の追跡を可能にします + トークンIDは、各トークンに割り当てられる識別子であり、コレクション内の他のトークンと区別されます + トークン標準は、トークンの種類と、さまざまなウォレットやプラットフォームでどのように機能するかを定義します 最終販売価格 レアリティ・ラベル レアリティ・ランク @@ -546,7 +546,7 @@ 利用可能 ネットワークを選択 NFTを受け取る - このネットワークはまだ追加されていません。NFTの受け取りを開始するには、メイン画面に追加してください。 + このネットワークはまだ追加されていません。NFTを受け取るには、ポートフォリオに追加してください。 ネットワークが追加されていません 未追加 NFTを送信する @@ -1234,7 +1234,7 @@ タイムアウトエラーが発生しました。しばらくしてからもう一度お試しください。 WalletConnectを確立できませんでした このドメインは検証できません。承認前にリクエスト内容をよく確認してください。 - ブラウザに戻り、WalletConnect経由で再度接続してください。 + ブラウザに戻り、WalletConnect経由で再接続してください。 WalletConnectセッションが接続解除されました エラーコード: %s 。問題が解決しない場合は、お気軽にサポートまでお問い合わせください。 不明なエラーが発生しました @@ -1242,7 +1242,7 @@ サポートされていないネットワーク Tangemは%sで必要なネットワークをサポートします。 検証済みドメイン - Tangemアプリで誤ったカードまたはリングが選択されました + アプリで間違ったカードまたはリングが選択されました 問題が起きています アドレス 接続する diff --git a/core/res/src/main/res/values-ru/strings.xml b/core/res/src/main/res/values-ru/strings.xml index a4e05ae190..930c417497 100644 --- a/core/res/src/main/res/values-ru/strings.xml +++ b/core/res/src/main/res/values-ru/strings.xml @@ -167,6 +167,7 @@ Сетевая комиссия Сумма отправки будет уменьшена на %1$s (%2$s) для покрытия выбранного уровня комиссии Далее + NFT Нет Нет адреса Сейчас @@ -189,6 +190,7 @@ Поиск Поиск токенов сек + Показать всё Seed-фраза Выберите действие Продать @@ -311,6 +313,7 @@ Ожидание депозита Ожидаем пополнения... Возвращено + Возврат средств Отправляем Отправка средств... Отправлено @@ -450,6 +453,9 @@ Лидеры роста Лидеры падения В тренде + Стейкинг — простой способ получать доход с вашей криптовалюты. %s + Показать больше + Получайте до %s APY О %s %d биржа @@ -521,6 +527,51 @@ Потяните вверх или коснитесь поисковой строки, чтобы добавить токены напрямую из рынка Добавить токены Функция NFC недоступна на вашем устройстве + Об NFT + NFT + + NFT + NFT + NFT + NFT + + Здесь будут отображаться NFT, полученные на ваш кошелёк + У вас ещё нет коллекций + Получить NFT + NFT коллекции + Часть данных может не отобразиться + Не удалось загрузить данные + Базовая информация + Сеть + Адрес контракта + Сеть — это блокчейн, на котором существует NFT. + Адрес контракта — уникальный код, который идентифицирует смарт-контракт, управляющий токенами в блокчейне + Метка, которая описывает, насколько редким является NFT. Чем ниже значение, тем уникальнее NFT. + Позиция NFT в рейтинге редкости среди других токенов. Чем выше ранг, тем реже и ценнее NFT. + Адрес токена — это уникальный идентификатор токена в блокчейне, который позволяет отслеживать транзакции и владение им. + ID токена — уникальный идентификатор, присваиваемый каждому токену, который отличает его от других в коллекции + Стандарт токена определяет его тип и как он работает с различными кошельками и платформами + Последняя цена продажи + Метка уникальности + Ранг редкости + Адрес токена + ID токена + Стандарт токена + Черты + Нет результатов. Пожалуйста, попробуйте другой запрос. + Нет коллекции + Доступно + Выберите сеть + Получить NFT + Вы ещё не добавили эту сеть. Чтобы получать NFT, добавьте её в ваш портфель. + Сеть не добавлена + Не добавлено + Отправить NFT + Черты + %1$d NFTs в %2$d коллекциях + Нажмите здесь, чтобы получить первую NFT + NFT коллекции + Невозможно загрузить данные Чтобы использовать сеть %1$s, вы должны оплатить резерв аккаунта (%2$s %3$s), который блокируется и не используется в вашем балансе Аккаунт получателя не активирован. Отправьте %s или более для активации аккаунта. Для создания аккаунта отправьте средства на этот адрес @@ -1024,6 +1075,8 @@ валидатор: %s Минимум %s Минимальная сумма транзакции равна %1$s. + Комиссии сети Tron для популярных токенов могут быть выше. Стейкинг TRX может помочь снизить расходы на транзакции. + Экономьте на Tron комиссиях Попробовать снова Вы отсканировали ту же карту. Для создания twin-кошелька вам необходимо отсканировать карту с номером %d Вы отсканировали не ту twin-карту. Пожалуйста, попробуйте отсканировать другую @@ -1184,6 +1237,20 @@ Это Testnet карта. Он не может обрабатывать транзакции и используется только в целях тестирования и разработки. Только для целей тестирования Возможно, баланс устарел. Обновите страницу. + Пожалуйста, вернитесь в браузер и выполните повторное подключение через WalletConnect. + Выбрана не верная карта или кольцо + Адрес + Подключение + Посмотреть баланс кошелька и его активность + Запрос на подключение + Вложение + Копировать данные + Запрос от + Тип подписи + На + Запрос транзакции + Запрос транзакции + Подключение кошелька Отказаться Вы не закончили резервное копирование. Хотите продолжить? Да, возобновить diff --git a/core/res/src/main/res/values/strings.xml b/core/res/src/main/res/values/strings.xml index e43530482d..869cf9bf8b 100644 --- a/core/res/src/main/res/values/strings.xml +++ b/core/res/src/main/res/values/strings.xml @@ -535,13 +535,13 @@ Base information Chain Contract Address - stub - stub - stub - stub - stub - stub - stub + Chain is the blockchain where the NFT exists. + The contract address is a unique identifier for the smart contract that governs the tokens on the blockchain + A label that describes how rare the NFT is. The lower the value, the more unique the NFT. + The position of an NFT in the rarity ranking among other tokens. The higher the rank, the rarer and more valuable the NFT. + The token address is a unique identifier for the token on the blockchain, allowing tracking of transactions and ownership + The token ID is a unique identifier assigned to each token, distinguishing it from others in the collection + Token standard defines what type of token it is and how it works with different wallets and platforms Last sale price Rarity label Rarity rank @@ -554,7 +554,7 @@ Available Choose network Receive NFT - You haven\'t added this network yet. To start receiving NFTs, add it to the main screen! + You haven\'t added this network yet. To receive NFTs, add it to your portfolio. Network not added Not Added Send NFT @@ -1121,7 +1121,7 @@ This won’t take long. We’re completing the activation. Getting everything ready! Other wallet - Set up a 4-digit code. 
It will be used for payments. + Set up a 4-digit code.\nIt will be used for payments. PIN code Create PIN Code PIN was not accepted. Try again or use a different code. @@ -1295,7 +1295,7 @@ Timeout error. Please, try again later. Failed to establish WalletConnect This domain cannot be verified. Check the request carefully approving. - Go back to your browser and connect via WalletConnect again. + Please return to your browser and reconnect via WalletConnect. WalletConnect session was disconnected Error code: %s. If the problem persists — feel free to contact our support. We\'ve encountered unknown error @@ -1303,8 +1303,9 @@ Unsuported networks Tangem support a required network by %s. Verified domain - Wrong card or ring selected in Tangem App + Wrong card or ring selected in the App We\'ve got some kind of problem + Allow to spend Address Connect Network @@ -1320,6 +1321,7 @@ Connections Contents Copy data + Custom allowance Disconnect all Text about discnected all dApps Disconect All dApps @@ -1336,8 +1338,6 @@ Transaction request Transaction request Wallet connect - Custom allowance - Allow to spend Discard You have an interrupted backup. Do you want to resume? Yes, resume From 3171058cb614cc0ce30dd224e52e2151ba265514 Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 7 May 2025 15:50:08 +0400 Subject: [PATCH 013/165] Updated on 2026-08-14 --- .../datasource/di/local/LocalTokenModule.kt | 21 + .../token/DefaultUserTokensResponseStore.kt | 25 + .../local/token/UserTokensResponseStore.kt | 15 + data/common/build.gradle.kts | 18 +- .../currency/CardCryptoCurrencyFactory.kt | 46 ++ .../DefaultCardCryptoCurrencyFactory.kt | 128 ++++++ .../tangem/data/common/di/DataCommonModule.kt | 33 ++ .../DefaultCardCryptoCurrencyFactoryTest.kt | 430 ++++++++++++++++++ .../DefaultManageTokensRepository.kt | 19 +- .../managetokens/di/ManageTokensDataModule.kt | 17 +- .../DefaultSingleNetworkStatusFetcher.kt | 70 +-- .../DefaultSingleNetworkStatusFetcherTest.kt | 76 ++-- .../tangem/data/tokens/di/TokensDataModule.kt | 5 + .../repository/DefaultCurrenciesRepository.kt | 15 +- .../repository/DefaultNetworksRepository.kt | 14 +- .../utils/CardCryptoCurrenciesFactory.kt | 86 ---- 16 files changed, 796 insertions(+), 222 deletions(-) create mode 100644 core/datasource/src/main/java/com/tangem/datasource/di/local/LocalTokenModule.kt create mode 100644 core/datasource/src/main/java/com/tangem/datasource/local/token/DefaultUserTokensResponseStore.kt create mode 100644 core/datasource/src/main/java/com/tangem/datasource/local/token/UserTokensResponseStore.kt create mode 100644 data/common/src/main/kotlin/com/tangem/data/common/currency/CardCryptoCurrencyFactory.kt create mode 100644 data/common/src/main/kotlin/com/tangem/data/common/currency/DefaultCardCryptoCurrencyFactory.kt create mode 100644 data/common/src/main/kotlin/com/tangem/data/common/di/DataCommonModule.kt create mode 100644 data/common/src/test/kotlin/com/tangem/data/common/currency/DefaultCardCryptoCurrencyFactoryTest.kt delete mode 100644 data/tokens/src/main/kotlin/com/tangem/data/tokens/utils/CardCryptoCurrenciesFactory.kt diff --git a/core/datasource/src/main/java/com/tangem/datasource/di/local/LocalTokenModule.kt b/core/datasource/src/main/java/com/tangem/datasource/di/local/LocalTokenModule.kt new file mode 100644 index 0000000000..1165379d12 --- /dev/null +++ b/core/datasource/src/main/java/com/tangem/datasource/di/local/LocalTokenModule.kt @@ -0,0 +1,21 @@ +package com.tangem.datasource.di.local + +import com.tangem.datasource.local.preferences.AppPreferencesStore +import com.tangem.datasource.local.token.DefaultUserTokensResponseStore +import com.tangem.datasource.local.token.UserTokensResponseStore +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent +import javax.inject.Singleton + +@Module +@InstallIn(SingletonComponent::class) +internal object LocalTokenModule { + + @Provides + @Singleton + fun provideUserTokensResponseStore(appPreferencesStore: AppPreferencesStore): UserTokensResponseStore { + return DefaultUserTokensResponseStore(appPreferencesStore = appPreferencesStore) + } +} \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/token/DefaultUserTokensResponseStore.kt b/core/datasource/src/main/java/com/tangem/datasource/local/token/DefaultUserTokensResponseStore.kt new file mode 100644 index 0000000000..c04690b7ce --- /dev/null +++ b/core/datasource/src/main/java/com/tangem/datasource/local/token/DefaultUserTokensResponseStore.kt @@ -0,0 +1,25 @@ +package com.tangem.datasource.local.token + +import com.tangem.datasource.api.tangemTech.models.UserTokensResponse +import com.tangem.datasource.local.preferences.AppPreferencesStore +import com.tangem.datasource.local.preferences.PreferencesKeys +import com.tangem.datasource.local.preferences.utils.getObjectSyncOrNull +import com.tangem.domain.wallets.models.UserWalletId + +/** + * Default implementation of [UserTokensResponseStore] + * + * @property appPreferencesStore app preferences store + * +[REDACTED_AUTHOR] + */ +internal class DefaultUserTokensResponseStore( + private val appPreferencesStore: AppPreferencesStore, +) : UserTokensResponseStore { + + override suspend fun getSyncOrNull(userWalletId: UserWalletId): UserTokensResponse? { + return appPreferencesStore.getObjectSyncOrNull( + key = PreferencesKeys.getUserTokensKey(userWalletId = userWalletId.stringValue), + ) + } +} \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/token/UserTokensResponseStore.kt b/core/datasource/src/main/java/com/tangem/datasource/local/token/UserTokensResponseStore.kt new file mode 100644 index 0000000000..df802c591c --- /dev/null +++ b/core/datasource/src/main/java/com/tangem/datasource/local/token/UserTokensResponseStore.kt @@ -0,0 +1,15 @@ +package com.tangem.datasource.local.token + +import com.tangem.datasource.api.tangemTech.models.UserTokensResponse +import com.tangem.domain.wallets.models.UserWalletId + +/** + * Store of [UserTokensResponse] + * +[REDACTED_AUTHOR] + */ +interface UserTokensResponseStore { + + /** Get [UserTokensResponse] synchronously by [userWalletId] or null */ + suspend fun getSyncOrNull(userWalletId: UserWalletId): UserTokensResponse? +} \ No newline at end of file diff --git a/data/common/build.gradle.kts b/data/common/build.gradle.kts index 96dc4861ca..05ffe54f76 100644 --- a/data/common/build.gradle.kts +++ b/data/common/build.gradle.kts @@ -14,9 +14,11 @@ dependencies { implementation(projects.core.datasource) /* Domain */ - implementation(projects.domain.models) + implementation(projects.domain.demo) implementation(projects.domain.legacy) + implementation(projects.domain.models) implementation(projects.domain.tokens.models) + implementation(projects.domain.wallets.models) /* Libs - SDK */ implementation(tangemDeps.blockchain) @@ -28,8 +30,16 @@ dependencies { kapt(deps.hilt.kapt) /* Libs - Other */ - implementation(deps.kotlin.coroutines) - implementation(deps.jodatime) - implementation(deps.timber) + implementation(deps.androidx.datastore) implementation(deps.arrow.core) + implementation(deps.jodatime) + implementation(deps.kotlin.coroutines) + implementation(deps.timber) + + /* Test */ + testImplementation(projects.common.test) + testImplementation(deps.test.coroutine) + testImplementation(deps.test.junit) + testImplementation(deps.test.mockk) + testImplementation(deps.test.truth) } \ No newline at end of file diff --git a/data/common/src/main/kotlin/com/tangem/data/common/currency/CardCryptoCurrencyFactory.kt b/data/common/src/main/kotlin/com/tangem/data/common/currency/CardCryptoCurrencyFactory.kt new file mode 100644 index 0000000000..fca4ffba1a --- /dev/null +++ b/data/common/src/main/kotlin/com/tangem/data/common/currency/CardCryptoCurrencyFactory.kt @@ -0,0 +1,46 @@ +package com.tangem.data.common.currency + +import com.tangem.domain.models.scan.ScanResponse +import com.tangem.domain.tokens.model.CryptoCurrency +import com.tangem.domain.tokens.model.Network +import com.tangem.domain.wallets.models.UserWalletId + +/** + * Factory for creating list of [CryptoCurrency] for selected card + * +[REDACTED_AUTHOR] + */ +interface CardCryptoCurrencyFactory { + + /** + * Universal method for creating list of [CryptoCurrency] in [network] for any card + * + * @param userWalletId user wallet id that determines type of card + * @param network network + */ + @Throws + suspend fun create(userWalletId: UserWalletId, network: Network): List + + /** + * Create default coins for multi currency card + * + * @param scanResponse scan response + */ + fun createDefaultCoinsForMultiCurrencyCard(scanResponse: ScanResponse): List + + /** + * Create primary currency for single currency card + * + * @param scanResponse scan response + */ + @Throws + fun createPrimaryCurrencyForSingleCurrencyCard(scanResponse: ScanResponse): CryptoCurrency + + /** + * Create currencies for single currency card with token (like, NODL) + * + * @param scanResponse scan response + */ + @Throws + fun createCurrenciesForSingleCurrencyCardWithToken(scanResponse: ScanResponse): List +} \ No newline at end of file diff --git a/data/common/src/main/kotlin/com/tangem/data/common/currency/DefaultCardCryptoCurrencyFactory.kt b/data/common/src/main/kotlin/com/tangem/data/common/currency/DefaultCardCryptoCurrencyFactory.kt new file mode 100644 index 0000000000..c4da63c87c --- /dev/null +++ b/data/common/src/main/kotlin/com/tangem/data/common/currency/DefaultCardCryptoCurrencyFactory.kt @@ -0,0 +1,128 @@ +package com.tangem.data.common.currency + +import com.tangem.blockchain.common.Blockchain +import com.tangem.blockchainsdk.utils.ExcludedBlockchains +import com.tangem.blockchainsdk.utils.fromNetworkId +import com.tangem.datasource.local.token.UserTokensResponseStore +import com.tangem.datasource.local.userwallet.UserWalletsStore +import com.tangem.domain.common.TapWorkarounds.isTestCard +import com.tangem.domain.common.util.cardTypesResolver +import com.tangem.domain.demo.DemoConfig +import com.tangem.domain.models.scan.ScanResponse +import com.tangem.domain.tokens.model.CryptoCurrency +import com.tangem.domain.tokens.model.Network +import com.tangem.domain.wallets.models.UserWallet +import com.tangem.domain.wallets.models.UserWalletId + +/** + * Default implementation of factory for creating list of [CryptoCurrency] for selected card + * + * @property demoConfig demo config + * @property excludedBlockchains excluded blockchains + * @property userWalletsStore user wallets store + * @property userTokensResponseStore user tokens response store + */ +internal class DefaultCardCryptoCurrencyFactory( + private val demoConfig: DemoConfig, + private val excludedBlockchains: ExcludedBlockchains, + private val userWalletsStore: UserWalletsStore, + private val userTokensResponseStore: UserTokensResponseStore, +) : CardCryptoCurrencyFactory { + + private val cryptoCurrencyFactory by lazy { CryptoCurrencyFactory(excludedBlockchains) } + + override suspend fun create(userWalletId: UserWalletId, network: Network): List { + val userWallet = userWalletsStore.getSyncStrict(key = userWalletId) + + val blockchain = Blockchain.fromNetworkId(networkId = network.backendId) + + // multi-currency wallet + if (userWallet.isMultiCurrency) return getMultiWalletCurrencies(userWallet = userWallet, network = network) + + // check if the blockchain of single-currency wallet is the same as network + val cardBlockchain = userWallet.scanResponse.cardTypesResolver.getBlockchain() + if (cardBlockchain != blockchain) return emptyList() + + // single-currency wallet with token (NODL) + if (userWallet.scanResponse.cardTypesResolver.isSingleWalletWithToken()) { + return createCurrenciesForSingleCurrencyCardWithToken(userWallet.scanResponse) + } + + // single-currency wallet + return createPrimaryCurrencyForSingleCurrencyCard(userWallet.scanResponse).let(::listOf) + } + + override fun createDefaultCoinsForMultiCurrencyCard(scanResponse: ScanResponse): List { + val card = scanResponse.card + + var blockchains = if (demoConfig.isDemoCardId(card.cardId)) { + demoConfig.demoBlockchains + } else { + listOf(Blockchain.Bitcoin, Blockchain.Ethereum) + } + + if (card.isTestCard) { + blockchains = blockchains.mapNotNull { it.getTestnetVersion() } + } + + return blockchains.mapNotNull { + cryptoCurrencyFactory.createCoin( + blockchain = it, + extraDerivationPath = null, + scanResponse = scanResponse, + ) + } + } + + override fun createPrimaryCurrencyForSingleCurrencyCard(scanResponse: ScanResponse): CryptoCurrency { + return with(getSingleWalletCurrencies(scanResponse)) { + primaryToken ?: coin + } + } + + override fun createCurrenciesForSingleCurrencyCardWithToken(scanResponse: ScanResponse): List { + return with(getSingleWalletCurrencies(scanResponse)) { + listOfNotNull(coin, primaryToken) + } + } + + private suspend fun getMultiWalletCurrencies(userWallet: UserWallet, network: Network): List { + val response = userTokensResponseStore.getSyncOrNull(userWalletId = userWallet.walletId) + ?: return emptyList() + + val responseCurrenciesFactory = ResponseCryptoCurrenciesFactory(excludedBlockchains) + + return responseCurrenciesFactory.createCurrencies( + tokens = response.tokens.filter { + it.networkId == network.backendId && it.derivationPath == network.derivationPath.value + }, + scanResponse = userWallet.scanResponse, + ) + } + + private fun getSingleWalletCurrencies(scanResponse: ScanResponse): SingleWalletCurrencies { + val resolver = scanResponse.cardTypesResolver + val blockchain = resolver.getBlockchain() + + val coin = cryptoCurrencyFactory.createCoin( + blockchain = blockchain, + extraDerivationPath = null, + scanResponse = scanResponse, + ) + + requireNotNull(coin) { "Coin for the single currency card cannot be null" } + + val primaryToken = resolver.getPrimaryToken()?.let { token -> + cryptoCurrencyFactory.createToken( + sdkToken = token, + blockchain = blockchain, + extraDerivationPath = null, + scanResponse = scanResponse, + ) + } + + return SingleWalletCurrencies(coin = coin, primaryToken = primaryToken) + } + + private data class SingleWalletCurrencies(val coin: CryptoCurrency, val primaryToken: CryptoCurrency?) +} \ No newline at end of file diff --git a/data/common/src/main/kotlin/com/tangem/data/common/di/DataCommonModule.kt b/data/common/src/main/kotlin/com/tangem/data/common/di/DataCommonModule.kt new file mode 100644 index 0000000000..c9f546e6dd --- /dev/null +++ b/data/common/src/main/kotlin/com/tangem/data/common/di/DataCommonModule.kt @@ -0,0 +1,33 @@ +package com.tangem.data.common.di + +import com.tangem.blockchainsdk.utils.ExcludedBlockchains +import com.tangem.data.common.currency.CardCryptoCurrencyFactory +import com.tangem.data.common.currency.DefaultCardCryptoCurrencyFactory +import com.tangem.datasource.local.token.UserTokensResponseStore +import com.tangem.datasource.local.userwallet.UserWalletsStore +import com.tangem.domain.demo.DemoConfig +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent +import javax.inject.Singleton + +@Module +@InstallIn(SingletonComponent::class) +internal object DataCommonModule { + + @Provides + @Singleton + fun provideCardCryptoCurrencyFactory( + excludedBlockchains: ExcludedBlockchains, + userWalletsStore: UserWalletsStore, + userTokensResponseStore: UserTokensResponseStore, + ): CardCryptoCurrencyFactory { + return DefaultCardCryptoCurrencyFactory( + demoConfig = DemoConfig(), + excludedBlockchains = excludedBlockchains, + userWalletsStore = userWalletsStore, + userTokensResponseStore = userTokensResponseStore, + ) + } +} \ No newline at end of file diff --git a/data/common/src/test/kotlin/com/tangem/data/common/currency/DefaultCardCryptoCurrencyFactoryTest.kt b/data/common/src/test/kotlin/com/tangem/data/common/currency/DefaultCardCryptoCurrencyFactoryTest.kt new file mode 100644 index 0000000000..e6b868dc39 --- /dev/null +++ b/data/common/src/test/kotlin/com/tangem/data/common/currency/DefaultCardCryptoCurrencyFactoryTest.kt @@ -0,0 +1,430 @@ +package com.tangem.data.common.currency + +import android.net.Uri +import com.google.common.truth.Truth +import com.tangem.blockchain.common.Blockchain +import com.tangem.blockchainsdk.utils.ExcludedBlockchains +import com.tangem.common.card.WalletData +import com.tangem.common.test.domain.card.MockScanResponseFactory +import com.tangem.common.test.domain.token.MockCryptoCurrencyFactory +import com.tangem.datasource.local.token.UserTokensResponseStore +import com.tangem.datasource.local.userwallet.UserWalletsStore +import com.tangem.domain.common.configs.GenericCardConfig +import com.tangem.domain.common.util.cardTypesResolver +import com.tangem.domain.demo.DemoConfig +import com.tangem.domain.models.scan.ProductType +import com.tangem.domain.tokens.model.CryptoCurrency +import com.tangem.domain.wallets.models.UserWallet +import com.tangem.domain.wallets.models.UserWalletId +import io.mockk.* +import kotlinx.coroutines.test.runTest +import org.junit.Before +import org.junit.Test + +/** +[REDACTED_AUTHOR] + */ +internal class DefaultCardCryptoCurrencyFactoryTest { + + private val userWalletsStore: UserWalletsStore = mockk() + private val userTokensResponseStore: UserTokensResponseStore = mockk() + + private val factory = DefaultCardCryptoCurrencyFactory( + demoConfig = DemoConfig(), + excludedBlockchains = ExcludedBlockchains(), + userWalletsStore = userWalletsStore, + userTokensResponseStore = userTokensResponseStore, + ) + + @Before + fun setup() { + mockkStatic(Uri::class) + every { Uri.parse(any()) } returns mockk() + } + + @Test + fun `test create if userTokensResponse is not empty`() = runTest { + val multiWallet = createMultiWallet() + + val userTokensResponse = UserTokensResponseFactory().createUserTokensResponse( + currencies = listOf(ethereum), + isGroupedByNetwork = false, + isSortedByBalance = false, + ) + + coEvery { userWalletsStore.getSyncStrict(key = multiWallet.walletId) } returns multiWallet + coEvery { userTokensResponseStore.getSyncOrNull(multiWallet.walletId) } returns userTokensResponse + + val actual = factory.create(userWalletId = multiWallet.walletId, network = ethereum.network) + + coVerifyOrder { + userWalletsStore.getSyncStrict(key = multiWallet.walletId) + userTokensResponseStore.getSyncOrNull(multiWallet.walletId) + } + + val expected = listOf(ethereum) + + Truth.assertThat(actual).isEqualTo(expected) + } + + @Test + fun `test create if userTokensResponse is empty`() = runTest { + val multiWallet = createMultiWallet() + + val userTokensResponse = UserTokensResponseFactory().createUserTokensResponse( + currencies = listOf(), + isGroupedByNetwork = false, + isSortedByBalance = false, + ) + + coEvery { userWalletsStore.getSyncStrict(key = multiWallet.walletId) } returns multiWallet + coEvery { userTokensResponseStore.getSyncOrNull(multiWallet.walletId) } returns userTokensResponse + + val actual = factory.create(userWalletId = multiWallet.walletId, network = ethereum.network) + + coVerifyOrder { + userWalletsStore.getSyncStrict(key = multiWallet.walletId) + userTokensResponseStore.getSyncOrNull(multiWallet.walletId) + } + + val expected = emptyList() + + Truth.assertThat(actual).isEqualTo(expected) + } + + @Test + fun `test create if userTokensResponse is null`() = runTest { + val multiWallet = createMultiWallet() + + coEvery { userWalletsStore.getSyncStrict(key = multiWallet.walletId) } returns multiWallet + coEvery { userTokensResponseStore.getSyncOrNull(multiWallet.walletId) } returns null + + val actual = factory.create(userWalletId = multiWallet.walletId, network = ethereum.network) + + coVerifyOrder { + userWalletsStore.getSyncStrict(key = multiWallet.walletId) + userTokensResponseStore.getSyncOrNull(multiWallet.walletId) + } + + val expected = emptyList() + + Truth.assertThat(actual).isEqualTo(expected) + } + + @Test + fun `test create if userTokensResponse does not contain currency of selected network`() = runTest { + val multiWallet = createMultiWallet() + + val userTokensResponse = UserTokensResponseFactory().createUserTokensResponse( + currencies = listOf(bitcoin), + isGroupedByNetwork = false, + isSortedByBalance = false, + ) + + coEvery { userWalletsStore.getSyncStrict(key = multiWallet.walletId) } returns multiWallet + coEvery { userTokensResponseStore.getSyncOrNull(multiWallet.walletId) } returns userTokensResponse + + val actual = factory.create(userWalletId = multiWallet.walletId, network = ethereum.network) + + coVerifyOrder { + userWalletsStore.getSyncStrict(key = multiWallet.walletId) + userTokensResponseStore.getSyncOrNull(multiWallet.walletId) + } + + val expected = emptyList() + + Truth.assertThat(actual).isEqualTo(expected) + } + + @Test + fun `test create if single wallet has another primary network`() = runTest { + val singleWallet = createSingleWallet() + + coEvery { userWalletsStore.getSyncStrict(key = singleWallet.walletId) } returns singleWallet + + val actual = factory.create(userWalletId = singleWallet.walletId, network = bitcoin.network) + + coVerifyOrder { + userWalletsStore.getSyncStrict(key = singleWallet.walletId) + singleWallet.scanResponse.cardTypesResolver.getBlockchain() + } + + val expected = emptyList() + + Truth.assertThat(actual).isEqualTo(expected) + } + + @Test + fun `test create if card is single wallet`() = runTest { + val singleWallet = createSingleWallet() + + coEvery { userWalletsStore.getSyncStrict(key = singleWallet.walletId) } returns singleWallet + + val actual = factory.create(userWalletId = singleWallet.walletId, network = ethereum.network) + + coVerifyOrder { + userWalletsStore.getSyncStrict(key = singleWallet.walletId) + singleWallet.scanResponse.cardTypesResolver.getBlockchain() + } + + val expected = listOf(ethereum) + + Truth.assertThat(actual).isEqualTo(expected) + } + + @Test + fun `test create if card is single wallet with token`() = runTest { + val singleWallet = createSingleWalletWithToken() + + coEvery { userWalletsStore.getSyncStrict(key = singleWallet.walletId) } returns singleWallet + + val actual = factory.create(userWalletId = singleWallet.walletId, network = ethereum.network) + + val token = CryptoCurrencyFactory(excludedBlockchains = ExcludedBlockchains()).createToken( + sdkToken = singleWallet.scanResponse.cardTypesResolver.getPrimaryToken()!!, + blockchain = Blockchain.Ethereum, + extraDerivationPath = null, + scanResponse = singleWallet.scanResponse, + ) + val expected = listOf(ethereum, token) + + Truth.assertThat(actual).isEqualTo(expected) + } + + @Test + fun `test createDefaultCoinsForMultiCurrencyCard if card is prod`() = runTest { + val multiWallet = createMultiWallet() + + val actual = factory.createDefaultCoinsForMultiCurrencyCard(scanResponse = multiWallet.scanResponse) + + val expected = listOf(bitcoin, ethereum) + + Truth.assertThat(actual).isEqualTo(expected) + } + + @Test + fun `test createDefaultCoinsForMultiCurrencyCard if card is test`() = runTest { + val multiWallet = createMultiWallet().let { + it.copy( + scanResponse = it.scanResponse.copy( + card = it.scanResponse.card.copy(cardId = "FF99", batchId = "99FF"), + ), + ) + } + + val actual = factory.createDefaultCoinsForMultiCurrencyCard(scanResponse = multiWallet.scanResponse) + + val expected = listOf( + cryptoCurrencyFactory.createCoin(blockchain = Blockchain.BitcoinTestnet), + cryptoCurrencyFactory.createCoin(blockchain = Blockchain.EthereumTestnet).setCanHandleTokens(true), + ) + + Truth.assertThat(actual).isEqualTo(expected) + } + + @Test + fun `test createDefaultCoinsForMultiCurrencyCard if card is demo`() = runTest { + val multiWallet = createMultiWallet().let { + it.copy( + scanResponse = it.scanResponse.copy( + card = it.scanResponse.card.copy(cardId = "AC01000000041225"), + ), + ) + } + + val actual = factory.createDefaultCoinsForMultiCurrencyCard(scanResponse = multiWallet.scanResponse) + + val expected = listOf( + bitcoin, + ethereum, + cryptoCurrencyFactory.createCoin(blockchain = Blockchain.Dogecoin), + cryptoCurrencyFactory.createCoin(blockchain = Blockchain.Solana), + ) + + Truth.assertThat(actual).isEqualTo(expected) + } + + @Test + fun `test createPrimaryCurrencyForSingleCurrencyCard if unable to create token`() = runTest { + val singleWallet = UserWallet( + name = "Note", + walletId = UserWalletId("011"), + cardsInWallet = setOf(), + isMultiCurrency = false, + scanResponse = MockScanResponseFactory.create( + cardConfig = GenericCardConfig(maxWalletCount = 2), + derivedKeys = emptyMap(), + ), + hasBackupError = false, + ) + + val actual = runCatching { + factory.createPrimaryCurrencyForSingleCurrencyCard(scanResponse = singleWallet.scanResponse) + } + + val exception = IllegalArgumentException("Coin for the single currency card cannot be null") + + Truth.assertThat(actual.isFailure).isTrue() + Truth.assertThat(actual.exceptionOrNull()).isInstanceOf(exception::class.java) + Truth.assertThat(actual.exceptionOrNull()).hasMessageThat().isEqualTo(exception.message) + } + + @Test + fun `test createPrimaryCurrencyForSingleCurrencyCard if primaryToken is null`() = runTest { + val singleWallet = createSingleWallet() + + val actual = factory.createPrimaryCurrencyForSingleCurrencyCard(scanResponse = singleWallet.scanResponse) + + val expected = ethereum + + Truth.assertThat(actual).isEqualTo(expected) + } + + @Test + fun `test createPrimaryCurrencyForSingleCurrencyCard if primaryToken is not null`() = runTest { + val singleWallet = createSingleWalletWithToken() + + val actual = factory.createPrimaryCurrencyForSingleCurrencyCard(scanResponse = singleWallet.scanResponse) + + val expected = CryptoCurrencyFactory(excludedBlockchains = ExcludedBlockchains()).createToken( + sdkToken = singleWallet.scanResponse.cardTypesResolver.getPrimaryToken()!!, + blockchain = Blockchain.Ethereum, + extraDerivationPath = null, + scanResponse = singleWallet.scanResponse, + ) + + Truth.assertThat(actual).isEqualTo(expected) + } + + @Test + fun `test createCurrenciesForSingleCurrencyCardWithToken if unable to create token`() = runTest { + val singleWalletWithToken = UserWallet( + name = "Note", + walletId = UserWalletId("011"), + cardsInWallet = setOf(), + isMultiCurrency = false, + scanResponse = MockScanResponseFactory.create( + cardConfig = GenericCardConfig(maxWalletCount = 2), + derivedKeys = emptyMap(), + ), + hasBackupError = false, + ) + + val actual = runCatching { + factory.createCurrenciesForSingleCurrencyCardWithToken(scanResponse = singleWalletWithToken.scanResponse) + } + + val exception = IllegalArgumentException("Coin for the single currency card cannot be null") + + Truth.assertThat(actual.isFailure).isTrue() + Truth.assertThat(actual.exceptionOrNull()).isInstanceOf(exception::class.java) + Truth.assertThat(actual.exceptionOrNull()).hasMessageThat().isEqualTo(exception.message) + } + + @Test + fun `test createCurrenciesForSingleCurrencyCardWithToken if primaryToken is null`() = runTest { + val singleWalletWithToken = createSingleWallet() + + val actual = factory.createCurrenciesForSingleCurrencyCardWithToken( + scanResponse = singleWalletWithToken.scanResponse, + ) + + val expected = listOf(ethereum) + + Truth.assertThat(actual).isEqualTo(expected) + } + + @Test + fun `test createCurrenciesForSingleCurrencyCardWithToken if primaryToken is not null`() = runTest { + val singleWalletWithToken = createSingleWalletWithToken() + + val actual = factory.createCurrenciesForSingleCurrencyCardWithToken( + scanResponse = singleWalletWithToken.scanResponse, + ) + + val token = CryptoCurrencyFactory(excludedBlockchains = ExcludedBlockchains()).createToken( + sdkToken = singleWalletWithToken.scanResponse.cardTypesResolver.getPrimaryToken()!!, + blockchain = Blockchain.Ethereum, + extraDerivationPath = null, + scanResponse = singleWalletWithToken.scanResponse, + ) + + val expected = listOf(ethereum, token) + + Truth.assertThat(actual).isEqualTo(expected) + } + + private fun createMultiWallet(): UserWallet { + return UserWallet( + name = "Wallet 1", + walletId = UserWalletId("011"), + cardsInWallet = setOf(), + isMultiCurrency = true, + scanResponse = MockScanResponseFactory.create( + cardConfig = GenericCardConfig(maxWalletCount = 2), + derivedKeys = emptyMap(), + ), + hasBackupError = false, + ) + } + + private fun createSingleWallet(): UserWallet { + return UserWallet( + name = "Note", + walletId = UserWalletId("011"), + cardsInWallet = setOf(), + isMultiCurrency = false, + scanResponse = MockScanResponseFactory.create( + cardConfig = GenericCardConfig(maxWalletCount = 2), + derivedKeys = emptyMap(), + ).let { + it.copy( + card = it.card.copy(batchId = "AB10"), + productType = ProductType.Note, + ) + }, + hasBackupError = false, + ) + } + + private fun createSingleWalletWithToken(): UserWallet { + return UserWallet( + name = "NODL", + walletId = UserWalletId("011"), + cardsInWallet = setOf(), + isMultiCurrency = false, + scanResponse = MockScanResponseFactory.create( + cardConfig = GenericCardConfig(maxWalletCount = 2), + derivedKeys = emptyMap(), + ).copy( + productType = ProductType.Note, + walletData = WalletData( + blockchain = "ETH", + token = WalletData.Token( + name = "Ethereum", + symbol = "ETH", + contractAddress = "0x", + decimals = 8, + ), + ), + ), + hasBackupError = false, + ) + } + + private companion object { + + val cryptoCurrencyFactory = MockCryptoCurrencyFactory() + + val ethereum = cryptoCurrencyFactory.ethereum.setCanHandleTokens(value = true) + + val bitcoin = cryptoCurrencyFactory.createCoin(blockchain = Blockchain.Bitcoin) + + fun CryptoCurrency.setCanHandleTokens(value: Boolean): CryptoCurrency { + return when (this) { + is CryptoCurrency.Coin -> copy(network = network.copy(canHandleTokens = value)) + is CryptoCurrency.Token -> copy(network = network.copy(canHandleTokens = value)) + } + } + } +} \ No newline at end of file diff --git a/data/manage-tokens/src/main/kotlin/com/tangem/data/managetokens/DefaultManageTokensRepository.kt b/data/manage-tokens/src/main/kotlin/com/tangem/data/managetokens/DefaultManageTokensRepository.kt index 84780326f4..f5755416bd 100644 --- a/data/manage-tokens/src/main/kotlin/com/tangem/data/managetokens/DefaultManageTokensRepository.kt +++ b/data/manage-tokens/src/main/kotlin/com/tangem/data/managetokens/DefaultManageTokensRepository.kt @@ -6,12 +6,12 @@ import com.tangem.blockchainsdk.utils.ExcludedBlockchains import com.tangem.blockchainsdk.utils.fromNetworkId import com.tangem.blockchainsdk.utils.toNetworkId import com.tangem.data.common.api.safeApiCall +import com.tangem.data.common.currency.CardCryptoCurrencyFactory import com.tangem.data.common.currency.UserTokensResponseFactory import com.tangem.data.common.currency.getBlockchain import com.tangem.data.common.utils.retryOnError import com.tangem.data.managetokens.utils.ManageTokensUpdateFetcher import com.tangem.data.managetokens.utils.ManagedCryptoCurrencyFactory -import com.tangem.data.tokens.utils.CardCryptoCurrenciesFactory import com.tangem.datasource.api.common.response.getOrThrow import com.tangem.datasource.api.tangemTech.TangemTechApi import com.tangem.datasource.api.tangemTech.models.UserTokensResponse @@ -26,7 +26,6 @@ import com.tangem.domain.common.extensions.canHandleToken import com.tangem.domain.common.extensions.supportedBlockchains import com.tangem.domain.common.extensions.supportedTokens import com.tangem.domain.common.util.cardTypesResolver -import com.tangem.domain.demo.DemoConfig import com.tangem.domain.managetokens.model.* import com.tangem.domain.managetokens.model.ManagedCryptoCurrency.SourceNetwork import com.tangem.domain.managetokens.repository.ManageTokensRepository @@ -48,12 +47,12 @@ internal class DefaultManageTokensRepository( private val appPreferencesStore: AppPreferencesStore, private val testnetTokensStorage: TestnetTokensStorage, private val excludedBlockchains: ExcludedBlockchains, + private val cardCryptoCurrencyFactory: CardCryptoCurrencyFactory, private val dispatchers: CoroutineDispatcherProvider, ) : ManageTokensRepository { private val managedCryptoCurrencyFactory = ManagedCryptoCurrencyFactory(excludedBlockchains) private val userTokensResponseFactory = UserTokensResponseFactory() - private val cardCurrenciesFactory = CardCryptoCurrenciesFactory(DemoConfig(), excludedBlockchains) // region getTokenListBatchFlow override fun getTokenListBatchFlow( @@ -77,7 +76,7 @@ internal class DefaultManageTokensRepository( prefetchDistance = batchSize, batchSize = batchSize, subFetcher = { request, _, isFirstBatchFetching -> - val userWallet = request.params.userWalletId?.let { getUserWallet(it) } + val userWallet = request.params.userWalletId?.let(userWalletsStore::getSyncStrict) if (userWallet?.scanResponse?.card?.isTestCard == true) { fetchTestnetCurrencies(userWallet, request) @@ -190,17 +189,11 @@ internal class DefaultManageTokensRepository( private fun createDefaultUserTokensResponse(userWallet: UserWallet) = userTokensResponseFactory.createUserTokensResponse( - currencies = cardCurrenciesFactory.createDefaultCoinsForMultiCurrencyCard(userWallet.scanResponse), + currencies = cardCryptoCurrencyFactory.createDefaultCoinsForMultiCurrencyCard(userWallet.scanResponse), isGroupedByNetwork = false, isSortedByBalance = false, ) - private suspend fun getUserWallet(userWalletId: UserWalletId): UserWallet { - return requireNotNull(userWalletsStore.getSyncOrNull(userWalletId)) { - "Unable to find a user wallet with provided ID: $userWalletId" - } - } - private fun getSupportedBlockchains(userWallet: UserWallet?): List { return userWallet?.scanResponse?.let { it.card.supportedBlockchains(it.cardTypesResolver, excludedBlockchains) @@ -261,7 +254,7 @@ internal class DefaultManageTokensRepository( userWalletId: UserWalletId, sourceNetwork: SourceNetwork, ): CurrencyUnsupportedState? { - val userWallet = getUserWallet(userWalletId = userWalletId) + val userWallet = userWalletsStore.getSyncStrict(key = userWalletId) val blockchain = getBlockchain(sourceNetwork.id) return when (sourceNetwork) { is SourceNetwork.Default -> checkTokenUnsupportedState(userWallet = userWallet, blockchain = blockchain) @@ -274,7 +267,7 @@ internal class DefaultManageTokensRepository( rawNetworkId: String, isMainNetwork: Boolean, ): CurrencyUnsupportedState? { - val userWallet = getUserWallet(userWalletId = userWalletId) + val userWallet = userWalletsStore.getSyncStrict(key = userWalletId) val blockchain = Blockchain.fromNetworkId(networkId = rawNetworkId) ?: error("Can not create blockchain with given networkId -> $rawNetworkId") return if (isMainNetwork) { diff --git a/data/manage-tokens/src/main/kotlin/com/tangem/data/managetokens/di/ManageTokensDataModule.kt b/data/manage-tokens/src/main/kotlin/com/tangem/data/managetokens/di/ManageTokensDataModule.kt index 3f64ae5970..73044971f5 100644 --- a/data/manage-tokens/src/main/kotlin/com/tangem/data/managetokens/di/ManageTokensDataModule.kt +++ b/data/manage-tokens/src/main/kotlin/com/tangem/data/managetokens/di/ManageTokensDataModule.kt @@ -1,6 +1,7 @@ package com.tangem.data.managetokens.di import com.tangem.blockchainsdk.utils.ExcludedBlockchains +import com.tangem.data.common.currency.CardCryptoCurrencyFactory import com.tangem.data.managetokens.DefaultCustomTokensRepository import com.tangem.data.managetokens.DefaultManageTokensRepository import com.tangem.data.managetokens.utils.ManageTokensUpdateFetcher @@ -32,15 +33,17 @@ internal object ManageTokensDataModule { testnetTokensStorage: TestnetTokensStorage, dispatchers: CoroutineDispatcherProvider, excludedBlockchains: ExcludedBlockchains, + cardCryptoCurrencyFactory: CardCryptoCurrencyFactory, ): ManageTokensRepository { return DefaultManageTokensRepository( - tangemTechApi, - userWalletsStore, - manageTokensUpdateFetcher, - appPreferencesStore, - testnetTokensStorage, - excludedBlockchains, - dispatchers, + tangemTechApi = tangemTechApi, + userWalletsStore = userWalletsStore, + manageTokensUpdateFetcher = manageTokensUpdateFetcher, + appPreferencesStore = appPreferencesStore, + testnetTokensStorage = testnetTokensStorage, + excludedBlockchains = excludedBlockchains, + cardCryptoCurrencyFactory = cardCryptoCurrencyFactory, + dispatchers = dispatchers, ) } diff --git a/data/networks/src/main/java/com/tangem/data/networks/single/DefaultSingleNetworkStatusFetcher.kt b/data/networks/src/main/java/com/tangem/data/networks/single/DefaultSingleNetworkStatusFetcher.kt index 6f72de7745..fed32a810e 100644 --- a/data/networks/src/main/java/com/tangem/data/networks/single/DefaultSingleNetworkStatusFetcher.kt +++ b/data/networks/src/main/java/com/tangem/data/networks/single/DefaultSingleNetworkStatusFetcher.kt @@ -1,27 +1,14 @@ package com.tangem.data.networks.single import arrow.core.Either -import com.tangem.blockchain.common.Blockchain -import com.tangem.blockchainsdk.utils.ExcludedBlockchains -import com.tangem.blockchainsdk.utils.fromNetworkId -import com.tangem.data.common.currency.ResponseCryptoCurrenciesFactory +import com.tangem.data.common.currency.CardCryptoCurrencyFactory import com.tangem.data.networks.store.NetworksStatusesStoreV2 -import com.tangem.data.tokens.utils.CardCryptoCurrenciesFactory import com.tangem.data.tokens.utils.NetworkStatusFactory -import com.tangem.datasource.api.tangemTech.models.UserTokensResponse -import com.tangem.datasource.local.preferences.AppPreferencesStore -import com.tangem.datasource.local.preferences.PreferencesKeys -import com.tangem.datasource.local.preferences.utils.getObjectSyncOrNull -import com.tangem.datasource.local.userwallet.UserWalletsStore -import com.tangem.domain.common.util.cardTypesResolver import com.tangem.domain.core.utils.catchOn -import com.tangem.domain.demo.DemoConfig import com.tangem.domain.networks.single.SingleNetworkStatusFetcher import com.tangem.domain.tokens.model.CryptoCurrency -import com.tangem.domain.tokens.model.Network import com.tangem.domain.tokens.model.NetworkStatus import com.tangem.domain.walletmanager.WalletManagersFacade -import com.tangem.domain.wallets.models.UserWallet import com.tangem.utils.coroutines.CoroutineDispatcherProvider import kotlinx.coroutines.withContext import timber.log.Timber @@ -30,27 +17,20 @@ import javax.inject.Inject /** * Default implementation of [SingleNetworkStatusFetcher] * - * @param excludedBlockchains excluded blockchains - * @property walletManagersFacade wallet managers facade - * @property networksStatusesStore networks statuses store - * @property userWalletsStore user wallets store - * @property appPreferencesStore app preferences store - * @property dispatchers dispatchers + * @property walletManagersFacade wallet managers facade + * @property networksStatusesStore networks statuses store + * @property cardCryptoCurrencyFactory card crypto currency factory + * @property dispatchers dispatchers * [REDACTED_AUTHOR] */ internal class DefaultSingleNetworkStatusFetcher @Inject constructor( - excludedBlockchains: ExcludedBlockchains, private val walletManagersFacade: WalletManagersFacade, private val networksStatusesStore: NetworksStatusesStoreV2, - private val userWalletsStore: UserWalletsStore, - private val appPreferencesStore: AppPreferencesStore, + private val cardCryptoCurrencyFactory: CardCryptoCurrencyFactory, private val dispatchers: CoroutineDispatcherProvider, ) : SingleNetworkStatusFetcher { - private val demoConfig = DemoConfig() - private val cardCurrenciesFactory = CardCryptoCurrenciesFactory(demoConfig, excludedBlockchains) - private val responseCurrenciesFactory = ResponseCryptoCurrenciesFactory(excludedBlockchains) private val networkStatusFactory = NetworkStatusFactory() override suspend fun invoke(params: SingleNetworkStatusFetcher.Params) = Either.catchOn(dispatchers.default) { @@ -58,8 +38,10 @@ internal class DefaultSingleNetworkStatusFetcher @Inject constructor( networksStatusesStore.refresh(userWalletId = params.userWalletId, network = params.network) } - val userWallet = userWalletsStore.getSyncStrict(key = params.userWalletId) - val networkCurrencies = createCurrencies(userWallet = userWallet, network = params.network) + val networkCurrencies = cardCryptoCurrencyFactory.create( + userWalletId = params.userWalletId, + network = params.network, + ) val result = withContext(dispatchers.io) { walletManagersFacade.update( @@ -92,36 +74,4 @@ internal class DefaultSingleNetworkStatusFetcher @Inject constructor( Timber.e("Failed to fetch network status for $params: $it") networksStatusesStore.storeError(userWalletId = params.userWalletId, network = params.network) } - - private suspend fun createCurrencies(userWallet: UserWallet, network: Network): List { - val blockchain = Blockchain.fromNetworkId(networkId = network.backendId) - - // multi-currency wallet - if (userWallet.isMultiCurrency) return getMultiWalletCurrencies(userWallet = userWallet, network = network) - - // check if the blockchain of single-currency wallet is the same as network - val cardBlockchain = userWallet.scanResponse.cardTypesResolver.getBlockchain() - if (cardBlockchain != blockchain) return emptyList() - - // single-currency wallet with token (NODL) - if (userWallet.scanResponse.cardTypesResolver.isSingleWalletWithToken()) { - return cardCurrenciesFactory.createCurrenciesForSingleCurrencyCardWithToken(userWallet.scanResponse) - } - - // single-currency wallet - return cardCurrenciesFactory.createPrimaryCurrencyForSingleCurrencyCard(userWallet.scanResponse).let(::listOf) - } - - private suspend fun getMultiWalletCurrencies(userWallet: UserWallet, network: Network): List { - val response = appPreferencesStore.getObjectSyncOrNull( - key = PreferencesKeys.getUserTokensKey(userWallet.walletId.stringValue), - ) ?: return emptyList() - - return responseCurrenciesFactory.createCurrencies( - tokens = response.tokens.filter { - it.networkId == network.backendId && it.derivationPath == network.derivationPath.value - }, - scanResponse = userWallet.scanResponse, - ) - } } \ No newline at end of file diff --git a/data/networks/src/test/java/com/tangem/data/networks/single/DefaultSingleNetworkStatusFetcherTest.kt b/data/networks/src/test/java/com/tangem/data/networks/single/DefaultSingleNetworkStatusFetcherTest.kt index 7f2a925235..a0da0c0c96 100644 --- a/data/networks/src/test/java/com/tangem/data/networks/single/DefaultSingleNetworkStatusFetcherTest.kt +++ b/data/networks/src/test/java/com/tangem/data/networks/single/DefaultSingleNetworkStatusFetcherTest.kt @@ -2,8 +2,8 @@ package com.tangem.data.networks.single import com.google.common.truth.Truth import com.tangem.common.test.domain.token.MockCryptoCurrencyFactory +import com.tangem.data.common.currency.CardCryptoCurrencyFactory import com.tangem.data.networks.store.NetworksStatusesStoreV2 -import com.tangem.datasource.local.userwallet.UserWalletsStore import com.tangem.domain.networks.single.SingleNetworkStatusFetcher import com.tangem.domain.tokens.model.NetworkStatus import com.tangem.domain.walletmanager.WalletManagersFacade @@ -22,39 +22,35 @@ import org.junit.Test */ internal class DefaultSingleNetworkStatusFetcherTest { - private val walletManagersFacade: WalletManagersFacade = mockk(relaxed = true) - private val networksStatusesStore: NetworksStatusesStoreV2 = mockk(relaxed = true) - private val userWalletsStore: UserWalletsStore = mockk(relaxed = true) + private val walletManagersFacade: WalletManagersFacade = mockk(relaxUnitFun = true) + private val networksStatusesStore: NetworksStatusesStoreV2 = mockk(relaxUnitFun = true) + private val cardCryptoCurrencyFactory: CardCryptoCurrencyFactory = mockk() private val fetcher = DefaultSingleNetworkStatusFetcher( - excludedBlockchains = mockk(relaxed = true), walletManagersFacade = walletManagersFacade, networksStatusesStore = networksStatusesStore, - userWalletsStore = userWalletsStore, - appPreferencesStore = mockk(relaxed = true), + cardCryptoCurrencyFactory = cardCryptoCurrencyFactory, dispatchers = TestingCoroutineDispatcherProvider(), ) @Test fun `fetch network status successfully`() = runTest { - val params = SingleNetworkStatusFetcher.Params( - userWalletId = userWalletId, - network = network, - applyRefresh = true, - ) + val params = createParams() + + coEvery { cardCryptoCurrencyFactory.create(params.userWalletId, params.network) } returns listOf(ethereum) val result = UpdateWalletManagerResult.MissedDerivation - coEvery { walletManagersFacade.update(userWalletId, network, emptySet()) } returns result + coEvery { walletManagersFacade.update(params.userWalletId, params.network, emptySet()) } returns result val actual = fetcher(params) coVerifyOrder { - networksStatusesStore.refresh(userWalletId = userWalletId, network = network) - userWalletsStore.getSyncStrict(key = userWalletId) - walletManagersFacade.update(userWalletId, network, emptySet()) + networksStatusesStore.refresh(params.userWalletId, params.network) + cardCryptoCurrencyFactory.create(params.userWalletId, params.network) + walletManagersFacade.update(params.userWalletId, params.network, emptySet()) networksStatusesStore.storeSuccess( - userWalletId = userWalletId, - value = NetworkStatus(network, NetworkStatus.MissedDerivation), + userWalletId = params.userWalletId, + value = NetworkStatus(params.network, NetworkStatus.MissedDerivation), ) } @@ -63,21 +59,17 @@ internal class DefaultSingleNetworkStatusFetcherTest { @Test fun `fetch network status failure`() = runTest { - val params = SingleNetworkStatusFetcher.Params( - userWalletId = userWalletId, - network = network, - applyRefresh = true, - ) + val params = createParams() val exception = IllegalStateException() - coEvery { userWalletsStore.getSyncStrict(key = userWalletId) } throws exception + coEvery { cardCryptoCurrencyFactory.create(params.userWalletId, params.network) } throws exception val actual = fetcher(params) coVerifyOrder { - networksStatusesStore.refresh(userWalletId = userWalletId, network = network) - userWalletsStore.getSyncStrict(key = userWalletId) - networksStatusesStore.storeError(userWalletId = userWalletId, network = network) + networksStatusesStore.refresh(userWalletId = params.userWalletId, network = params.network) + cardCryptoCurrencyFactory.create(userWalletId = params.userWalletId, network = params.network) + networksStatusesStore.storeError(userWalletId = params.userWalletId, network = params.network) } coVerify(inverse = true) { @@ -91,23 +83,21 @@ internal class DefaultSingleNetworkStatusFetcherTest { @Test fun `fetch network status if applyRefresh is false`() = runTest { - val params = SingleNetworkStatusFetcher.Params( - userWalletId = userWalletId, - network = network, - applyRefresh = false, - ) + val params = createParams(applyRefresh = false) + + coEvery { cardCryptoCurrencyFactory.create(params.userWalletId, params.network) } returns listOf(ethereum) val result = UpdateWalletManagerResult.MissedDerivation - coEvery { walletManagersFacade.update(userWalletId, network, emptySet()) } returns result + coEvery { walletManagersFacade.update(params.userWalletId, params.network, emptySet()) } returns result val actual = fetcher(params) coVerifyOrder { - userWalletsStore.getSyncStrict(key = userWalletId) - walletManagersFacade.update(userWalletId, network, emptySet()) + cardCryptoCurrencyFactory.create(params.userWalletId, params.network) + walletManagersFacade.update(params.userWalletId, params.network, emptySet()) networksStatusesStore.storeSuccess( - userWalletId = userWalletId, - value = NetworkStatus(network, NetworkStatus.MissedDerivation), + userWalletId = params.userWalletId, + value = NetworkStatus(params.network, NetworkStatus.MissedDerivation), ) } @@ -118,8 +108,16 @@ internal class DefaultSingleNetworkStatusFetcherTest { Truth.assertThat(actual.isRight()).isTrue() } + private fun createParams(applyRefresh: Boolean = true): SingleNetworkStatusFetcher.Params { + return SingleNetworkStatusFetcher.Params( + userWalletId = UserWalletId("011"), + network = ethereum.network, + applyRefresh = applyRefresh, + ) + } + private companion object { - val userWalletId = UserWalletId("011") - val network = MockCryptoCurrencyFactory().ethereum.network + + val ethereum = MockCryptoCurrencyFactory().ethereum } } \ No newline at end of file diff --git a/data/tokens/src/main/kotlin/com/tangem/data/tokens/di/TokensDataModule.kt b/data/tokens/src/main/kotlin/com/tangem/data/tokens/di/TokensDataModule.kt index 1172126125..af3cf30f5f 100644 --- a/data/tokens/src/main/kotlin/com/tangem/data/tokens/di/TokensDataModule.kt +++ b/data/tokens/src/main/kotlin/com/tangem/data/tokens/di/TokensDataModule.kt @@ -2,6 +2,7 @@ package com.tangem.data.tokens.di import com.tangem.blockchainsdk.utils.ExcludedBlockchains import com.tangem.data.common.cache.CacheRegistry +import com.tangem.data.common.currency.CardCryptoCurrencyFactory import com.tangem.data.tokens.repository.* import com.tangem.datasource.api.tangemTech.TangemTechApi import com.tangem.datasource.exchangeservice.swap.ExpressServiceLoader @@ -33,6 +34,7 @@ internal object TokensDataModule { dispatchers: CoroutineDispatcherProvider, expressServiceLoader: ExpressServiceLoader, excludedBlockchains: ExcludedBlockchains, + cardCryptoCurrencyFactory: CardCryptoCurrencyFactory, ): CurrenciesRepository { return DefaultCurrenciesRepository( tangemTechApi = tangemTechApi, @@ -43,6 +45,7 @@ internal object TokensDataModule { expressServiceLoader = expressServiceLoader, dispatchers = dispatchers, excludedBlockchains = excludedBlockchains, + cardCryptoCurrencyFactory = cardCryptoCurrencyFactory, ) } @@ -74,6 +77,7 @@ internal object TokensDataModule { cacheRegistry: CacheRegistry, dispatchers: CoroutineDispatcherProvider, excludedBlockchains: ExcludedBlockchains, + cardCryptoCurrencyFactory: CardCryptoCurrencyFactory, ): NetworksRepository { return DefaultNetworksRepository( networksStatusesStore = networksStatusesStore, @@ -83,6 +87,7 @@ internal object TokensDataModule { cacheRegistry = cacheRegistry, dispatchers = dispatchers, excludedBlockchains = excludedBlockchains, + cardCryptoCurrencyFactory = cardCryptoCurrencyFactory, ) } diff --git a/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultCurrenciesRepository.kt b/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultCurrenciesRepository.kt index 9454dd4a87..d68c468811 100644 --- a/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultCurrenciesRepository.kt +++ b/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultCurrenciesRepository.kt @@ -6,7 +6,6 @@ import com.tangem.blockchainsdk.utils.* import com.tangem.data.common.api.safeApiCall import com.tangem.data.common.cache.CacheRegistry import com.tangem.data.common.currency.* -import com.tangem.data.tokens.utils.CardCryptoCurrenciesFactory import com.tangem.data.tokens.utils.CustomTokensMerger import com.tangem.data.tokens.utils.UserTokensBackwardCompatibility import com.tangem.datasource.api.common.response.ApiResponseError @@ -51,12 +50,12 @@ internal class DefaultCurrenciesRepository( private val expressServiceLoader: ExpressServiceLoader, private val dispatchers: CoroutineDispatcherProvider, private val excludedBlockchains: ExcludedBlockchains, + private val cardCryptoCurrencyFactory: CardCryptoCurrencyFactory, ) : CurrenciesRepository { private val demoConfig = DemoConfig() private val responseCurrenciesFactory = ResponseCryptoCurrenciesFactory(excludedBlockchains) private val cryptoCurrencyFactory = CryptoCurrencyFactory(excludedBlockchains) - private val cardCurrenciesFactory = CardCryptoCurrenciesFactory(demoConfig, excludedBlockchains) private val userTokensResponseFactory = UserTokensResponseFactory() private val userTokensBackwardCompatibility = UserTokensBackwardCompatibility() private val customTokensMerger = CustomTokensMerger(tangemTechApi, dispatchers) @@ -223,7 +222,7 @@ internal class DefaultCurrenciesRepository( val userWallet = getUserWallet(userWalletId) ensureIsCorrectUserWallet(userWallet, isMultiCurrencyWalletExpected = false) - val currency = cardCurrenciesFactory.createPrimaryCurrencyForSingleCurrencyCard(userWallet.scanResponse) + val currency = cardCryptoCurrencyFactory.createPrimaryCurrencyForSingleCurrencyCard(userWallet.scanResponse) fetchExpressAssetsByNetworkIds(userWalletId, listOf(currency), refresh) currency } @@ -237,8 +236,8 @@ internal class DefaultCurrenciesRepository( val userWallet = getUserWallet(userWalletId) ensureIsCorrectUserWallet(userWallet, isMultiCurrencyWalletExpected = false) - val currencies = cardCurrenciesFactory.createCurrenciesForSingleCurrencyCardWithToken( - userWallet.scanResponse, + val currencies = cardCryptoCurrencyFactory.createCurrenciesForSingleCurrencyCardWithToken( + scanResponse = userWallet.scanResponse, ) fetchExpressAssetsByNetworkIds(userWalletId, currencies, refresh) currencies @@ -253,7 +252,9 @@ internal class DefaultCurrenciesRepository( val userWallet = getUserWallet(userWalletId) ensureIsCorrectUserWallet(userWallet, isMultiCurrencyWalletExpected = false) - val currency = cardCurrenciesFactory.createCurrenciesForSingleCurrencyCardWithToken(userWallet.scanResponse) + val currency = cardCryptoCurrencyFactory.createCurrenciesForSingleCurrencyCardWithToken( + scanResponse = userWallet.scanResponse, + ) .find { it.id == id } requireNotNull(currency) { "Unable to find currency with provided ID: $id" } fetchExpressAssetsByNetworkIds(userWalletId, listOf(currency)) @@ -672,7 +673,7 @@ internal class DefaultCurrenciesRepository( private fun createDefaultUserTokensResponse(userWallet: UserWallet) = userTokensResponseFactory.createUserTokensResponse( - currencies = cardCurrenciesFactory.createDefaultCoinsForMultiCurrencyCard(userWallet.scanResponse), + currencies = cardCryptoCurrencyFactory.createDefaultCoinsForMultiCurrencyCard(userWallet.scanResponse), isGroupedByNetwork = false, isSortedByBalance = false, ) diff --git a/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultNetworksRepository.kt b/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultNetworksRepository.kt index b85018c34b..80acea39bf 100644 --- a/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultNetworksRepository.kt +++ b/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultNetworksRepository.kt @@ -5,8 +5,8 @@ import com.tangem.blockchain.common.address.AddressType import com.tangem.blockchainsdk.utils.ExcludedBlockchains import com.tangem.blockchainsdk.utils.fromNetworkId import com.tangem.data.common.cache.CacheRegistry +import com.tangem.data.common.currency.CardCryptoCurrencyFactory import com.tangem.data.common.currency.ResponseCryptoCurrenciesFactory -import com.tangem.data.tokens.utils.CardCryptoCurrenciesFactory import com.tangem.data.tokens.utils.NetworkStatusFactory import com.tangem.datasource.api.tangemTech.models.UserTokensResponse import com.tangem.datasource.local.network.NetworksStatusesStore @@ -15,7 +15,6 @@ import com.tangem.datasource.local.preferences.PreferencesKeys import com.tangem.datasource.local.preferences.utils.getObjectSyncOrNull import com.tangem.datasource.local.userwallet.UserWalletsStore import com.tangem.domain.common.util.cardTypesResolver -import com.tangem.domain.demo.DemoConfig import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.domain.tokens.model.CryptoCurrencyAddress import com.tangem.domain.tokens.model.Network @@ -38,12 +37,11 @@ internal class DefaultNetworksRepository( private val userWalletsStore: UserWalletsStore, private val appPreferencesStore: AppPreferencesStore, private val cacheRegistry: CacheRegistry, + private val cardCryptoCurrencyFactory: CardCryptoCurrencyFactory, private val dispatchers: CoroutineDispatcherProvider, excludedBlockchains: ExcludedBlockchains, ) : NetworksRepository { - private val demoConfig = DemoConfig() - private val cardCurrenciesFactory = CardCryptoCurrenciesFactory(demoConfig, excludedBlockchains) private val responseCurrenciesFactory = ResponseCryptoCurrenciesFactory(excludedBlockchains) private val networkStatusFactory = NetworkStatusFactory() @@ -225,10 +223,14 @@ internal class DefaultNetworksRepository( responseCurrenciesFactory.createCurrencies(response, userWallet.scanResponse).asSequence() } else { if (userWallet.scanResponse.cardTypesResolver.isSingleWalletWithToken()) { - cardCurrenciesFactory.createCurrenciesForSingleCurrencyCardWithToken(userWallet.scanResponse) + cardCryptoCurrencyFactory.createCurrenciesForSingleCurrencyCardWithToken( + scanResponse = userWallet.scanResponse, + ) .asSequence() } else { - val currency = cardCurrenciesFactory.createPrimaryCurrencyForSingleCurrencyCard(userWallet.scanResponse) + val currency = cardCryptoCurrencyFactory.createPrimaryCurrencyForSingleCurrencyCard( + scanResponse = userWallet.scanResponse, + ) sequenceOf(currency) } diff --git a/data/tokens/src/main/kotlin/com/tangem/data/tokens/utils/CardCryptoCurrenciesFactory.kt b/data/tokens/src/main/kotlin/com/tangem/data/tokens/utils/CardCryptoCurrenciesFactory.kt deleted file mode 100644 index 1e3ed23c01..0000000000 --- a/data/tokens/src/main/kotlin/com/tangem/data/tokens/utils/CardCryptoCurrenciesFactory.kt +++ /dev/null @@ -1,86 +0,0 @@ -package com.tangem.data.tokens.utils - -import com.tangem.blockchain.common.Blockchain -import com.tangem.blockchainsdk.utils.ExcludedBlockchains -import com.tangem.data.common.currency.CryptoCurrencyFactory -import com.tangem.domain.common.TapWorkarounds.isTestCard -import com.tangem.domain.common.util.cardTypesResolver -import com.tangem.domain.demo.DemoConfig -import com.tangem.domain.models.scan.ScanResponse -import com.tangem.domain.tokens.model.CryptoCurrency - -class CardCryptoCurrenciesFactory( - private val demoConfig: DemoConfig, - excludedBlockchains: ExcludedBlockchains, -) { - - private val cryptoCurrencyFactory = CryptoCurrencyFactory(excludedBlockchains) - - fun createDefaultCoinsForMultiCurrencyCard(scanResponse: ScanResponse): List { - val card = scanResponse.card - - var blockchains = if (demoConfig.isDemoCardId(card.cardId)) { - demoConfig.demoBlockchains - } else { - listOf(Blockchain.Bitcoin, Blockchain.Ethereum) - } - - if (card.isTestCard) { - blockchains = blockchains.mapNotNull { it.getTestnetVersion() } - } - - return blockchains.mapNotNull { - cryptoCurrencyFactory.createCoin( - blockchain = it, - extraDerivationPath = null, - scanResponse = scanResponse, - ) - } - } - - fun createPrimaryCurrencyForSingleCurrencyCard(scanResponse: ScanResponse): CryptoCurrency { - val resolver = scanResponse.cardTypesResolver - val blockchain = resolver.getBlockchain() - - val coin = cryptoCurrencyFactory.createCoin( - blockchain = blockchain, - extraDerivationPath = null, - scanResponse = scanResponse, - ) - requireNotNull(coin) { "Coin for the single currency card cannot be null" } - - val primaryToken = resolver.getPrimaryToken()?.let { token -> - cryptoCurrencyFactory.createToken( - sdkToken = token, - blockchain = blockchain, - extraDerivationPath = null, - scanResponse = scanResponse, - ) - } - - return primaryToken ?: coin - } - - fun createCurrenciesForSingleCurrencyCardWithToken(scanResponse: ScanResponse): List { - val resolver = scanResponse.cardTypesResolver - val blockchain = resolver.getBlockchain() - - val coin = cryptoCurrencyFactory.createCoin( - blockchain = blockchain, - extraDerivationPath = null, - scanResponse = scanResponse, - ) - requireNotNull(coin) { "Coin for the single currency card cannot be null" } - - val primaryToken = resolver.getPrimaryToken()?.let { token -> - cryptoCurrencyFactory.createToken( - sdkToken = token, - blockchain = blockchain, - extraDerivationPath = null, - scanResponse = scanResponse, - ) - } - - return listOfNotNull(coin, primaryToken) - } -} \ No newline at end of file From 06cd34a35a6a60be05c5619efb99d28f9e5b77cf Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 7 May 2025 15:11:03 +0500 Subject: [PATCH 014/165] Updated on 2026-08-14 --- .../local/nft/DefaultNFTPersistenceStore.kt | 8 +- .../local/nft/NFTPersistenceStoreFactory.kt | 7 +- .../datasource/local/nft/custom/NFTPriceId.kt | 8 + .../data/quotes/di/QuoteFetcherModule.kt | 6 + .../single/DefaultSingleQuoteFetcher.kt | 17 ++ .../single/DefaultSingleQuoteFetcherTest.kt | 174 ++++++++++++++++++ .../DefaultTransactionRepository.kt | 9 +- .../quotes/single/SingleQuoteFetcher.kt | 22 +++ 8 files changed, 243 insertions(+), 8 deletions(-) create mode 100644 core/datasource/src/main/java/com/tangem/datasource/local/nft/custom/NFTPriceId.kt create mode 100644 data/quotes/src/main/java/com/tangem/data/quotes/single/DefaultSingleQuoteFetcher.kt create mode 100644 data/quotes/src/test/java/com/tangem/data/quotes/single/DefaultSingleQuoteFetcherTest.kt create mode 100644 domain/quotes/src/main/java/com/tangem/domain/quotes/single/SingleQuoteFetcher.kt diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/nft/DefaultNFTPersistenceStore.kt b/core/datasource/src/main/java/com/tangem/datasource/local/nft/DefaultNFTPersistenceStore.kt index a418b31a5d..42d11c055f 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/local/nft/DefaultNFTPersistenceStore.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/local/nft/DefaultNFTPersistenceStore.kt @@ -3,13 +3,14 @@ package com.tangem.datasource.local.nft import androidx.datastore.core.DataStore import com.tangem.blockchain.nft.models.NFTAsset import com.tangem.blockchain.nft.models.NFTCollection +import com.tangem.datasource.local.nft.custom.NFTPriceId import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.firstOrNull import kotlinx.coroutines.flow.map internal class DefaultNFTPersistenceStore( private val collectionsPersistenceStore: DataStore>, - private val pricesPersistenceStore: DataStore>, + private val pricesPersistenceStore: DataStore>, ) : NFTPersistenceStore { override fun getCollections(): Flow?> = collectionsPersistenceStore.data @@ -27,11 +28,12 @@ internal class DefaultNFTPersistenceStore( } override fun getSalePrice(assetId: NFTAsset.Identifier): Flow = pricesPersistenceStore.data - .map { it[assetId] } + .map { data -> data.associate { it.assetId to it.price }[assetId] } override suspend fun getSalePricesSync(): Map? = pricesPersistenceStore .data .firstOrNull() + ?.associate { it.assetId to it.price } override suspend fun saveCollections(collections: List) { collectionsPersistenceStore.updateData { @@ -41,7 +43,7 @@ internal class DefaultNFTPersistenceStore( override suspend fun saveSalePrice(assetId: NFTAsset.Identifier, salePrice: NFTAsset.SalePrice) { pricesPersistenceStore.updateData { - it.toMutableMap().apply { this[assetId] = salePrice } + it.toMutableList().plus(NFTPriceId(assetId = assetId, price = salePrice)) } } diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/nft/NFTPersistenceStoreFactory.kt b/core/datasource/src/main/java/com/tangem/datasource/local/nft/NFTPersistenceStoreFactory.kt index cb2e6399bf..2dad3c41c8 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/local/nft/NFTPersistenceStoreFactory.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/local/nft/NFTPersistenceStoreFactory.kt @@ -5,12 +5,11 @@ import androidx.datastore.core.DataStore import androidx.datastore.core.DataStoreFactory import androidx.datastore.dataStoreFile import com.squareup.moshi.Moshi -import com.tangem.blockchain.nft.models.NFTAsset import com.tangem.blockchain.nft.models.NFTCollection import com.tangem.datasource.di.NetworkMoshi +import com.tangem.datasource.local.nft.custom.NFTPriceId import com.tangem.datasource.utils.MoshiDataStoreSerializer import com.tangem.datasource.utils.listTypes -import com.tangem.datasource.utils.mapWithCustomKeyTypes import com.tangem.domain.tokens.model.Network import com.tangem.domain.wallets.models.UserWalletId import com.tangem.utils.coroutines.CoroutineDispatcherProvider @@ -48,8 +47,8 @@ class NFTPersistenceStoreFactory @Inject constructor( // result file name example: nft_9a1a178f951a7115555568c09ebad8a882f3d96de25429f0017fe570931e208a_eth_m4460000_prices // result file name example: nft_9a1a178f951a7115555568c09ebad8a882f3d96de25429f0017fe570931e208a_theopennetwork_m446070_prices fileName = "nft_${userWalletStringId}_${networkStringId}_prices", - types = mapWithCustomKeyTypes(), - defaultValue = emptyMap(), + types = listTypes(), + defaultValue = emptyList(), ), ) } diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/nft/custom/NFTPriceId.kt b/core/datasource/src/main/java/com/tangem/datasource/local/nft/custom/NFTPriceId.kt new file mode 100644 index 0000000000..0f933cd743 --- /dev/null +++ b/core/datasource/src/main/java/com/tangem/datasource/local/nft/custom/NFTPriceId.kt @@ -0,0 +1,8 @@ +package com.tangem.datasource.local.nft.custom + +import com.tangem.blockchain.nft.models.NFTAsset + +data class NFTPriceId( + val assetId: NFTAsset.Identifier, + val price: NFTAsset.SalePrice, +) \ No newline at end of file diff --git a/data/quotes/src/main/java/com/tangem/data/quotes/di/QuoteFetcherModule.kt b/data/quotes/src/main/java/com/tangem/data/quotes/di/QuoteFetcherModule.kt index 5a5eb2ecfd..1bdef12d69 100644 --- a/data/quotes/src/main/java/com/tangem/data/quotes/di/QuoteFetcherModule.kt +++ b/data/quotes/src/main/java/com/tangem/data/quotes/di/QuoteFetcherModule.kt @@ -2,8 +2,10 @@ package com.tangem.data.quotes.di import com.tangem.data.quotes.multi.DefaultMultiQuoteFetcher import com.tangem.data.quotes.multi.DefaultMultiQuoteUpdater +import com.tangem.data.quotes.single.DefaultSingleQuoteFetcher import com.tangem.domain.quotes.multi.MultiQuoteFetcher import com.tangem.domain.quotes.multi.MultiQuoteUpdater +import com.tangem.domain.quotes.single.SingleQuoteFetcher import dagger.Binds import dagger.Module import dagger.hilt.InstallIn @@ -21,4 +23,8 @@ internal interface QuoteFetcherModule { @Binds @Singleton fun bindMultiQuoteUpdater(impl: DefaultMultiQuoteUpdater): MultiQuoteUpdater + + @Binds + @Singleton + fun bindSingleQuoteFetcher(impl: DefaultSingleQuoteFetcher): SingleQuoteFetcher } \ No newline at end of file diff --git a/data/quotes/src/main/java/com/tangem/data/quotes/single/DefaultSingleQuoteFetcher.kt b/data/quotes/src/main/java/com/tangem/data/quotes/single/DefaultSingleQuoteFetcher.kt new file mode 100644 index 0000000000..9cd56dacc2 --- /dev/null +++ b/data/quotes/src/main/java/com/tangem/data/quotes/single/DefaultSingleQuoteFetcher.kt @@ -0,0 +1,17 @@ +package com.tangem.data.quotes.single + +import com.tangem.domain.quotes.multi.MultiQuoteFetcher +import com.tangem.domain.quotes.single.SingleQuoteFetcher +import javax.inject.Inject + +internal class DefaultSingleQuoteFetcher @Inject constructor( + private val multiQuoteFetcher: MultiQuoteFetcher, +) : SingleQuoteFetcher { + + override suspend fun invoke(params: SingleQuoteFetcher.Params) = multiQuoteFetcher.invoke( + MultiQuoteFetcher.Params( + currenciesIds = setOf(params.rawCurrencyId), + appCurrencyId = params.appCurrencyId, + ), + ) +} \ No newline at end of file diff --git a/data/quotes/src/test/java/com/tangem/data/quotes/single/DefaultSingleQuoteFetcherTest.kt b/data/quotes/src/test/java/com/tangem/data/quotes/single/DefaultSingleQuoteFetcherTest.kt new file mode 100644 index 0000000000..fc1799ba0c --- /dev/null +++ b/data/quotes/src/test/java/com/tangem/data/quotes/single/DefaultSingleQuoteFetcherTest.kt @@ -0,0 +1,174 @@ +package com.tangem.data.quotes.single + +import com.google.common.truth.Truth +import com.tangem.common.test.data.quote.MockQuoteResponseFactory +import com.tangem.data.quotes.multi.DefaultMultiQuoteFetcher +import com.tangem.data.quotes.store.QuotesStoreV2 +import com.tangem.datasource.api.common.response.ApiResponse +import com.tangem.datasource.api.common.response.ApiResponseError +import com.tangem.datasource.api.tangemTech.TangemTechApi +import com.tangem.datasource.api.tangemTech.models.CurrenciesResponse +import com.tangem.datasource.api.tangemTech.models.QuotesResponse +import com.tangem.datasource.appcurrency.AppCurrencyResponseStore +import com.tangem.domain.quotes.single.SingleQuoteFetcher +import com.tangem.domain.tokens.model.CryptoCurrency +import com.tangem.utils.coroutines.TestingCoroutineDispatcherProvider +import io.mockk.coEvery +import io.mockk.coVerify +import io.mockk.coVerifyOrder +import io.mockk.mockk +import kotlinx.coroutines.test.runTest +import org.junit.Test +import java.math.BigDecimal + +internal class DefaultSingleQuoteFetcherTest { + + private val tangemTechApi = mockk(relaxed = true) + private val appCurrencyResponseStore = mockk(relaxed = true) + private val quotesStore = mockk(relaxed = true) + + private val multiFetcher = DefaultMultiQuoteFetcher( + tangemTechApi = tangemTechApi, + appCurrencyResponseStore = appCurrencyResponseStore, + quotesStore = quotesStore, + dispatchers = TestingCoroutineDispatcherProvider(), + ) + + private val singleFetcher = DefaultSingleQuoteFetcher(multiFetcher) + + @Test + fun `fetch single quote successfully`() = runTest { + val params = SingleQuoteFetcher.Params(rawCurrencyId = currenciesId, appCurrencyId = null) + + coEvery { appCurrencyResponseStore.getSyncOrNull() } returns usdAppCurrency + + val coinIds = "BTC" + coEvery { + tangemTechApi.getQuotes(currencyId = "usd", coinIds = coinIds) + } returns ApiResponse.Success(successResponse) + + val actual = singleFetcher(params) + + coVerifyOrder { + quotesStore.refresh(currenciesIds = setOf(params.rawCurrencyId)) + appCurrencyResponseStore.getSyncOrNull() + tangemTechApi.getQuotes(currencyId = "usd", coinIds = coinIds) + quotesStore.storeActual(values = successResponse.quotes) + } + + coVerify(inverse = true) { + quotesStore.storeError(currenciesIds = any()) + } + + Truth.assertThat(actual.isRight()).isTrue() + } + + @Test + fun `fetch single quote successfully if appCurrencyId from params is not null`() = runTest { + val appCurrencyId = "usd" + val params = SingleQuoteFetcher.Params(rawCurrencyId = currenciesId, appCurrencyId = appCurrencyId) + + val coinIds = "BTC" + coEvery { + tangemTechApi.getQuotes(currencyId = appCurrencyId, coinIds = coinIds) + } returns ApiResponse.Success(successResponse) + + val actual = singleFetcher(params) + + coVerifyOrder { + quotesStore.refresh(currenciesIds = setOf(currenciesId)) + tangemTechApi.getQuotes(currencyId = "usd", coinIds = coinIds) + quotesStore.storeActual(values = successResponse.quotes) + } + + Truth.assertThat(actual.isRight()).isTrue() + } + + @Test + fun `fetch single quote failure because appCurrencyId from params is blank`() = runTest { + val appCurrencyId = "" + val params = SingleQuoteFetcher.Params(rawCurrencyId = currenciesId, appCurrencyId = appCurrencyId) + + val actual = singleFetcher(params) + + coVerifyOrder { + quotesStore.refresh(currenciesIds = setOf(currenciesId)) + quotesStore.storeError(currenciesIds = setOf(currenciesId)) + } + + Truth.assertThat(actual.isLeft()).isTrue() + Truth.assertThat(actual.leftOrNull()).isInstanceOf(IllegalStateException::class.java) + Truth.assertThat(actual.leftOrNull()).hasMessageThat() + .isEqualTo("Unable to get AppCurrency for updating quotes") + } + + @Test + fun `fetch single quote failure because api request failed`() = runTest { + val params = SingleQuoteFetcher.Params(rawCurrencyId = currenciesId, appCurrencyId = null) + + coEvery { appCurrencyResponseStore.getSyncOrNull() } returns usdAppCurrency + + val coinIds = "BTC" + + @Suppress("UNCHECKED_CAST") + val errorResponse = ApiResponse.Error(ApiResponseError.NetworkException) as ApiResponse + coEvery { tangemTechApi.getQuotes(currencyId = "usd", coinIds = coinIds) } returns errorResponse + + val actual = singleFetcher(params) + + coVerifyOrder { + quotesStore.refresh(currenciesIds = setOf(currenciesId)) + appCurrencyResponseStore.getSyncOrNull() + tangemTechApi.getQuotes(currencyId = "usd", coinIds = coinIds) + quotesStore.storeError(currenciesIds = setOf(currenciesId)) + } + + coVerify(inverse = true) { + quotesStore.storeActual(values = any()) + } + + Truth.assertThat(actual.isLeft()).isTrue() + } + + @Test + fun `fetch single quote failure because app currency not found`() = runTest { + val params = SingleQuoteFetcher.Params(rawCurrencyId = currenciesId, appCurrencyId = null) + + coEvery { appCurrencyResponseStore.getSyncOrNull() } returns null + + val actual = singleFetcher(params) + + coVerifyOrder { + quotesStore.refresh(currenciesIds = setOf(currenciesId)) + appCurrencyResponseStore.getSyncOrNull() + quotesStore.storeError(currenciesIds = setOf(currenciesId)) + } + + coVerify(inverse = true) { + tangemTechApi.getQuotes(currencyId = any(), coinIds = any()) + quotesStore.storeActual(values = any()) + } + + Truth.assertThat(actual.isLeft()).isTrue() + } + + private companion object { + + val currenciesId = CryptoCurrency.RawID(value = "BTC") + + val usdAppCurrency = CurrenciesResponse.Currency( + id = "USD".lowercase(), + code = "USD", + name = "US Dollar", + unit = "$", + type = "fiat", + rateBTC = "", + ) + + val successResponse = QuotesResponse( + quotes = mapOf( + "BTC" to MockQuoteResponseFactory.createSinglePrice(value = BigDecimal.ONE), + ), + ) + } +} \ No newline at end of file diff --git a/data/transaction/src/main/java/com/tangem/data/transaction/DefaultTransactionRepository.kt b/data/transaction/src/main/java/com/tangem/data/transaction/DefaultTransactionRepository.kt index 6ba07ef2e1..540ffdb8ed 100644 --- a/data/transaction/src/main/java/com/tangem/data/transaction/DefaultTransactionRepository.kt +++ b/data/transaction/src/main/java/com/tangem/data/transaction/DefaultTransactionRepository.kt @@ -190,12 +190,19 @@ internal class DefaultTransactionRepository( null } + val contractAddress = when (val identifier = nftAsset.identifier) { + is NFTAsset.Identifier.EVM -> identifier.tokenAddress + is NFTAsset.Identifier.Solana -> identifier.tokenAddress + is NFTAsset.Identifier.TON -> identifier.tokenAddress + NFTAsset.Identifier.Unknown -> "" + } + return@withContext createTransaction( amount = Amount( value = nftAsset.amount?.toBigDecimal() ?: error("Invalid amount"), token = Token( symbol = blockchain.currency, - contractAddress = "", + contractAddress = contractAddress, decimals = nftAsset.decimals ?: error("Invalid decimals"), ), ), diff --git a/domain/quotes/src/main/java/com/tangem/domain/quotes/single/SingleQuoteFetcher.kt b/domain/quotes/src/main/java/com/tangem/domain/quotes/single/SingleQuoteFetcher.kt new file mode 100644 index 0000000000..6923e9a50e --- /dev/null +++ b/domain/quotes/src/main/java/com/tangem/domain/quotes/single/SingleQuoteFetcher.kt @@ -0,0 +1,22 @@ +package com.tangem.domain.quotes.single + +import com.tangem.domain.core.flow.FlowFetcher +import com.tangem.domain.tokens.model.CryptoCurrency + +/** + * Fetcher of quote for [CryptoCurrency.RawID] + * +[REDACTED_AUTHOR] + */ +interface SingleQuoteFetcher : FlowFetcher { + + /** + * Params + * + * @property rawCurrencyId crypto currency id + */ + data class Params( + val rawCurrencyId: CryptoCurrency.RawID, + val appCurrencyId: String?, + ) +} \ No newline at end of file From 5ddc091e5655415a48f9fb2ac5493cb121d22b62 Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 12 May 2025 15:07:38 +0400 Subject: [PATCH 015/165] Updated on 2026-08-14 --- .../entity/approve/WcApproveTransactionUM.kt | 4 ---- .../entity/common/WcNetworkInfoUM.kt | 2 -- .../common/WcTransactionRequestInfoUM.kt | 4 ---- .../entity/sign/WcSignTransactionUM.kt | 3 --- .../ui/approve/WcSpendAllowanceItem.kt | 20 ++++++++++++------- .../transaction/ui/common/WcNetworkFeeItem.kt | 16 ++++++++++----- ...cSignTransactionModalBottomSheetContent.kt | 20 +++++++++---------- 7 files changed, 34 insertions(+), 35 deletions(-) diff --git a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/entity/approve/WcApproveTransactionUM.kt b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/entity/approve/WcApproveTransactionUM.kt index f899662751..42c283e2e1 100644 --- a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/entity/approve/WcApproveTransactionUM.kt +++ b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/entity/approve/WcApproveTransactionUM.kt @@ -1,20 +1,17 @@ package com.tangem.features.walletconnect.transaction.entity.approve import androidx.annotation.DrawableRes -import androidx.compose.runtime.Immutable import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfigContent import com.tangem.features.walletconnect.transaction.entity.common.WcNetworkInfoUM import com.tangem.features.walletconnect.transaction.entity.common.WcTransactionAppInfoContentUM import com.tangem.features.walletconnect.transaction.entity.common.WcTransactionRequestInfoUM -@Immutable internal data class WcApproveTransactionUM( val transaction: WcApproveTransactionItemUM, val transactionRequestInfo: WcTransactionRequestInfoUM, val customAllowance: WcCustomAllowanceUM, ) -@Immutable internal data class WcApproveTransactionItemUM( val onDismiss: () -> Unit, val onSend: () -> Unit, @@ -26,7 +23,6 @@ internal data class WcApproveTransactionItemUM( val isLoading: Boolean = false, ) : TangemBottomSheetConfigContent -@Immutable internal data class WcCustomAllowanceUM( @DrawableRes val networkIconRes: Int, val tokenIconUrl: String, diff --git a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/entity/common/WcNetworkInfoUM.kt b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/entity/common/WcNetworkInfoUM.kt index e0706d7cd7..788036d8fd 100644 --- a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/entity/common/WcNetworkInfoUM.kt +++ b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/entity/common/WcNetworkInfoUM.kt @@ -1,9 +1,7 @@ package com.tangem.features.walletconnect.transaction.entity.common import androidx.annotation.DrawableRes -import androidx.compose.runtime.Immutable -@Immutable internal data class WcNetworkInfoUM( val name: String, @DrawableRes val iconRes: Int, diff --git a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/entity/common/WcTransactionRequestInfoUM.kt b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/entity/common/WcTransactionRequestInfoUM.kt index 946ace922b..37bf3b7fde 100644 --- a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/entity/common/WcTransactionRequestInfoUM.kt +++ b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/entity/common/WcTransactionRequestInfoUM.kt @@ -1,22 +1,18 @@ package com.tangem.features.walletconnect.transaction.entity.common -import androidx.compose.runtime.Immutable import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfigContent import com.tangem.core.ui.extensions.TextReference import kotlinx.collections.immutable.ImmutableList -@Immutable internal data class WcTransactionRequestInfoUM( val blocks: ImmutableList, val onCopy: () -> Unit, ) : TangemBottomSheetConfigContent -@Immutable internal data class WcTransactionRequestBlockUM( val info: ImmutableList, ) -@Immutable internal data class WcTransactionRequestInfoItemUM( val title: TextReference, val description: String = "", diff --git a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/entity/sign/WcSignTransactionUM.kt b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/entity/sign/WcSignTransactionUM.kt index 3487650bd4..48a1bc062e 100644 --- a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/entity/sign/WcSignTransactionUM.kt +++ b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/entity/sign/WcSignTransactionUM.kt @@ -1,18 +1,15 @@ package com.tangem.features.walletconnect.transaction.entity.sign -import androidx.compose.runtime.Immutable import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfigContent import com.tangem.features.walletconnect.transaction.entity.common.WcNetworkInfoUM import com.tangem.features.walletconnect.transaction.entity.common.WcTransactionAppInfoContentUM import com.tangem.features.walletconnect.transaction.entity.common.WcTransactionRequestInfoUM -@Immutable internal data class WcSignTransactionUM( val transaction: WcSignTransactionItemUM, val transactionRequestInfo: WcTransactionRequestInfoUM, ) -@Immutable internal data class WcSignTransactionItemUM( val onDismiss: () -> Unit, val onSign: () -> Unit, diff --git a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/ui/approve/WcSpendAllowanceItem.kt b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/ui/approve/WcSpendAllowanceItem.kt index b6be7f15e0..329402c6cd 100644 --- a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/ui/approve/WcSpendAllowanceItem.kt +++ b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/ui/approve/WcSpendAllowanceItem.kt @@ -1,5 +1,6 @@ package com.tangem.features.walletconnect.transaction.ui.approve +import androidx.compose.animation.AnimatedContent import androidx.compose.foundation.background import androidx.compose.foundation.layout.* import androidx.compose.foundation.shape.CircleShape @@ -12,8 +13,8 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import com.tangem.features.walletconnect.impl.R import androidx.compose.ui.res.painterResource -import androidx.compose.ui.res.stringResource import coil.compose.AsyncImage +import com.tangem.core.ui.extensions.stringResourceSafe import com.tangem.core.ui.res.TangemTheme import com.tangem.features.walletconnect.transaction.entity.approve.WcSpendAllowanceUM import com.tangem.features.walletconnect.transaction.ui.common.WcSmallTitleItem @@ -52,11 +53,16 @@ internal fun WcSpendAllowanceItem(spendAllowance: WcSpendAllowanceUM, modifier: Spacer(modifier = Modifier.width(TangemTheme.dimens.spacing12)) - Text( - text = spendAllowance.amountText, - style = TangemTheme.typography.body1, - color = TangemTheme.colors.text.primary1, - ) + AnimatedContent( + targetState = spendAllowance.amountText, + label = "Animate Spend allowance text", + ) { amount -> + Text( + text = amount, + style = TangemTheme.typography.body1, + color = TangemTheme.colors.text.primary1, + ) + } } Row( @@ -64,7 +70,7 @@ internal fun WcSpendAllowanceItem(spendAllowance: WcSpendAllowanceUM, modifier: horizontalArrangement = Arrangement.End, ) { Text( - text = stringResource(R.string.manage_tokens_edit), + text = stringResourceSafe(R.string.manage_tokens_edit), style = TangemTheme.typography.body1, color = TangemTheme.colors.text.tertiary, ) diff --git a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/ui/common/WcNetworkFeeItem.kt b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/ui/common/WcNetworkFeeItem.kt index bb4c1867c5..df6080b3e4 100644 --- a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/ui/common/WcNetworkFeeItem.kt +++ b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/ui/common/WcNetworkFeeItem.kt @@ -1,5 +1,6 @@ package com.tangem.features.walletconnect.transaction.ui.common +import androidx.compose.animation.AnimatedContent import androidx.compose.foundation.layout.* import androidx.compose.foundation.shape.CircleShape import androidx.compose.material3.Icon @@ -48,11 +49,16 @@ internal fun WcNetworkFeeItem(networkFeeText: String, modifier: Modifier = Modif verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.End, ) { - Text( - text = networkFeeText, - style = TangemTheme.typography.body1, - color = TangemTheme.colors.text.tertiary, - ) + AnimatedContent( + targetState = networkFeeText, + label = "Animate Network fee text", + ) { fee -> + Text( + text = fee, + style = TangemTheme.typography.body1, + color = TangemTheme.colors.text.tertiary, + ) + } Spacer(modifier = Modifier.width(TangemTheme.dimens.spacing6)) Icon( modifier = Modifier diff --git a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/ui/sign/WcSignTransactionModalBottomSheetContent.kt b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/ui/sign/WcSignTransactionModalBottomSheetContent.kt index 838f9f4786..4596bcc2b6 100644 --- a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/ui/sign/WcSignTransactionModalBottomSheetContent.kt +++ b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/ui/sign/WcSignTransactionModalBottomSheetContent.kt @@ -38,11 +38,11 @@ internal fun WcSignTransactionModalBottomSheetContent( ) { Column( modifier = Modifier - .padding(horizontal = TangemTheme.dimens.spacing16), + .padding(horizontal = 16.dp), ) { Column( modifier = Modifier - .clip(RoundedCornerShape(TangemTheme.dimens.radius14)) + .clip(RoundedCornerShape(14.dp)) .background(color = TangemTheme.colors.background.action) .fillMaxWidth() .animateContentSize(), @@ -60,13 +60,13 @@ internal fun WcSignTransactionModalBottomSheetContent( modifier = Modifier .fillMaxWidth() .clickable { onClickTransactionRequest() } - .padding(TangemTheme.dimens.spacing12), + .padding(12.dp), ) } - Column(modifier = Modifier.padding(top = TangemTheme.dimens.spacing16)) { + Column(modifier = Modifier.padding(top = 16.dp)) { WcSignTransactionItems(state) WcTransactionRequestButtons( - modifier = Modifier.padding(vertical = TangemTheme.dimens.spacing16), + modifier = Modifier.padding(vertical = 16.dp), onDismiss = state.onDismiss, onClickActiveButton = state.onSign, activeButtonText = resourceReference(R.string.common_sign), @@ -80,27 +80,27 @@ internal fun WcSignTransactionModalBottomSheetContent( private fun WcSignTransactionItems(state: WcSignTransactionItemUM) { Column( modifier = Modifier - .clip(RoundedCornerShape(TangemTheme.dimens.radius14)) + .clip(RoundedCornerShape(14.dp)) .background(color = TangemTheme.colors.background.action) .fillMaxWidth() .animateContentSize(), ) { val itemsModifier = Modifier .fillMaxWidth() - .padding(TangemTheme.dimens.spacing12) + .padding(12.dp) DividerWithPadding(start = 0.dp, end = 0.dp) WcWalletItem( modifier = itemsModifier, walletName = state.walletName, ) - DividerWithPadding(start = TangemTheme.dimens.spacing40, end = TangemTheme.dimens.spacing12) + DividerWithPadding(start = 40.dp, end = 12.dp) WcNetworkItem( modifier = itemsModifier, networkInfo = state.networkInfo, ) if (!state.addressText.isNullOrEmpty()) { - DividerWithPadding(start = TangemTheme.dimens.spacing40, end = TangemTheme.dimens.spacing12) + DividerWithPadding(start = 40.dp, end = 12.dp) WcAddressItem( modifier = itemsModifier, addressText = state.addressText, @@ -116,7 +116,7 @@ internal fun DividerWithPadding(start: Dp, end: Dp) { start = start, end = end, ), - thickness = TangemTheme.dimens.size1, + thickness = 1.dp, color = TangemTheme.colors.stroke.primary, ) } From a6e40999354d3052c21ebe10ec13ac6eeed34d48 Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 12 May 2025 18:42:37 +0500 Subject: [PATCH 016/165] Updated on 2026-08-14 --- .../tap/network/auth/DefaultAuthProvider.kt | 6 ++ .../datasource/api/common/AuthProvider.kt | 5 ++ .../api/tangemTech/models/CardInfoBody.kt | 10 +++ .../api/tangemTech/models/WalletIdBody.kt | 2 + .../DefaultNotificationsRepository.kt | 11 --- .../DefaultNotificationsRepositoryTest.kt | 19 ----- data/wallets/build.gradle.kts | 1 + .../data/wallets/DefaultWalletsRepository.kt | 20 +++++ .../converters/WalletIdBodyConverter.kt | 21 +++++ .../data/wallets/di/WalletsDataModule.kt | 3 + .../wallets/DefaultWalletsRepositoryTest.kt | 72 +++++++++++++++++ .../converters/WalletIdBodyConverterTest.kt | 80 +++++++++++++++++++ .../repository/NotificationsRepository.kt | 3 - .../DefaultUserWalletsSyncDelegate.kt | 1 + .../wallets/repository/WalletsRepository.kt | 4 + ...ssociateWalletsWithApplicationIdUseCase.kt | 17 ++++ 16 files changed, 242 insertions(+), 33 deletions(-) create mode 100644 core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/models/CardInfoBody.kt create mode 100644 data/wallets/src/main/java/com/tangem/data/wallets/converters/WalletIdBodyConverter.kt create mode 100644 data/wallets/src/test/java/com/tangem/data/wallets/converters/WalletIdBodyConverterTest.kt create mode 100644 domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/AssociateWalletsWithApplicationIdUseCase.kt diff --git a/app/src/main/java/com/tangem/tap/network/auth/DefaultAuthProvider.kt b/app/src/main/java/com/tangem/tap/network/auth/DefaultAuthProvider.kt index c39150c2c2..be40dc6f87 100644 --- a/app/src/main/java/com/tangem/tap/network/auth/DefaultAuthProvider.kt +++ b/app/src/main/java/com/tangem/tap/network/auth/DefaultAuthProvider.kt @@ -13,4 +13,10 @@ internal class DefaultAuthProvider(private val userWalletsListManager: UserWalle override fun getCardId(): String { return userWalletsListManager.selectedUserWalletSync?.scanResponse?.card?.cardId ?: "" } + + override fun getCardsPublicKeys(): Map { + return userWalletsListManager.userWalletsSync.associate { + it.scanResponse.card.cardId to it.scanResponse.card.cardPublicKey.toHexString() + } + } } \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/common/AuthProvider.kt b/core/datasource/src/main/java/com/tangem/datasource/api/common/AuthProvider.kt index a11b7bf574..7945fc59eb 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/common/AuthProvider.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/common/AuthProvider.kt @@ -11,4 +11,9 @@ interface AuthProvider { fun getCardPublicKey(): String fun getCardId(): String + + /** + * Returns map where keys(cardId) associated with cardPublicKey + */ + fun getCardsPublicKeys(): Map } \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/models/CardInfoBody.kt b/core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/models/CardInfoBody.kt new file mode 100644 index 0000000000..80454a733b --- /dev/null +++ b/core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/models/CardInfoBody.kt @@ -0,0 +1,10 @@ +package com.tangem.datasource.api.tangemTech.models + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +@JsonClass(generateAdapter = true) +data class CardInfoBody( + @Json(name = "card_id") val cardId: String, + @Json(name = "card_public_key") val cardPublicKey: String, +) \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/models/WalletIdBody.kt b/core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/models/WalletIdBody.kt index dbadddd6fc..3fd5ff6196 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/models/WalletIdBody.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/models/WalletIdBody.kt @@ -6,4 +6,6 @@ import com.squareup.moshi.JsonClass @JsonClass(generateAdapter = true) data class WalletIdBody( @Json(name = "id") val walletId: String, + @Json(name = "name") val name: String, + @Json(name = "cards") val cards: List, ) \ No newline at end of file diff --git a/data/notifications/src/main/java/com/tangem/data/notifications/DefaultNotificationsRepository.kt b/data/notifications/src/main/java/com/tangem/data/notifications/DefaultNotificationsRepository.kt index 62f837994d..d0ea5837e7 100644 --- a/data/notifications/src/main/java/com/tangem/data/notifications/DefaultNotificationsRepository.kt +++ b/data/notifications/src/main/java/com/tangem/data/notifications/DefaultNotificationsRepository.kt @@ -4,7 +4,6 @@ import com.tangem.data.notifications.converters.NotificationsEligibleNetworkConv import com.tangem.datasource.api.common.response.getOrThrow import com.tangem.datasource.api.tangemTech.TangemTechApi import com.tangem.datasource.api.tangemTech.models.NotificationApplicationCreateBody -import com.tangem.datasource.api.tangemTech.models.WalletIdBody import com.tangem.utils.info.AppInfoProvider import com.tangem.datasource.local.preferences.AppPreferencesStore import com.tangem.datasource.local.preferences.PreferencesKeys @@ -62,16 +61,6 @@ internal class DefaultNotificationsRepository @Inject constructor( ) } - override suspend fun associateApplicationIdWithWallets(appId: String, wallets: List) = - withContext(dispatchers.io) { - tangemTechApi.associateApplicationIdWithWallets( - applicationId = appId, - body = wallets.map { - WalletIdBody(it) - }, - ).getOrThrow() - } - override suspend fun sendPushToken(appId: ApplicationId, pushToken: String) { withContext(dispatchers.io) { tangemTechApi.updatePushTokenForApplicationId( diff --git a/data/notifications/src/test/java/com/tangem/data/notifications/DefaultNotificationsRepositoryTest.kt b/data/notifications/src/test/java/com/tangem/data/notifications/DefaultNotificationsRepositoryTest.kt index 57d8d4c429..691dff0714 100644 --- a/data/notifications/src/test/java/com/tangem/data/notifications/DefaultNotificationsRepositoryTest.kt +++ b/data/notifications/src/test/java/com/tangem/data/notifications/DefaultNotificationsRepositoryTest.kt @@ -104,25 +104,6 @@ class DefaultNotificationsRepositoryTest { assertThat(result).isEqualTo(expectedAppId) } - @Test - fun `GIVEN application id and wallet list WHEN associateApplicationIdWithWallets THEN associates them`() = runTest { - // GIVEN - val appId = "test-app-id" - val wallets = listOf("wallet1", "wallet2") - coEvery { - tangemTechApi.associateApplicationIdWithWallets( - appId, - wallets.map { WalletIdBody(it) }, - ) - } returns ApiResponse.Success(Unit) - - // WHEN - repository.associateApplicationIdWithWallets(appId, wallets) - - // THEN - coVerify { tangemTechApi.associateApplicationIdWithWallets(appId, wallets.map { WalletIdBody(it) }) } - } - @Test fun `GIVEN application id and push token WHEN sendPushToken THEN updates push token`() = runTest { // GIVEN diff --git a/data/wallets/build.gradle.kts b/data/wallets/build.gradle.kts index e84f33b3bc..a9ca9af06f 100644 --- a/data/wallets/build.gradle.kts +++ b/data/wallets/build.gradle.kts @@ -36,6 +36,7 @@ dependencies { implementation(deps.arrow.core) /** tests */ + testImplementation(projects.domain.models) testImplementation(deps.test.junit) testImplementation(deps.test.coroutine) testImplementation(deps.test.truth) diff --git a/data/wallets/src/main/java/com/tangem/data/wallets/DefaultWalletsRepository.kt b/data/wallets/src/main/java/com/tangem/data/wallets/DefaultWalletsRepository.kt index 25bc5ae0b6..1b8c62c423 100644 --- a/data/wallets/src/main/java/com/tangem/data/wallets/DefaultWalletsRepository.kt +++ b/data/wallets/src/main/java/com/tangem/data/wallets/DefaultWalletsRepository.kt @@ -1,6 +1,8 @@ package com.tangem.data.wallets import com.tangem.data.wallets.converters.UserWalletRemoteInfoConverter +import com.tangem.data.wallets.converters.WalletIdBodyConverter +import com.tangem.datasource.api.common.AuthProvider import com.tangem.datasource.api.common.response.ApiResponseError.HttpException import com.tangem.datasource.api.common.response.getOrThrow import com.tangem.datasource.api.tangemTech.TangemTechApi @@ -18,6 +20,7 @@ import com.tangem.datasource.local.preferences.utils.getSyncOrDefault import com.tangem.datasource.local.preferences.utils.store import com.tangem.datasource.local.userwallet.UserWalletsStore import com.tangem.domain.wallets.models.SeedPhraseNotificationsStatus +import com.tangem.domain.wallets.models.UserWallet import com.tangem.domain.wallets.models.UserWalletId import com.tangem.domain.wallets.models.UserWalletRemoteInfo import com.tangem.domain.wallets.repository.WalletsRepository @@ -37,6 +40,7 @@ internal class DefaultWalletsRepository( private val userWalletsStore: UserWalletsStore, private val seedPhraseNotificationVisibilityStore: RuntimeStateStore, private val dispatchers: CoroutineDispatcherProvider, + private val authProvider: AuthProvider, ) : WalletsRepository { override suspend fun shouldSaveUserWalletsSync(): Boolean { @@ -288,6 +292,22 @@ internal class DefaultWalletsRepository( } } + override suspend fun associateWallets(applicationId: String, wallets: List) = + withContext(dispatchers.io) { + val publicKeys = authProvider.getCardsPublicKeys() + val walletsBody = wallets.map { userWallet -> + WalletIdBodyConverter.convert( + userWallet = userWallet, + publicKeys = publicKeys.filterKeys { userWallet.cardsInWallet.contains(it) }, + ) + } + + tangemTechApi.associateApplicationIdWithWallets( + applicationId = applicationId, + body = walletsBody, + ).getOrThrow() + } + private suspend fun loadAndSaveNotificationsEnabled(userWalletId: UserWalletId): Boolean { val walletResponse = tangemTechApi.getWalletById(walletId = userWalletId.stringValue).getOrThrow() val isEnabled = walletResponse.notifyStatus diff --git a/data/wallets/src/main/java/com/tangem/data/wallets/converters/WalletIdBodyConverter.kt b/data/wallets/src/main/java/com/tangem/data/wallets/converters/WalletIdBodyConverter.kt new file mode 100644 index 0000000000..fdf8d28b3b --- /dev/null +++ b/data/wallets/src/main/java/com/tangem/data/wallets/converters/WalletIdBodyConverter.kt @@ -0,0 +1,21 @@ +package com.tangem.data.wallets.converters + +import com.tangem.datasource.api.tangemTech.models.CardInfoBody +import com.tangem.datasource.api.tangemTech.models.WalletIdBody +import com.tangem.domain.wallets.models.UserWallet + +internal object WalletIdBodyConverter { + + fun convert(userWallet: UserWallet, publicKeys: Map): WalletIdBody { + return WalletIdBody( + walletId = userWallet.walletId.stringValue, + name = userWallet.name, + cards = publicKeys.map { + CardInfoBody( + cardId = it.key, + cardPublicKey = it.value, + ) + }, + ) + } +} \ No newline at end of file diff --git a/data/wallets/src/main/java/com/tangem/data/wallets/di/WalletsDataModule.kt b/data/wallets/src/main/java/com/tangem/data/wallets/di/WalletsDataModule.kt index 778257f353..a154dc4f99 100644 --- a/data/wallets/src/main/java/com/tangem/data/wallets/di/WalletsDataModule.kt +++ b/data/wallets/src/main/java/com/tangem/data/wallets/di/WalletsDataModule.kt @@ -2,6 +2,7 @@ package com.tangem.data.wallets.di import com.tangem.data.wallets.DefaultWalletNamesMigrationRepository import com.tangem.data.wallets.DefaultWalletsRepository +import com.tangem.datasource.api.common.AuthProvider import com.tangem.datasource.api.tangemTech.TangemTechApi import com.tangem.datasource.local.datastore.RuntimeStateStore import com.tangem.datasource.local.preferences.AppPreferencesStore @@ -26,6 +27,7 @@ internal object WalletsDataModule { tangemTechApi: TangemTechApi, userWalletsStore: UserWalletsStore, dispatchers: CoroutineDispatcherProvider, + authProvider: AuthProvider, ): WalletsRepository { return DefaultWalletsRepository( appPreferencesStore = appPreferencesStore, @@ -33,6 +35,7 @@ internal object WalletsDataModule { userWalletsStore = userWalletsStore, seedPhraseNotificationVisibilityStore = RuntimeStateStore(defaultValue = emptyMap()), dispatchers = dispatchers, + authProvider = authProvider, ) } diff --git a/data/wallets/src/test/java/com/tangem/data/wallets/DefaultWalletsRepositoryTest.kt b/data/wallets/src/test/java/com/tangem/data/wallets/DefaultWalletsRepositoryTest.kt index f430dab62f..af1295b684 100644 --- a/data/wallets/src/test/java/com/tangem/data/wallets/DefaultWalletsRepositoryTest.kt +++ b/data/wallets/src/test/java/com/tangem/data/wallets/DefaultWalletsRepositoryTest.kt @@ -18,6 +18,8 @@ import io.mockk.coVerify import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.test.runTest import com.google.common.truth.Truth.assertThat +import com.tangem.datasource.api.common.AuthProvider +import com.tangem.domain.wallets.models.UserWallet import org.junit.Before import org.junit.Test @@ -48,6 +50,7 @@ class DefaultWalletsRepositoryTest { userWalletsStore = mockk(), seedPhraseNotificationVisibilityStore = mockk(), dispatchers = dispatchers, + authProvider = mockk(), ) } @@ -231,4 +234,73 @@ 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" + + val userWallets = listOf( + mockk { + every { cardsInWallet } returns setOf(card1PublicKey) + every { walletId } returns UserWalletId(wallet1Id) + every { name } returns "Wallet 1" + }, + mockk { + every { cardsInWallet } returns setOf(card2PublicKey) + every { walletId } returns UserWalletId(wallet2Id) + every { name } returns "Wallet 2" + }, + ) + + val publicKeys = mapOf( + card1PublicKey to "public_key_1", + card2PublicKey to "public_key_2", + ) + + val authProvider = mockk { + every { getCardsPublicKeys() } returns publicKeys + } + + repository = DefaultWalletsRepository( + appPreferencesStore = appPreferenceStore, + tangemTechApi = tangemTechApi, + userWalletsStore = mockk(), + seedPhraseNotificationVisibilityStore = mockk(), + dispatchers = dispatchers, + authProvider = authProvider, + ) + + coEvery { + tangemTechApi.associateApplicationIdWithWallets( + eq(applicationId), + any(), + ) + } returns ApiResponse.Success(Unit) + + // WHEN + repository.associateWallets(applicationId, userWallets) + + // THEN + coVerify(exactly = 1) { + tangemTechApi.associateApplicationIdWithWallets( + eq(applicationId), + 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" + } + }, + ) + } + } } \ No newline at end of file diff --git a/data/wallets/src/test/java/com/tangem/data/wallets/converters/WalletIdBodyConverterTest.kt b/data/wallets/src/test/java/com/tangem/data/wallets/converters/WalletIdBodyConverterTest.kt new file mode 100644 index 0000000000..2aa327b6d9 --- /dev/null +++ b/data/wallets/src/test/java/com/tangem/data/wallets/converters/WalletIdBodyConverterTest.kt @@ -0,0 +1,80 @@ +package com.tangem.data.wallets.converters + +import com.tangem.datasource.api.tangemTech.models.CardInfoBody +import com.tangem.datasource.api.tangemTech.models.WalletIdBody +import com.tangem.domain.wallets.models.UserWallet +import com.tangem.domain.wallets.models.UserWalletId +import com.google.common.truth.Truth.assertThat +import io.mockk.mockk +import org.junit.Test + +class WalletIdBodyConverterTest { + + @Test + fun `GIVEN user wallet with cards WHEN convert THEN should return correct WalletIdBody`() { + // GIVEN + val walletId = UserWalletId("1234567890abcdef") + val walletName = "Test Wallet" + val userWallet = UserWallet( + walletId = walletId, + name = walletName, + cardsInWallet = setOf("card1", "card2"), + isMultiCurrency = true, + hasBackupError = false, + scanResponse = mockk(), + ) + val publicKeys = mapOf( + "card1" to "public_key_1", + "card2" to "public_key_2", + ) + + // WHEN + val result = WalletIdBodyConverter.convert(userWallet, publicKeys) + + // THEN + assertThat(result).isEqualTo( + WalletIdBody( + walletId = walletId.stringValue, + name = walletName, + cards = listOf( + CardInfoBody( + cardId = "card1", + cardPublicKey = "public_key_1", + ), + CardInfoBody( + cardId = "card2", + cardPublicKey = "public_key_2", + ), + ), + ), + ) + } + + @Test + fun `GIVEN user wallet without cards WHEN convert THEN should return WalletIdBody with empty cards list`() { + // GIVEN + val walletId = UserWalletId("1234567890abcdef") + val walletName = "Test Wallet" + val userWallet = UserWallet( + walletId = walletId, + name = walletName, + cardsInWallet = emptySet(), + isMultiCurrency = true, + hasBackupError = false, + scanResponse = mockk(), + ) + val publicKeys = emptyMap() + + // WHEN + val result = WalletIdBodyConverter.convert(userWallet, publicKeys) + + // THEN + assertThat(result).isEqualTo( + WalletIdBody( + walletId = walletId.stringValue, + name = walletName, + cards = emptyList(), + ), + ) + } +} \ No newline at end of file diff --git a/domain/notifications/src/main/java/com/tangem/domain/notifications/repository/NotificationsRepository.kt b/domain/notifications/src/main/java/com/tangem/domain/notifications/repository/NotificationsRepository.kt index d5e99ab745..a8c46854df 100644 --- a/domain/notifications/src/main/java/com/tangem/domain/notifications/repository/NotificationsRepository.kt +++ b/domain/notifications/src/main/java/com/tangem/domain/notifications/repository/NotificationsRepository.kt @@ -16,9 +16,6 @@ interface NotificationsRepository { suspend fun incrementTronTokenFeeNotificationShowCounter() - @Throws - suspend fun associateApplicationIdWithWallets(appId: String, wallets: List) - @Throws suspend fun sendPushToken(appId: ApplicationId, pushToken: String) diff --git a/domain/wallets/src/main/java/com/tangem/domain/wallets/delegate/DefaultUserWalletsSyncDelegate.kt b/domain/wallets/src/main/java/com/tangem/domain/wallets/delegate/DefaultUserWalletsSyncDelegate.kt index b1c1e11c7e..37ab17d016 100644 --- a/domain/wallets/src/main/java/com/tangem/domain/wallets/delegate/DefaultUserWalletsSyncDelegate.kt +++ b/domain/wallets/src/main/java/com/tangem/domain/wallets/delegate/DefaultUserWalletsSyncDelegate.kt @@ -27,6 +27,7 @@ class DefaultUserWalletsSyncDelegate( } } + // TODO remove dispatchers whnen UserWalletsListManager will be main safe private suspend fun renameUserWallet( userWalletId: UserWalletId, name: String, diff --git a/domain/wallets/src/main/java/com/tangem/domain/wallets/repository/WalletsRepository.kt b/domain/wallets/src/main/java/com/tangem/domain/wallets/repository/WalletsRepository.kt index 90768f5e78..eaf24f6cfa 100644 --- a/domain/wallets/src/main/java/com/tangem/domain/wallets/repository/WalletsRepository.kt +++ b/domain/wallets/src/main/java/com/tangem/domain/wallets/repository/WalletsRepository.kt @@ -1,6 +1,7 @@ package com.tangem.domain.wallets.repository import com.tangem.domain.wallets.models.SeedPhraseNotificationsStatus +import com.tangem.domain.wallets.models.UserWallet import com.tangem.domain.wallets.models.UserWalletId import com.tangem.domain.wallets.models.UserWalletRemoteInfo import kotlinx.coroutines.flow.Flow @@ -54,4 +55,7 @@ interface WalletsRepository { @Throws suspend fun getWalletsInfo(applicationId: String, updateCache: Boolean = true): List + + @Throws + suspend fun associateWallets(applicationId: String, wallets: List) } \ No newline at end of file diff --git a/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/AssociateWalletsWithApplicationIdUseCase.kt b/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/AssociateWalletsWithApplicationIdUseCase.kt new file mode 100644 index 0000000000..e67bf5b744 --- /dev/null +++ b/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/AssociateWalletsWithApplicationIdUseCase.kt @@ -0,0 +1,17 @@ +package com.tangem.domain.wallets.usecase + +import arrow.core.Either +import com.tangem.domain.notifications.models.ApplicationId +import com.tangem.domain.wallets.legacy.UserWalletsListManager +import com.tangem.domain.wallets.repository.WalletsRepository + +class AssociateWalletsWithApplicationIdUseCase( + private val userWalletsListManager: UserWalletsListManager, + private val walletsRepository: WalletsRepository, +) { + + suspend operator fun invoke(applicationId: ApplicationId): Either = Either.catch { + val wallets = userWalletsListManager.userWalletsSync + walletsRepository.associateWallets(applicationId.value, wallets) + } +} \ No newline at end of file From 08da4aaba926eaaab2bbc25d8ea883fbfc0dcf8f Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 12 May 2025 14:12:13 +0000 Subject: [PATCH 017/165] Updated on 2026-08-14 --- gradle/tangem_dependencies.toml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/gradle/tangem_dependencies.toml b/gradle/tangem_dependencies.toml index ef3a5a7696..9e58bcf67c 100644 --- a/gradle/tangem_dependencies.toml +++ b/gradle/tangem_dependencies.toml @@ -5,9 +5,9 @@ # https://github.com/tangem/tangem-sdk-android/ # https://github.com/tangem/vico -tangemBlockchainSdk = "releases-5.24.0-1062" +tangemBlockchainSdk = "develop-1052" #tangemBlockchainSdk = "0.0.1" # Keep it! - used for local builds -tangemCardSdk = "releases-5.24.0-467" +tangemCardSdk = "develop-468" #tangemCardSdk = "0.0.1" # Keep it! - used for local builds ^ tangemVico = "2.0.0-alpha.25-tangem12" #tangemVico = "0.0.1" # Keep it! - used for local builds ^ From f5d33821d50d7067f9daf69a366dc35a5a6f6d21 Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 12 May 2025 17:20:21 +0300 Subject: [PATCH 018/165] Updated on 2026-08-14 --- gradle/tangem_dependencies.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/tangem_dependencies.toml b/gradle/tangem_dependencies.toml index 9e58bcf67c..ccc406b5ff 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-1052" +tangemBlockchainSdk = "develop-1063" #tangemBlockchainSdk = "0.0.1" # Keep it! - used for local builds tangemCardSdk = "develop-468" #tangemCardSdk = "0.0.1" # Keep it! - used for local builds ^ From ad8d026e400592227bf34e69c4b8b23838bc4009 Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 7 May 2025 15:11:03 +0500 Subject: [PATCH 019/165] Updated on 2026-08-14 --- .../datasource/local/nft/custom/NFTPriceKeyValue.kt | 8 ++++++++ 1 file changed, 8 insertions(+) create mode 100644 core/datasource/src/main/java/com/tangem/datasource/local/nft/custom/NFTPriceKeyValue.kt diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/nft/custom/NFTPriceKeyValue.kt b/core/datasource/src/main/java/com/tangem/datasource/local/nft/custom/NFTPriceKeyValue.kt new file mode 100644 index 0000000000..99d24825e5 --- /dev/null +++ b/core/datasource/src/main/java/com/tangem/datasource/local/nft/custom/NFTPriceKeyValue.kt @@ -0,0 +1,8 @@ +package com.tangem.datasource.local.nft.custom + +import com.tangem.blockchain.nft.models.NFTAsset + +data class NFTPriceKeyValue( + val key: NFTAsset.Identifier, + val value: NFTAsset.SalePrice, +) \ No newline at end of file From 4b4b1401825c2a011581fb3bed25660d94d91cdf Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 7 May 2025 15:44:38 +0500 Subject: [PATCH 020/165] Updated on 2026-08-14 --- .../tangem/tap/di/domain/NFTDomainModule.kt | 20 ++++ .../local/nft/DefaultNFTPersistenceStore.kt | 2 +- .../NFTSdkAssetSalePriceConverter.kt | 7 +- .../tangem/data/nft/DefaultNFTRepository.kt | 100 +++++++++++++----- domain/nft/build.gradle.kts | 1 + .../tangem/domain/nft/models/NFTSalePrice.kt | 2 + .../tangem/domain/nft/FetchNFTPriceUseCase.kt | 26 +++++ .../tangem/domain/nft/GetNFTPriceUseCase.kt | 44 ++++++++ .../domain/nft/repository/NFTRepository.kt | 11 ++ 9 files changed, 181 insertions(+), 32 deletions(-) create mode 100644 domain/nft/src/main/kotlin/com/tangem/domain/nft/FetchNFTPriceUseCase.kt create mode 100644 domain/nft/src/main/kotlin/com/tangem/domain/nft/GetNFTPriceUseCase.kt diff --git a/app/src/main/java/com/tangem/tap/di/domain/NFTDomainModule.kt b/app/src/main/java/com/tangem/tap/di/domain/NFTDomainModule.kt index 090a030029..1a79b6209b 100644 --- a/app/src/main/java/com/tangem/tap/di/domain/NFTDomainModule.kt +++ b/app/src/main/java/com/tangem/tap/di/domain/NFTDomainModule.kt @@ -2,6 +2,8 @@ package com.tangem.tap.di.domain import com.tangem.domain.nft.* import com.tangem.domain.nft.repository.NFTRepository +import com.tangem.domain.quotes.single.SingleQuoteFetcher +import com.tangem.domain.quotes.single.SingleQuoteSupplier import com.tangem.domain.tokens.repository.CurrenciesRepository import com.tangem.domain.tokens.repository.NetworksRepository import com.tangem.utils.coroutines.CoroutineDispatcherProvider @@ -83,4 +85,22 @@ internal object NFTDomainModule { GetNFTExploreUrlUseCase( nftRepository = nftRepository, ) + + @Provides + @Singleton + fun provideGetNFTPriceUseCase( + nftRepository: NFTRepository, + singleQuoteSupplier: SingleQuoteSupplier, + ): GetNFTPriceUseCase { + return GetNFTPriceUseCase(nftRepository, singleQuoteSupplier) + } + + @Provides + @Singleton + fun provideFetchNFTPriceUseCase( + nftRepository: NFTRepository, + singleQuoteFetcher: SingleQuoteFetcher, + ): FetchNFTPriceUseCase { + return FetchNFTPriceUseCase(nftRepository, singleQuoteFetcher) + } } \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/nft/DefaultNFTPersistenceStore.kt b/core/datasource/src/main/java/com/tangem/datasource/local/nft/DefaultNFTPersistenceStore.kt index 42d11c055f..51b17b2bbf 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/local/nft/DefaultNFTPersistenceStore.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/local/nft/DefaultNFTPersistenceStore.kt @@ -43,7 +43,7 @@ internal class DefaultNFTPersistenceStore( override suspend fun saveSalePrice(assetId: NFTAsset.Identifier, salePrice: NFTAsset.SalePrice) { pricesPersistenceStore.updateData { - it.toMutableList().plus(NFTPriceId(assetId = assetId, price = salePrice)) + it.toMutableList() + NFTPriceId(assetId = assetId, price = salePrice) } } diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/nft/converter/NFTSdkAssetSalePriceConverter.kt b/core/datasource/src/main/java/com/tangem/datasource/local/nft/converter/NFTSdkAssetSalePriceConverter.kt index 252753005a..f0741130bd 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/local/nft/converter/NFTSdkAssetSalePriceConverter.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/local/nft/converter/NFTSdkAssetSalePriceConverter.kt @@ -5,14 +5,16 @@ import com.tangem.domain.nft.models.NFTSalePrice import com.tangem.utils.converter.TwoWayConverter import com.tangem.blockchain.nft.models.NFTAsset.SalePrice as SDKSalePrice -internal class NFTSdkAssetSalePriceConverter( +class NFTSdkAssetSalePriceConverter( private val assetId: NFTAsset.Identifier, ) : TwoWayConverter { override fun convert(value: SDKSalePrice): NFTSalePrice.Value { return NFTSalePrice.Value( assetId = assetId, + fiatValue = null, value = value.value, - symbol = value.symbol, + symbol = value.symbol.orEmpty(), + decimals = value.decimals ?: 0, ) } @@ -20,6 +22,7 @@ internal class NFTSdkAssetSalePriceConverter( return SDKSalePrice( symbol = value.symbol, value = value.value, + decimals = value.decimals, ) } } \ No newline at end of file diff --git a/data/nft/src/main/kotlin/com/tangem/data/nft/DefaultNFTRepository.kt b/data/nft/src/main/kotlin/com/tangem/data/nft/DefaultNFTRepository.kt index 28b4ba6492..2747f7c697 100644 --- a/data/nft/src/main/kotlin/com/tangem/data/nft/DefaultNFTRepository.kt +++ b/data/nft/src/main/kotlin/com/tangem/data/nft/DefaultNFTRepository.kt @@ -4,12 +4,14 @@ import arrow.core.Either import com.tangem.blockchain.common.Blockchain import com.tangem.blockchainsdk.utils.ExcludedBlockchains import com.tangem.blockchainsdk.utils.fromNetworkId +import com.tangem.data.common.currency.CryptoCurrencyFactory import com.tangem.data.common.currency.getNetwork import com.tangem.datasource.local.nft.NFTPersistenceStore import com.tangem.datasource.local.nft.NFTPersistenceStoreFactory import com.tangem.datasource.local.nft.NFTRuntimeStore import com.tangem.datasource.local.nft.NFTRuntimeStoreFactory import com.tangem.datasource.local.nft.converter.NFTSdkAssetIdentifierConverter +import com.tangem.datasource.local.nft.converter.NFTSdkAssetSalePriceConverter import com.tangem.datasource.local.nft.converter.NFTSdkCollectionConverter import com.tangem.datasource.local.nft.converter.NFTSdkCollectionIdentifierConverter import com.tangem.datasource.local.userwallet.UserWalletsStore @@ -19,6 +21,7 @@ import com.tangem.domain.nft.models.NFTCollection import com.tangem.domain.nft.models.NFTCollections import com.tangem.domain.nft.models.NFTSalePrice import com.tangem.domain.nft.repository.NFTRepository +import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.domain.tokens.model.Network import com.tangem.domain.walletmanager.WalletManagersFacade import com.tangem.domain.wallets.models.UserWalletId @@ -30,7 +33,7 @@ import kotlinx.coroutines.coroutineScope import kotlinx.coroutines.flow.* import kotlinx.coroutines.joinAll import kotlinx.coroutines.launch -import java.lang.UnsupportedOperationException +import kotlinx.coroutines.withContext import java.util.concurrent.ConcurrentHashMap import javax.inject.Inject import com.tangem.blockchain.nft.models.NFTAsset as SdkNFTAsset @@ -49,13 +52,62 @@ internal class DefaultNFTRepository @Inject constructor( private val networkJobs = ConcurrentHashMap() private val collectionJobs = ConcurrentHashMap() + private val cryptoCurrencyFactory = CryptoCurrencyFactory(excludedBlockchains) private val nftRuntimeStores = ConcurrentHashMap() private val nftPersistenceStores = ConcurrentHashMap() + private val collectionIdConverter = NFTSdkCollectionIdentifierConverter + private val assetIdConverter = NFTSdkAssetIdentifierConverter + override fun observeCollections(userWalletId: UserWalletId, networks: List): Flow> = flow { emitAll(observeCollectionsInternal(userWalletId, networks)) } + override fun getNFTCurrency(network: Network): CryptoCurrency { + return cryptoCurrencyFactory.createCoin(network) + } + + override suspend fun getNFTSalePrice( + userWalletId: UserWalletId, + network: Network, + collectionId: NFTCollection.Identifier, + assetId: NFTAsset.Identifier, + ): NFTSalePrice = withContext(dispatchers.io) { + val salePriceConverter = NFTSdkAssetSalePriceConverter(assetId) + + runCatching { + saveSalePriceInRuntime(userWalletId, network, NFTSalePrice.Loading(assetId)) + + val sdkPrice = walletManagersFacade.getNFTSalePrice( + userWalletId = userWalletId, + network = network, + collectionIdentifier = collectionIdConverter.convertBack(collectionId), + assetIdentifier = assetIdConverter.convertBack(assetId), + ) + val nftCurrency = getNFTCurrency(network) + val salePrice = sdkPrice?.let { + val convertedPrice = salePriceConverter.convert(sdkPrice) + convertedPrice.copy( + value = convertedPrice.value.movePointLeft(nftCurrency.decimals), + decimals = nftCurrency.decimals, + symbol = nftCurrency.symbol, + ) + } ?: NFTSalePrice.Empty(assetId) + + saveSalePriceInRuntime(userWalletId, network, salePrice) + + sdkPrice?.let { + val sdkAssetId = assetIdConverter.convertBack(assetId) + saveSalePriceInPersistence(userWalletId, network, sdkAssetId, it) + } + + salePrice + }.getOrElse { + saveSalePriceInRuntime(userWalletId, network, NFTSalePrice.Error(assetId)) + NFTSalePrice.Error(assetId) + } + } + private suspend fun observeCollectionsInternal( userWalletId: UserWalletId, networks: List, @@ -86,7 +138,7 @@ internal class DefaultNFTRepository @Inject constructor( ) = coroutineScope { launch(dispatchers.io) { Either.catch { - val sdkCollectionId = NFTSdkCollectionIdentifierConverter.convertBack(collectionId) + val sdkCollectionId = collectionIdConverter.convertBack(collectionId) val assets = walletManagersFacade.getNFTAssets( userWalletId = userWalletId, @@ -97,7 +149,7 @@ internal class DefaultNFTRepository @Inject constructor( expireAssets(userWalletId, network, collectionId) assets.forEach { - val assetId = NFTSdkAssetIdentifierConverter.convert(it.identifier) + val assetId = assetIdConverter.convert(it.identifier) val price = getNFTRuntimeStore(userWalletId, network).getSalePriceSync(assetId) if (price is NFTSalePrice.Empty || price is NFTSalePrice.Error) { refreshSalePrice(userWalletId, network, sdkCollectionId, it.identifier) @@ -157,7 +209,7 @@ internal class DefaultNFTRepository @Inject constructor( override suspend fun getNFTExploreUrl(network: Network, assetIdentifier: NFTAsset.Identifier): String? = walletManagersFacade.getNFTExploreUrl( network = network, - assetIdentifier = NFTSdkAssetIdentifierConverter.convertBack(assetIdentifier), + assetIdentifier = assetIdConverter.convertBack(assetIdentifier), ) private suspend fun refreshCollectionsInternal( @@ -190,7 +242,7 @@ internal class DefaultNFTRepository @Inject constructor( refreshAssets( userWalletId = userWalletId, network = network, - collectionId = NFTSdkCollectionIdentifierConverter.convert(collection.identifier), + collectionId = collectionIdConverter.convert(collection.identifier), ) } } @@ -215,29 +267,16 @@ internal class DefaultNFTRepository @Inject constructor( sdkAssetId: SdkNFTAsset.Identifier, ) = coroutineScope { launch(dispatchers.io) { - val assetId = NFTSdkAssetIdentifierConverter.convert(sdkAssetId) + val assetId = assetIdConverter.convert(sdkAssetId) + val collectionId = collectionIdConverter.convert(sdkCollectionId) Either.catch { - saveSalePriceInRuntime(userWalletId, network, NFTSalePrice.Loading(assetId)) - - val sdkSalePrice = - walletManagersFacade.getNFTSalePrice(userWalletId, network, sdkCollectionId, sdkAssetId) - - val salePrice = if (sdkSalePrice == null) { - NFTSalePrice.Empty(assetId) - } else { - NFTSalePrice.Value( - assetId = assetId, - value = sdkSalePrice.value, - symbol = sdkSalePrice.symbol, - ) - } - - saveSalePriceInRuntime(userWalletId, network, salePrice) - - sdkSalePrice?.let { - saveSalePriceInPersistence(userWalletId, network, sdkAssetId, it) - } + getNFTSalePrice( + userWalletId = userWalletId, + network = network, + collectionId = collectionId, + assetId = assetId, + ) }.onLeft { saveSalePriceInRuntime(userWalletId, network, NFTSalePrice.Error(assetId)) } @@ -396,14 +435,17 @@ internal class DefaultNFTRepository @Inject constructor( prices .mapKeys { val (assetId, _) = it - NFTSdkAssetIdentifierConverter.convert(assetId) + assetIdConverter.convert(assetId) } .mapValues { val (assetId, price) = it + val nftCurrency = getNFTCurrency(network) NFTSalePrice.Value( assetId = assetId, - value = price.value, - symbol = price.symbol, + value = price.value.movePointLeft(nftCurrency.decimals), + fiatValue = null, + symbol = nftCurrency.symbol, + decimals = nftCurrency.decimals, ) } } diff --git a/domain/nft/build.gradle.kts b/domain/nft/build.gradle.kts index 518f509278..28e807c797 100644 --- a/domain/nft/build.gradle.kts +++ b/domain/nft/build.gradle.kts @@ -22,4 +22,5 @@ dependencies { implementation(projects.domain.tokens) implementation(projects.domain.tokens.models) implementation(projects.domain.wallets.models) + implementation(projects.domain.quotes) } \ No newline at end of file diff --git a/domain/nft/models/src/main/kotlin/com/tangem/domain/nft/models/NFTSalePrice.kt b/domain/nft/models/src/main/kotlin/com/tangem/domain/nft/models/NFTSalePrice.kt index 52cc43d072..43bc8a4a29 100644 --- a/domain/nft/models/src/main/kotlin/com/tangem/domain/nft/models/NFTSalePrice.kt +++ b/domain/nft/models/src/main/kotlin/com/tangem/domain/nft/models/NFTSalePrice.kt @@ -26,6 +26,8 @@ sealed class NFTSalePrice { data class Value( override val assetId: NFTAsset.Identifier, val value: SerializedBigDecimal, + val fiatValue: SerializedBigDecimal?, val symbol: String, + val decimals: Int, ) : NFTSalePrice() } \ No newline at end of file diff --git a/domain/nft/src/main/kotlin/com/tangem/domain/nft/FetchNFTPriceUseCase.kt b/domain/nft/src/main/kotlin/com/tangem/domain/nft/FetchNFTPriceUseCase.kt new file mode 100644 index 0000000000..fafac1aea8 --- /dev/null +++ b/domain/nft/src/main/kotlin/com/tangem/domain/nft/FetchNFTPriceUseCase.kt @@ -0,0 +1,26 @@ +package com.tangem.domain.nft + +import arrow.core.Either +import com.tangem.domain.nft.repository.NFTRepository +import com.tangem.domain.quotes.single.SingleQuoteFetcher +import com.tangem.domain.tokens.model.Network + +class FetchNFTPriceUseCase( + private val nftRepository: NFTRepository, + private val singleQuoteFetcher: SingleQuoteFetcher, +) { + + suspend operator fun invoke(network: Network, appCurrencyId: String?): Either { + return Either.catch { + val nftCurrency = nftRepository.getNFTCurrency(network) + val rawId = nftCurrency.id.rawCurrencyId ?: error("Invalid nft currency id") + + singleQuoteFetcher( + params = SingleQuoteFetcher.Params( + rawCurrencyId = rawId, + appCurrencyId = appCurrencyId, + ), + ) + } + } +} \ No newline at end of file diff --git a/domain/nft/src/main/kotlin/com/tangem/domain/nft/GetNFTPriceUseCase.kt b/domain/nft/src/main/kotlin/com/tangem/domain/nft/GetNFTPriceUseCase.kt new file mode 100644 index 0000000000..4dc44a7590 --- /dev/null +++ b/domain/nft/src/main/kotlin/com/tangem/domain/nft/GetNFTPriceUseCase.kt @@ -0,0 +1,44 @@ +package com.tangem.domain.nft + +import arrow.core.Either +import com.tangem.domain.nft.models.NFTAsset +import com.tangem.domain.nft.models.NFTSalePrice +import com.tangem.domain.nft.repository.NFTRepository +import com.tangem.domain.quotes.single.SingleQuoteProducer +import com.tangem.domain.quotes.single.SingleQuoteSupplier +import com.tangem.domain.tokens.model.Quote +import com.tangem.domain.wallets.models.UserWalletId +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.map + +class GetNFTPriceUseCase( + private val nftRepository: NFTRepository, + private val singleQuoteSupplier: SingleQuoteSupplier, +) { + + suspend operator fun invoke(userWalletId: UserWalletId, nftAsset: NFTAsset): Either> { + return Either.catch { + val nftCurrency = nftRepository.getNFTCurrency(nftAsset.network) + val rawId = nftCurrency.id.rawCurrencyId ?: error("Invalid nft currency id") + + singleQuoteSupplier( + params = SingleQuoteProducer.Params(rawCurrencyId = rawId), + ).map { quote -> + val nftPrice = nftRepository.getNFTSalePrice( + userWalletId = userWalletId, + network = nftAsset.network, + collectionId = nftAsset.collectionId, + assetId = nftAsset.id, + ) + + val quoteValue = quote as? Quote.Value + + if (nftPrice !is NFTSalePrice.Value) { + nftPrice + } else { + nftPrice.copy(fiatValue = quoteValue?.fiatRate?.multiply(nftPrice.value)) + } + } + } + } +} \ No newline at end of file diff --git a/domain/nft/src/main/kotlin/com/tangem/domain/nft/repository/NFTRepository.kt b/domain/nft/src/main/kotlin/com/tangem/domain/nft/repository/NFTRepository.kt index 1484b241a3..5f8817884c 100644 --- a/domain/nft/src/main/kotlin/com/tangem/domain/nft/repository/NFTRepository.kt +++ b/domain/nft/src/main/kotlin/com/tangem/domain/nft/repository/NFTRepository.kt @@ -3,6 +3,8 @@ package com.tangem.domain.nft.repository import com.tangem.domain.nft.models.NFTAsset import com.tangem.domain.nft.models.NFTCollection import com.tangem.domain.nft.models.NFTCollections +import com.tangem.domain.nft.models.NFTSalePrice +import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.domain.tokens.model.Network import com.tangem.domain.wallets.models.UserWalletId import kotlinx.coroutines.flow.Flow @@ -10,6 +12,15 @@ import kotlinx.coroutines.flow.Flow interface NFTRepository { fun observeCollections(userWalletId: UserWalletId, networks: List): Flow> + fun getNFTCurrency(network: Network): CryptoCurrency + + suspend fun getNFTSalePrice( + userWalletId: UserWalletId, + network: Network, + collectionId: NFTCollection.Identifier, + assetId: NFTAsset.Identifier, + ): NFTSalePrice + suspend fun refreshCollections(userWalletId: UserWalletId, networks: List) suspend fun refreshAssets(userWalletId: UserWalletId, network: Network, collectionId: NFTCollection.Identifier) From 597ff6b82e58033c9cb2e11cf07c9cb4973e3e03 Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 12 May 2025 15:26:21 +0000 Subject: [PATCH 021/165] Updated on 2026-08-14 --- gradle/tangem_dependencies.toml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/gradle/tangem_dependencies.toml b/gradle/tangem_dependencies.toml index ef3a5a7696..ccc406b5ff 100644 --- a/gradle/tangem_dependencies.toml +++ b/gradle/tangem_dependencies.toml @@ -5,9 +5,9 @@ # https://github.com/tangem/tangem-sdk-android/ # https://github.com/tangem/vico -tangemBlockchainSdk = "releases-5.24.0-1062" +tangemBlockchainSdk = "develop-1063" #tangemBlockchainSdk = "0.0.1" # Keep it! - used for local builds -tangemCardSdk = "releases-5.24.0-467" +tangemCardSdk = "develop-468" #tangemCardSdk = "0.0.1" # Keep it! - used for local builds ^ tangemVico = "2.0.0-alpha.25-tangem12" #tangemVico = "0.0.1" # Keep it! - used for local builds ^ From 90b09aeb93f6f9bf03d8e73053ba776ab7c53c0a Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 7 May 2025 16:02:53 +0500 Subject: [PATCH 022/165] Updated on 2026-08-14 --- features/nft/impl/build.gradle.kts | 1 + .../nft/collections/entity/NFTSalePriceUM.kt | 3 +- .../transformer/UpdateDataStateTransformer.kt | 46 ++++++---- .../nft/collections/ui/NFTCollectionAsset.kt | 5 +- .../collections/ui/NFTCollectionsContent.kt | 5 +- .../nft/collections/ui/NFTSalePrice.kt | 3 +- .../features/nft/details/entity/NFTAssetUM.kt | 10 +- .../nft/details/entity/NFTDetailsUM.kt | 3 + .../entity/factory/NFTDetailsUMFactory.kt | 40 +++++++- .../transformer/NFTPriceChangeTransformer.kt | 53 +++++++++++ .../NFTPriceUpdatingTransformer.kt | 22 +++++ .../nft/details/model/NFTDetailsModel.kt | 91 ++++++++++++++++++- .../features/nft/details/ui/NFTDetails.kt | 21 +++-- .../nft/details/ui/NFTDetailsAsset.kt | 10 +- .../nft/details/ui/NFTDetailsInfoGroup.kt | 59 +++++------- 15 files changed, 288 insertions(+), 84 deletions(-) create mode 100644 features/nft/impl/src/main/kotlin/com/tangem/features/nft/details/entity/transformer/NFTPriceChangeTransformer.kt create mode 100644 features/nft/impl/src/main/kotlin/com/tangem/features/nft/details/entity/transformer/NFTPriceUpdatingTransformer.kt diff --git a/features/nft/impl/build.gradle.kts b/features/nft/impl/build.gradle.kts index b30b854ca8..7ac4ae8799 100644 --- a/features/nft/impl/build.gradle.kts +++ b/features/nft/impl/build.gradle.kts @@ -28,6 +28,7 @@ dependencies { /** Domain modules */ implementation(projects.domain.appCurrency.models) + implementation(projects.domain.appCurrency) implementation(projects.domain.models) implementation(projects.domain.nft) implementation(projects.domain.nft.models) diff --git a/features/nft/impl/src/main/kotlin/com/tangem/features/nft/collections/entity/NFTSalePriceUM.kt b/features/nft/impl/src/main/kotlin/com/tangem/features/nft/collections/entity/NFTSalePriceUM.kt index 8747a49f55..19911a7d9b 100644 --- a/features/nft/impl/src/main/kotlin/com/tangem/features/nft/collections/entity/NFTSalePriceUM.kt +++ b/features/nft/impl/src/main/kotlin/com/tangem/features/nft/collections/entity/NFTSalePriceUM.kt @@ -1,10 +1,11 @@ package com.tangem.features.nft.collections.entity import androidx.compose.runtime.Immutable +import com.tangem.core.ui.extensions.TextReference @Immutable internal sealed class NFTSalePriceUM { data object Loading : NFTSalePriceUM() data object Failed : NFTSalePriceUM() - data class Content(val price: String) : NFTSalePriceUM() + data class Content(val price: TextReference) : NFTSalePriceUM() } \ No newline at end of file diff --git a/features/nft/impl/src/main/kotlin/com/tangem/features/nft/collections/entity/transformer/UpdateDataStateTransformer.kt b/features/nft/impl/src/main/kotlin/com/tangem/features/nft/collections/entity/transformer/UpdateDataStateTransformer.kt index a73a5aba58..9a7e3bc93d 100644 --- a/features/nft/impl/src/main/kotlin/com/tangem/features/nft/collections/entity/transformer/UpdateDataStateTransformer.kt +++ b/features/nft/impl/src/main/kotlin/com/tangem/features/nft/collections/entity/transformer/UpdateDataStateTransformer.kt @@ -2,10 +2,9 @@ package com.tangem.features.nft.collections.entity.transformer import com.tangem.core.ui.components.fields.entity.SearchBarUM import com.tangem.core.ui.components.notifications.NotificationConfig -import com.tangem.core.ui.extensions.TextReference -import com.tangem.core.ui.extensions.getActiveIconRes -import com.tangem.core.ui.extensions.resourceReference -import com.tangem.core.ui.extensions.wrappedList +import com.tangem.core.ui.extensions.* +import com.tangem.core.ui.format.bigdecimal.crypto +import com.tangem.core.ui.format.bigdecimal.format import com.tangem.domain.nft.models.* import com.tangem.features.nft.collections.entity.* import com.tangem.features.nft.impl.R @@ -146,20 +145,31 @@ internal class UpdateDataStateTransformer( ) } - private fun NFTAsset.transform(collectionName: String): NFTCollectionAssetUM = NFTCollectionAssetUM( - id = id.toString(), - name = name.orEmpty(), - imageUrl = media?.url, - price = when (val salePrice = salePrice) { - is NFTSalePrice.Empty -> NFTSalePriceUM.Failed - is NFTSalePrice.Loading -> NFTSalePriceUM.Loading - is NFTSalePrice.Error -> NFTSalePriceUM.Failed - is NFTSalePrice.Value -> NFTSalePriceUM.Content(salePrice.value.toString()) - }, - onItemClick = { - onAssetClick(this, collectionName) - }, - ) + private fun NFTAsset.transform(collectionName: String): NFTCollectionAssetUM { + return NFTCollectionAssetUM( + id = id.toString(), + name = name.orEmpty(), + imageUrl = media?.url, + price = when (val salePrice = salePrice) { + is NFTSalePrice.Empty -> NFTSalePriceUM.Failed + is NFTSalePrice.Loading -> NFTSalePriceUM.Loading + is NFTSalePrice.Error -> NFTSalePriceUM.Failed + is NFTSalePrice.Value -> NFTSalePriceUM.Content( + price = stringReference( + salePrice.value.format { + crypto( + symbol = salePrice.symbol, + decimals = salePrice.decimals, + ) + }, + ), + ) + }, + onItemClick = { + onAssetClick(this, collectionName) + }, + ) + } private fun NFTCollection.isExpanded(state: NFTCollectionsStateUM): Boolean = (state.content as? NFTCollectionsUM.Content) diff --git a/features/nft/impl/src/main/kotlin/com/tangem/features/nft/collections/ui/NFTCollectionAsset.kt b/features/nft/impl/src/main/kotlin/com/tangem/features/nft/collections/ui/NFTCollectionAsset.kt index dfb100463f..87832394e4 100644 --- a/features/nft/impl/src/main/kotlin/com/tangem/features/nft/collections/ui/NFTCollectionAsset.kt +++ b/features/nft/impl/src/main/kotlin/com/tangem/features/nft/collections/ui/NFTCollectionAsset.kt @@ -24,6 +24,7 @@ import coil.request.ImageRequest import com.tangem.core.ui.components.RectangleShimmer import com.tangem.core.ui.components.SpacerH12 import com.tangem.core.ui.components.SpacerH2 +import com.tangem.core.ui.extensions.stringReference import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreview import com.tangem.features.nft.collections.entity.NFTCollectionAssetUM @@ -116,7 +117,9 @@ private class NFTCollectionAssetProvider : CollectionPreviewParameterProvider { Text( modifier = modifier, - text = state.price, + text = state.price.resolveReference(), style = TangemTheme.typography.caption2, color = TangemTheme.colors.text.tertiary, maxLines = 1, diff --git a/features/nft/impl/src/main/kotlin/com/tangem/features/nft/details/entity/NFTAssetUM.kt b/features/nft/impl/src/main/kotlin/com/tangem/features/nft/details/entity/NFTAssetUM.kt index 88808c889c..66d1c85e54 100644 --- a/features/nft/impl/src/main/kotlin/com/tangem/features/nft/details/entity/NFTAssetUM.kt +++ b/features/nft/impl/src/main/kotlin/com/tangem/features/nft/details/entity/NFTAssetUM.kt @@ -3,9 +3,7 @@ package com.tangem.features.nft.details.entity import androidx.compose.runtime.Immutable import com.tangem.core.ui.components.atoms.text.TextEllipsis import com.tangem.core.ui.extensions.TextReference -import com.tangem.domain.appcurrency.model.AppCurrency import kotlinx.collections.immutable.ImmutableList -import java.math.BigDecimal data class NFTAssetUM( val name: String, @@ -41,11 +39,9 @@ data class NFTAssetUM( data object Loading : SalePrice() data object Empty : SalePrice() data class Content( - val value: BigDecimal, - val symbol: String, - val decimals: Int, - val rate: BigDecimal?, - val appCurrency: AppCurrency, + val isFlickering: Boolean, + val cryptoPrice: TextReference, + val fiatPrice: TextReference, ) : SalePrice() } diff --git a/features/nft/impl/src/main/kotlin/com/tangem/features/nft/details/entity/NFTDetailsUM.kt b/features/nft/impl/src/main/kotlin/com/tangem/features/nft/details/entity/NFTDetailsUM.kt index 95844969ae..b55a4dffa3 100644 --- a/features/nft/impl/src/main/kotlin/com/tangem/features/nft/details/entity/NFTDetailsUM.kt +++ b/features/nft/impl/src/main/kotlin/com/tangem/features/nft/details/entity/NFTDetailsUM.kt @@ -1,7 +1,10 @@ package com.tangem.features.nft.details.entity +import com.tangem.core.ui.components.containers.pullToRefresh.PullToRefreshConfig + internal data class NFTDetailsUM( val nftAsset: NFTAssetUM, + val pullToRefreshConfig: PullToRefreshConfig, val onBackClick: () -> Unit, val onReadMoreClick: () -> Unit, val onSeeAllTraitsClick: () -> Unit, diff --git a/features/nft/impl/src/main/kotlin/com/tangem/features/nft/details/entity/factory/NFTDetailsUMFactory.kt b/features/nft/impl/src/main/kotlin/com/tangem/features/nft/details/entity/factory/NFTDetailsUMFactory.kt index e2e64fdb2b..f4701906c6 100644 --- a/features/nft/impl/src/main/kotlin/com/tangem/features/nft/details/entity/factory/NFTDetailsUMFactory.kt +++ b/features/nft/impl/src/main/kotlin/com/tangem/features/nft/details/entity/factory/NFTDetailsUMFactory.kt @@ -1,9 +1,14 @@ package com.tangem.features.nft.details.entity.factory import com.tangem.core.ui.components.atoms.text.TextEllipsis +import com.tangem.core.ui.components.containers.pullToRefresh.PullToRefreshConfig import com.tangem.core.ui.extensions.TextReference import com.tangem.core.ui.extensions.resourceReference import com.tangem.core.ui.extensions.stringReference +import com.tangem.core.ui.format.bigdecimal.crypto +import com.tangem.core.ui.format.bigdecimal.fiat +import com.tangem.core.ui.format.bigdecimal.format +import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.nft.models.NFTAsset import com.tangem.domain.nft.models.NFTSalePrice import com.tangem.features.nft.details.entity.NFTAssetUM @@ -12,17 +17,24 @@ import com.tangem.features.nft.impl.R import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.toImmutableList +@Suppress("LongParameterList") internal class NFTDetailsUMFactory( + private val appCurrency: AppCurrency, private val onBackClick: () -> Unit, private val onReadMoreClick: () -> Unit, private val onSeeAllTraitsClick: () -> Unit, private val onExploreClick: () -> Unit, private val onSendClick: () -> Unit, + private val onRefresh: () -> Unit, private val onInfoBlockClick: (title: TextReference, text: TextReference) -> Unit, ) { fun getInitialState(nftAsset: NFTAsset): NFTDetailsUM = NFTDetailsUM( nftAsset = nftAsset.transform(), + pullToRefreshConfig = PullToRefreshConfig( + isRefreshing = false, + onRefresh = { onRefresh() }, + ), onBackClick = onBackClick, onReadMoreClick = onReadMoreClick, onSeeAllTraitsClick = onSeeAllTraitsClick, @@ -52,7 +64,7 @@ internal class NFTDetailsUMFactory( } else { null }, - salePrice = NFTAssetUM.SalePrice.Empty, + salePrice = toSalePrice(), description = description, rarity = if (rarity != null) { NFTAssetUM.Rarity.Content( @@ -91,6 +103,32 @@ internal class NFTDetailsUMFactory( private fun NFTAsset.hasSalePrice() = salePrice !is NFTSalePrice.Empty && salePrice !is NFTSalePrice.Error + private fun NFTAsset.toSalePrice() = when (val price = salePrice) { + is NFTSalePrice.Empty, + is NFTSalePrice.Error, + -> NFTAssetUM.SalePrice.Empty + is NFTSalePrice.Loading -> NFTAssetUM.SalePrice.Loading + is NFTSalePrice.Value -> NFTAssetUM.SalePrice.Content( + isFlickering = false, + cryptoPrice = stringReference( + price.value.format { + crypto( + symbol = price.symbol, + decimals = price.decimals, + ) + }, + ), + fiatPrice = stringReference( + price.fiatValue.format { + fiat( + fiatCurrencyCode = appCurrency.code, + fiatCurrencySymbol = appCurrency.symbol, + ) + }, + ), + ) + } + @Suppress("LongMethod") private fun NFTAsset.buildBaseInfoItems() = when (val id = id) { is NFTAsset.Identifier.EVM -> persistentListOf( diff --git a/features/nft/impl/src/main/kotlin/com/tangem/features/nft/details/entity/transformer/NFTPriceChangeTransformer.kt b/features/nft/impl/src/main/kotlin/com/tangem/features/nft/details/entity/transformer/NFTPriceChangeTransformer.kt new file mode 100644 index 0000000000..8ab05b6d7b --- /dev/null +++ b/features/nft/impl/src/main/kotlin/com/tangem/features/nft/details/entity/transformer/NFTPriceChangeTransformer.kt @@ -0,0 +1,53 @@ +package com.tangem.features.nft.details.entity.transformer + +import com.tangem.core.ui.extensions.stringReference +import com.tangem.core.ui.format.bigdecimal.crypto +import com.tangem.core.ui.format.bigdecimal.fiat +import com.tangem.core.ui.format.bigdecimal.format +import com.tangem.domain.appcurrency.model.AppCurrency +import com.tangem.domain.nft.models.NFTSalePrice +import com.tangem.features.nft.details.entity.NFTAssetUM +import com.tangem.features.nft.details.entity.NFTDetailsUM +import com.tangem.utils.transformer.Transformer + +internal class NFTPriceChangeTransformer( + private val appCurrency: AppCurrency, + private val nftSalePrice: NFTSalePrice, +) : Transformer { + + override fun transform(prevState: NFTDetailsUM): NFTDetailsUM { + val topInfo = prevState.nftAsset.topInfo as? NFTAssetUM.TopInfo.Content ?: return prevState + + return prevState.copy( + nftAsset = prevState.nftAsset.copy( + topInfo = topInfo.copy( + salePrice = when (nftSalePrice) { + is NFTSalePrice.Empty, + is NFTSalePrice.Error, + -> NFTAssetUM.SalePrice.Empty + is NFTSalePrice.Loading -> NFTAssetUM.SalePrice.Loading + is NFTSalePrice.Value -> NFTAssetUM.SalePrice.Content( + isFlickering = false, + cryptoPrice = stringReference( + nftSalePrice.value.format { + crypto( + symbol = nftSalePrice.symbol, + decimals = nftSalePrice.decimals, + ) + }, + ), + fiatPrice = stringReference( + nftSalePrice.fiatValue.format { + fiat( + fiatCurrencyCode = appCurrency.code, + fiatCurrencySymbol = appCurrency.symbol, + ) + }, + ), + ) + }, + ), + ), + ) + } +} \ No newline at end of file diff --git a/features/nft/impl/src/main/kotlin/com/tangem/features/nft/details/entity/transformer/NFTPriceUpdatingTransformer.kt b/features/nft/impl/src/main/kotlin/com/tangem/features/nft/details/entity/transformer/NFTPriceUpdatingTransformer.kt new file mode 100644 index 0000000000..c0de663d5e --- /dev/null +++ b/features/nft/impl/src/main/kotlin/com/tangem/features/nft/details/entity/transformer/NFTPriceUpdatingTransformer.kt @@ -0,0 +1,22 @@ +package com.tangem.features.nft.details.entity.transformer + +import com.tangem.features.nft.details.entity.NFTAssetUM +import com.tangem.features.nft.details.entity.NFTDetailsUM +import com.tangem.utils.transformer.Transformer + +internal object NFTPriceUpdatingTransformer : Transformer { + + override fun transform(prevState: NFTDetailsUM): NFTDetailsUM { + val topInfo = prevState.nftAsset.topInfo as? NFTAssetUM.TopInfo.Content ?: return prevState + val salePrice = topInfo.salePrice as? NFTAssetUM.SalePrice.Content + return prevState.copy( + nftAsset = prevState.nftAsset.copy( + topInfo = topInfo.copy( + salePrice = salePrice?.copy( + isFlickering = true, + ) ?: topInfo.salePrice, + ), + ), + ) + } +} \ No newline at end of file diff --git a/features/nft/impl/src/main/kotlin/com/tangem/features/nft/details/model/NFTDetailsModel.kt b/features/nft/impl/src/main/kotlin/com/tangem/features/nft/details/model/NFTDetailsModel.kt index dd61ccf4f8..8cdbd19cb9 100644 --- a/features/nft/impl/src/main/kotlin/com/tangem/features/nft/details/model/NFTDetailsModel.kt +++ b/features/nft/impl/src/main/kotlin/com/tangem/features/nft/details/model/NFTDetailsModel.kt @@ -1,5 +1,6 @@ package com.tangem.features.nft.details.model +import arrow.core.getOrElse import com.arkivanov.decompose.router.slot.SlotNavigation import com.arkivanov.decompose.router.slot.activate import com.tangem.common.routing.AppRoute @@ -12,20 +13,31 @@ import com.tangem.core.navigation.url.UrlOpener 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.appcurrency.GetSelectedAppCurrencyUseCase +import com.tangem.domain.appcurrency.model.AppCurrency +import com.tangem.domain.nft.FetchNFTCollectionAssetsUseCase +import com.tangem.domain.nft.FetchNFTPriceUseCase import com.tangem.domain.nft.GetNFTExploreUrlUseCase +import com.tangem.domain.nft.GetNFTPriceUseCase import com.tangem.domain.nft.analytics.NFTAnalyticsEvent import com.tangem.features.nft.details.NFTDetailsComponent import com.tangem.features.nft.details.entity.NFTAssetUM import com.tangem.features.nft.details.entity.NFTDetailsBottomSheetConfig import com.tangem.features.nft.details.entity.NFTDetailsUM import com.tangem.features.nft.details.entity.factory.NFTDetailsUMFactory +import com.tangem.features.nft.details.entity.transformer.NFTPriceChangeTransformer +import com.tangem.features.nft.details.entity.transformer.NFTPriceUpdatingTransformer import com.tangem.features.nft.impl.R import com.tangem.utils.coroutines.CoroutineDispatcherProvider -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.StateFlow +import com.tangem.utils.transformer.update +import kotlinx.coroutines.async +import kotlinx.coroutines.awaitAll +import kotlinx.coroutines.flow.* import kotlinx.coroutines.launch +import timber.log.Timber import javax.inject.Inject +@Suppress("LongParameterList") @ModelScoped internal class NFTDetailsModel @Inject constructor( override val dispatchers: CoroutineDispatcherProvider, @@ -33,6 +45,10 @@ internal class NFTDetailsModel @Inject constructor( private val analyticsEventHandler: AnalyticsEventHandler, private val urlOpener: UrlOpener, private val getNFTExploreUrlUseCase: GetNFTExploreUrlUseCase, + private val getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase, + private val getNFTPriceUseCase: GetNFTPriceUseCase, + private val fetchNFTCollectionAssetsUseCase: FetchNFTCollectionAssetsUseCase, + private val fetchNFTPriceUseCase: FetchNFTPriceUseCase, paramsContainer: ParamsContainer, ) : Model() { @@ -40,23 +56,88 @@ internal class NFTDetailsModel @Inject constructor( val state: StateFlow get() = _state + private var appCurrency: AppCurrency = AppCurrency.Default + private val stateFactory: NFTDetailsUMFactory = NFTDetailsUMFactory( + appCurrency = appCurrency, onBackClick = { params.onBackClick() }, onReadMoreClick = ::onReadMoreClick, onSeeAllTraitsClick = { params.onAllTraitsClick() }, onExploreClick = ::onExploreClick, onSendClick = ::onSendClick, onInfoBlockClick = ::onInfoBlockClick, + onRefresh = ::onRefresh, ) - private val _state = MutableStateFlow( - value = stateFactory.getInitialState(params.nftAsset), - ) + private val _state by lazy { + MutableStateFlow( + value = stateFactory.getInitialState(params.nftAsset), + ) + } val bottomSheetNavigation: SlotNavigation = SlotNavigation() init { analyticsEventHandler.send(NFTAnalyticsEvent.Details.ScreenOpened(params.nftAsset.network.name)) + initAppCurrency() + subscribeToPriceChanges() + } + + private fun initAppCurrency() { + modelScope.launch { + appCurrency = getSelectedAppCurrencyUseCase.invokeSync().getOrElse { AppCurrency.Default } + } + } + + private fun subscribeToPriceChanges() { + _state.update(NFTPriceUpdatingTransformer) + modelScope.launch { + getNFTPriceUseCase(params.userWalletId, params.nftAsset) + .fold( + ifLeft = { + Timber.w(it) + }, + ifRight = { quoteFlow -> + quoteFlow + .distinctUntilChanged() + .onEach { salePrice -> + _state.update( + NFTPriceChangeTransformer( + appCurrency = appCurrency, + nftSalePrice = salePrice, + ), + ) + }.launchIn(modelScope) + }, + ) + } + } + + private fun onRefresh() { + _state.update { + it.copy(pullToRefreshConfig = it.pullToRefreshConfig.copy(isRefreshing = true)) + } + _state.update(NFTPriceUpdatingTransformer) + modelScope.launch { + awaitAll( + async { + fetchNFTCollectionAssetsUseCase( + userWalletId = params.userWalletId, + network = params.nftAsset.network, + collectionId = params.nftAsset.collectionId, + ) + }, + async { + fetchNFTPriceUseCase( + network = params.nftAsset.network, + appCurrencyId = null, + ) + }, + ) + _state.update { + it.copy(pullToRefreshConfig = it.pullToRefreshConfig.copy(isRefreshing = false)) + } + } } private fun onInfoBlockClick(title: TextReference, text: TextReference) { diff --git a/features/nft/impl/src/main/kotlin/com/tangem/features/nft/details/ui/NFTDetails.kt b/features/nft/impl/src/main/kotlin/com/tangem/features/nft/details/ui/NFTDetails.kt index b662151b8d..203288ef78 100644 --- a/features/nft/impl/src/main/kotlin/com/tangem/features/nft/details/ui/NFTDetails.kt +++ b/features/nft/impl/src/main/kotlin/com/tangem/features/nft/details/ui/NFTDetails.kt @@ -1,6 +1,7 @@ package com.tangem.features.nft.details.ui import androidx.activity.compose.BackHandler +import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.statusBarsPadding @@ -11,6 +12,7 @@ import androidx.compose.ui.Modifier import com.tangem.core.ui.components.PrimaryButton import com.tangem.core.ui.components.appbar.TangemTopAppBar import com.tangem.core.ui.components.appbar.models.TopAppBarButtonUM +import com.tangem.core.ui.components.containers.pullToRefresh.TangemPullToRefreshContainer import com.tangem.core.ui.extensions.stringResourceSafe import com.tangem.core.ui.res.TangemTheme import com.tangem.features.nft.details.entity.NFTDetailsUM @@ -34,14 +36,19 @@ internal fun NFTDetails(state: NFTDetailsUM, modifier: Modifier = Modifier) { ) }, content = { innerPadding -> - NFTDetailsAsset( - state = state.nftAsset, - onReadMoreClick = state.onReadMoreClick, - onSeeAllTraitsClick = state.onSeeAllTraitsClick, - onExploreClick = state.onExploreClick, + TangemPullToRefreshContainer( + config = state.pullToRefreshConfig, modifier = Modifier - .padding(innerPadding), - ) + .padding(innerPadding) + .fillMaxSize(), + ) { + NFTDetailsAsset( + state = state.nftAsset, + onReadMoreClick = state.onReadMoreClick, + onSeeAllTraitsClick = state.onSeeAllTraitsClick, + onExploreClick = state.onExploreClick, + ) + } }, floatingActionButtonPosition = FabPosition.Center, floatingActionButton = { diff --git a/features/nft/impl/src/main/kotlin/com/tangem/features/nft/details/ui/NFTDetailsAsset.kt b/features/nft/impl/src/main/kotlin/com/tangem/features/nft/details/ui/NFTDetailsAsset.kt index 1f955d7860..377935d368 100644 --- a/features/nft/impl/src/main/kotlin/com/tangem/features/nft/details/ui/NFTDetailsAsset.kt +++ b/features/nft/impl/src/main/kotlin/com/tangem/features/nft/details/ui/NFTDetailsAsset.kt @@ -16,11 +16,9 @@ import com.tangem.core.ui.extensions.resourceReference import com.tangem.core.ui.extensions.stringReference import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreview -import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.features.nft.details.entity.NFTAssetUM import com.tangem.features.nft.impl.R import kotlinx.collections.immutable.persistentListOf -import java.math.BigDecimal @Composable internal fun NFTDetailsAsset( @@ -127,11 +125,9 @@ private class NFTAssetProvider : CollectionPreviewParameterProvider( topInfo = NFTAssetUM.TopInfo.Content( title = resourceReference(R.string.nft_details_last_sale_price), salePrice = NFTAssetUM.SalePrice.Content( - value = BigDecimal("18.75"), - symbol = "ETH", - decimals = 18, - rate = BigDecimal("1"), - appCurrency = AppCurrency.Default, + cryptoPrice = stringReference("ETH 18.75"), + fiatPrice = stringReference("$ 1"), + isFlickering = true, ), description = "Base edition by Piux. An illustration of Crypto Robot #7804".repeat(3), rarity = NFTAssetUM.Rarity.Content( diff --git a/features/nft/impl/src/main/kotlin/com/tangem/features/nft/details/ui/NFTDetailsInfoGroup.kt b/features/nft/impl/src/main/kotlin/com/tangem/features/nft/details/ui/NFTDetailsInfoGroup.kt index bb1580e513..5eac67fcd6 100644 --- a/features/nft/impl/src/main/kotlin/com/tangem/features/nft/details/ui/NFTDetailsInfoGroup.kt +++ b/features/nft/impl/src/main/kotlin/com/tangem/features/nft/details/ui/NFTDetailsInfoGroup.kt @@ -1,7 +1,10 @@ package com.tangem.features.nft.details.ui import android.content.res.Configuration -import androidx.compose.animation.* +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.animation.animateContentSize +import androidx.compose.animation.fadeIn +import androidx.compose.animation.fadeOut import androidx.compose.foundation.clickable import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.layout.* @@ -14,21 +17,17 @@ import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider import androidx.compose.ui.unit.dp -import com.tangem.common.ui.amountScreen.utils.getFiatString import com.tangem.core.ui.components.TextShimmer import com.tangem.core.ui.components.atoms.text.ReadMoreText import com.tangem.core.ui.components.block.information.InformationBlock +import com.tangem.core.ui.components.flicker import com.tangem.core.ui.extensions.resolveReference import com.tangem.core.ui.extensions.resourceReference import com.tangem.core.ui.extensions.stringReference -import com.tangem.core.ui.format.bigdecimal.crypto -import com.tangem.core.ui.format.bigdecimal.format import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreview -import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.features.nft.details.entity.NFTAssetUM import com.tangem.features.nft.impl.R -import java.math.BigDecimal private const val READ_MORE_MAX_LINES = 3 @@ -68,7 +67,9 @@ internal fun NFTDetailsInfoGroup( @Composable private fun SalePrice(state: NFTAssetUM.SalePrice, modifier: Modifier = Modifier) { AnimatedVisibility( - modifier = Modifier.fillMaxWidth().animateContentSize(), + modifier = Modifier + .fillMaxWidth() + .animateContentSize(), visible = state !is NFTAssetUM.SalePrice.Empty, enter = fadeIn(), exit = fadeOut(), @@ -108,19 +109,15 @@ private fun SalePrice(state: NFTAssetUM.SalePrice, modifier: Modifier = Modifier verticalArrangement = Arrangement.SpaceAround, ) { Text( - text = state.value.format { - crypto( - symbol = state.symbol, - decimals = state.decimals, - ) - }, + text = state.cryptoPrice.resolveReference(), style = TangemTheme.typography.head, color = TangemTheme.colors.text.primary1, ) Text( modifier = Modifier - .padding(top = TangemTheme.dimens.spacing4), - text = getFiatString(state.value, state.rate, state.appCurrency), + .padding(top = TangemTheme.dimens.spacing4) + .flicker(state.isFlickering), + text = state.fiatPrice.resolveReference(), style = TangemTheme.typography.caption2, color = TangemTheme.colors.text.tertiary, ) @@ -237,11 +234,9 @@ private class NFTAssetInfoProvider : CollectionPreviewParameterProvider Date: Mon, 12 May 2025 18:47:23 +0300 Subject: [PATCH 023/165] Updated on 2026-08-14 --- .../networks/multi/DefaultMultiNetworkStatusFetcher.kt | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/data/networks/src/main/java/com/tangem/data/networks/multi/DefaultMultiNetworkStatusFetcher.kt b/data/networks/src/main/java/com/tangem/data/networks/multi/DefaultMultiNetworkStatusFetcher.kt index cd67ca7880..eea619bd3b 100644 --- a/data/networks/src/main/java/com/tangem/data/networks/multi/DefaultMultiNetworkStatusFetcher.kt +++ b/data/networks/src/main/java/com/tangem/data/networks/multi/DefaultMultiNetworkStatusFetcher.kt @@ -5,9 +5,9 @@ import arrow.core.raise.ensure import com.tangem.blockchain.common.Blockchain import com.tangem.blockchainsdk.utils.ExcludedBlockchains import com.tangem.blockchainsdk.utils.fromNetworkId +import com.tangem.data.common.currency.CardCryptoCurrencyFactory import com.tangem.data.common.currency.ResponseCryptoCurrenciesFactory import com.tangem.data.networks.store.NetworksStatusesStoreV2 -import com.tangem.data.tokens.utils.CardCryptoCurrenciesFactory import com.tangem.datasource.api.tangemTech.models.UserTokensResponse import com.tangem.datasource.local.preferences.AppPreferencesStore import com.tangem.datasource.local.preferences.PreferencesKeys @@ -15,7 +15,6 @@ import com.tangem.datasource.local.preferences.utils.getObjectSyncOrNull import com.tangem.datasource.local.userwallet.UserWalletsStore import com.tangem.domain.common.util.cardTypesResolver import com.tangem.domain.core.utils.eitherOn -import com.tangem.domain.demo.DemoConfig import com.tangem.domain.networks.multi.MultiNetworkStatusFetcher import com.tangem.domain.networks.single.SingleNetworkStatusFetcher import com.tangem.domain.tokens.model.CryptoCurrency @@ -42,10 +41,9 @@ internal class DefaultMultiNetworkStatusFetcher @Inject constructor( private val appPreferencesStore: AppPreferencesStore, private val singleNetworkStatusFetcher: SingleNetworkStatusFetcher, private val dispatchers: CoroutineDispatcherProvider, + private val cardCryptoCurrencyFactory: CardCryptoCurrencyFactory, ) : MultiNetworkStatusFetcher { - private val demoConfig = DemoConfig() - private val cardCurrenciesFactory = CardCryptoCurrenciesFactory(demoConfig, excludedBlockchains) private val responseCurrenciesFactory by lazy { ResponseCryptoCurrenciesFactory(excludedBlockchains) } override suspend fun invoke(params: MultiNetworkStatusFetcher.Params) = eitherOn(dispatchers.default) { @@ -106,7 +104,7 @@ internal class DefaultMultiNetworkStatusFetcher @Inject constructor( val cardBlockchain = userWallet.scanResponse.cardTypesResolver.getBlockchain() if (!blockchains.contains(cardBlockchain)) return emptySet() - return cardCurrenciesFactory.createCurrenciesForSingleCurrencyCardWithToken(userWallet.scanResponse).toSet() + return cardCryptoCurrencyFactory.createCurrenciesForSingleCurrencyCardWithToken(userWallet.scanResponse).toSet() } private suspend fun getMultiWalletCurrencies(userWallet: UserWallet, networks: Set): Set { From d46b4c19a739c636d8cdec6be6bcae4a9a6b06bf Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 12 May 2025 18:50:10 +0300 Subject: [PATCH 024/165] Updated on 2026-08-14 --- .../data/networks/multi/DefaultMultiNetworkStatusFetcher.kt | 1 + 1 file changed, 1 insertion(+) diff --git a/data/networks/src/main/java/com/tangem/data/networks/multi/DefaultMultiNetworkStatusFetcher.kt b/data/networks/src/main/java/com/tangem/data/networks/multi/DefaultMultiNetworkStatusFetcher.kt index eea619bd3b..12b5b8fd81 100644 --- a/data/networks/src/main/java/com/tangem/data/networks/multi/DefaultMultiNetworkStatusFetcher.kt +++ b/data/networks/src/main/java/com/tangem/data/networks/multi/DefaultMultiNetworkStatusFetcher.kt @@ -34,6 +34,7 @@ import javax.inject.Inject * [REDACTED_AUTHOR] */ +@Suppress("LongParameterList") internal class DefaultMultiNetworkStatusFetcher @Inject constructor( excludedBlockchains: ExcludedBlockchains, private val networksStatusesStore: NetworksStatusesStoreV2, From 8880e16ce73a7bf4160d468a0fdf8502fa59ad0e Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 12 May 2025 17:48:09 +0000 Subject: [PATCH 025/165] Updated on 2026-08-14 --- gradle/tangem_dependencies.toml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/gradle/tangem_dependencies.toml b/gradle/tangem_dependencies.toml index ef3a5a7696..ccc406b5ff 100644 --- a/gradle/tangem_dependencies.toml +++ b/gradle/tangem_dependencies.toml @@ -5,9 +5,9 @@ # https://github.com/tangem/tangem-sdk-android/ # https://github.com/tangem/vico -tangemBlockchainSdk = "releases-5.24.0-1062" +tangemBlockchainSdk = "develop-1063" #tangemBlockchainSdk = "0.0.1" # Keep it! - used for local builds -tangemCardSdk = "releases-5.24.0-467" +tangemCardSdk = "develop-468" #tangemCardSdk = "0.0.1" # Keep it! - used for local builds ^ tangemVico = "2.0.0-alpha.25-tangem12" #tangemVico = "0.0.1" # Keep it! - used for local builds ^ From 4e69a47c327835a1aded0537d577bca3b16d5aad Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 13 May 2025 11:51:55 +0500 Subject: [PATCH 026/165] Updated on 2026-08-14 --- .../components/WcAppInfoContainerComponent.kt | 18 +- .../components/WcSelectNetworksComponent.kt | 367 ++++++++++++++++++ .../components/WcSelectWalletComponent.kt | 97 +++-- .../connections/entity/WcAppInfoUM.kt | 1 + .../entity/WcConnectedAppInfoUM.kt | 2 +- .../connections/entity/WcNetworkInfoItem.kt | 44 ++- .../connections/entity/WcSelectNetworksUM.kt | 11 + .../connections/model/WcAppInfoModel.kt | 54 ++- .../model/WcConnectedAppInfoModel.kt | 2 +- .../connections/model/WcConnectionsModel.kt | 3 - .../transformers/WcAppInfoTransformer.kt | 3 + .../transformers/WcNetworksInfoConverter.kt | 2 +- .../WcSelectNetworksCheckedTransformer.kt | 18 + ...WcSelectNetworksClearCheckedTransformer.kt | 13 + .../WcSelectNetworksTransformer.kt | 53 +++ .../connections/ui/WcAppInfoContent.kt | 26 +- .../connections/ui/WcConnectedAppInfoBS.kt | 13 +- 17 files changed, 652 insertions(+), 75 deletions(-) create mode 100644 features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/components/WcSelectNetworksComponent.kt create mode 100644 features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/entity/WcSelectNetworksUM.kt create mode 100644 features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/model/transformers/WcSelectNetworksCheckedTransformer.kt create mode 100644 features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/model/transformers/WcSelectNetworksClearCheckedTransformer.kt create mode 100644 features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/model/transformers/WcSelectNetworksTransformer.kt diff --git a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/components/WcAppInfoContainerComponent.kt b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/components/WcAppInfoContainerComponent.kt index be95b71cd3..ef47353704 100644 --- a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/components/WcAppInfoContainerComponent.kt +++ b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/components/WcAppInfoContainerComponent.kt @@ -74,7 +74,7 @@ internal class WcAppInfoContainerComponent( title = when (route) { is WcAppInfoRoutes.Alert -> null WcAppInfoRoutes.AppInfo -> resourceReference(R.string.wc_wallet_connect) - WcAppInfoRoutes.SelectNetworks -> TODO("[REDACTED_TASK_KEY]") + WcAppInfoRoutes.SelectNetworks -> stringReference("Choose networks") WcAppInfoRoutes.SelectWallet -> stringReference("Choose wallet") }, startIconRes = when (route) { @@ -100,10 +100,13 @@ internal class WcAppInfoContainerComponent( } private fun contentBack() { - if (contentStack.value.active.configuration == WcAppInfoRoutes.AppInfo) { - dismiss() - } else { - model.contentNavigation.pop() + when (contentStack.value.active.configuration) { + WcAppInfoRoutes.AppInfo -> dismiss() + WcAppInfoRoutes.SelectNetworks -> { + model.clearAvailableNetworks() + model.contentNavigation.pop() + } + else -> model.contentNavigation.pop() } } @@ -112,7 +115,10 @@ internal class WcAppInfoContainerComponent( return when (config) { is WcAppInfoRoutes.AppInfo -> WcAppInfoComponent(appComponentContext = appComponentContext, model = model) is WcAppInfoRoutes.Alert -> TODO() - WcAppInfoRoutes.SelectNetworks -> TODO() + WcAppInfoRoutes.SelectNetworks -> WcSelectNetworksComponent( + appComponentContext = appComponentContext, + model = model, + ) WcAppInfoRoutes.SelectWallet -> WcSelectWalletComponent( appComponentContext = appComponentContext, model = model, diff --git a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/components/WcSelectNetworksComponent.kt b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/components/WcSelectNetworksComponent.kt new file mode 100644 index 0000000000..8c12698940 --- /dev/null +++ b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/components/WcSelectNetworksComponent.kt @@ -0,0 +1,367 @@ +package com.tangem.features.walletconnect.connections.components + +import android.content.res.Configuration +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.HorizontalDivider +import androidx.compose.material3.Icon +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.key +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.tooling.preview.Devices +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.tooling.preview.PreviewParameter +import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider +import androidx.compose.ui.unit.dp +import androidx.compose.ui.util.fastForEach +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import com.tangem.common.ui.notifications.NotificationUM +import com.tangem.core.decompose.context.AppComponentContext +import com.tangem.core.ui.components.PrimaryButton +import com.tangem.core.ui.components.TangemSwitch +import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig +import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfigContent +import com.tangem.core.ui.components.bottomsheets.modal.TangemModalBottomSheet +import com.tangem.core.ui.components.bottomsheets.modal.TangemModalBottomSheetTitle +import com.tangem.core.ui.components.notifications.Notification +import com.tangem.core.ui.decompose.ComposableContentComponent +import com.tangem.core.ui.extensions.stringReference +import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.res.TangemThemePreview +import com.tangem.features.walletconnect.connections.entity.WcNetworkInfoItem +import com.tangem.features.walletconnect.connections.entity.WcSelectNetworksUM +import com.tangem.features.walletconnect.connections.model.WcAppInfoModel +import com.tangem.features.walletconnect.impl.R +import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.persistentListOf + +internal class WcSelectNetworksComponent( + appComponentContext: AppComponentContext, + private val model: WcAppInfoModel, +) : AppComponentContext by appComponentContext, ComposableContentComponent { + + @Composable + override fun Content(modifier: Modifier) { + val state by model.networksState.collectAsStateWithLifecycle() + WcSelectNetworksContent( + modifier = modifier.padding(horizontal = 16.dp), + state = state, + ) + } +} + +@Composable +private fun WcSelectNetworksContent(state: WcSelectNetworksUM, modifier: Modifier = Modifier) { + val blocksModifier = Modifier + .clip(RoundedCornerShape(TangemTheme.dimens.radius14)) + .background(color = TangemTheme.colors.background.action) + .fillMaxWidth() + Column(modifier = modifier, verticalArrangement = Arrangement.spacedBy(16.dp)) { + if (state.missing.isNotEmpty()) { + MissingRequiredBlock(modifier = blocksModifier, missingNetworks = state.missing) + } + if (state.required.isNotEmpty() || state.available.isNotEmpty()) { + AvailableNetworksBlock(modifier = blocksModifier, required = state.required, available = state.available) + } + if (state.notAdded.isNotEmpty()) { + NotAddedBlock(modifier = blocksModifier, notAdded = state.notAdded) + } + PrimaryButton( + modifier = Modifier + .padding(bottom = 16.dp) + .fillMaxWidth(), + text = "Done", + onClick = state.onDone, + enabled = state.missing.isEmpty(), + ) + } +} + +@Composable +private fun MissingRequiredBlock(missingNetworks: ImmutableList, modifier: Modifier = Modifier) { + val missingNetworksName = missingNetworks.joinToString { it.name } + val notificationUM = remember(missingNetworks) { + NotificationUM.Info( + title = stringReference("The wallet has no required networks"), + subtitle = stringReference("Add the $missingNetworksName network to your portfolio for this wallet."), + ) + } + Column(modifier = modifier) { + Notification( + config = notificationUM.config, + containerColor = TangemTheme.colors.background.action, + iconTint = TangemTheme.colors.icon.attention, + ) + HorizontalDivider( + modifier = modifier.fillMaxWidth(), + thickness = 1.dp, + color = TangemTheme.colors.stroke.primary, + ) + missingNetworks.fastForEach { network -> + key(network.id) { + NetworkItems(modifier = Modifier.padding(vertical = 14.dp, horizontal = 12.dp), networkItem = network) + } + } + } +} + +@Composable +private fun AvailableNetworksBlock( + required: ImmutableList, + available: ImmutableList, + modifier: Modifier = Modifier, +) { + Column(modifier = modifier) { + Text( + modifier = Modifier.padding(top = 12.dp, bottom = 4.dp, start = 12.dp, end = 12.dp), + text = "Available networks", + style = TangemTheme.typography.subtitle2, + color = TangemTheme.colors.text.tertiary, + ) + required.fastForEach { network -> + key(network.id) { + NetworkItems(modifier = Modifier.padding(vertical = 14.dp, horizontal = 12.dp), networkItem = network) + } + } + available.fastForEach { network -> + key(network.id) { + NetworkItems(modifier = Modifier.padding(vertical = 14.dp, horizontal = 12.dp), networkItem = network) + } + } + } +} + +@Composable +private fun NotAddedBlock(notAdded: ImmutableList, modifier: Modifier = Modifier) { + Column(modifier = modifier) { + Text( + modifier = Modifier.padding(top = 12.dp, bottom = 4.dp, start = 12.dp, end = 12.dp), + text = "Not Added", + style = TangemTheme.typography.subtitle2, + color = TangemTheme.colors.text.tertiary, + ) + notAdded.fastForEach { network -> + key(network.id) { + NetworkItems(modifier = Modifier.padding(vertical = 14.dp, horizontal = 12.dp), networkItem = network) + } + } + } +} + +@Composable +private fun NetworkItems(networkItem: WcNetworkInfoItem, modifier: Modifier = Modifier) { + Row(modifier = modifier, verticalAlignment = Alignment.CenterVertically) { + when (networkItem) { + is WcNetworkInfoItem.Checkable, + is WcNetworkInfoItem.Checked, + is WcNetworkInfoItem.Required, + -> Image( + modifier = Modifier.size(24.dp), + painter = painterResource(networkItem.icon), + contentDescription = null, + ) + is WcNetworkInfoItem.ReadOnly -> Icon( + modifier = Modifier + .size(24.dp) + .clip(CircleShape) + .background(TangemTheme.colors.button.secondary), + painter = painterResource(networkItem.icon), + contentDescription = null, + tint = TangemTheme.colors.icon.informative, + ) + } + NetworkNameAndSymbol( + modifier = Modifier + .weight(1f) + .padding(horizontal = 12.dp), + name = networkItem.name, + symbol = networkItem.symbol, + ) + when (networkItem) { + is WcNetworkInfoItem.Checkable -> TangemSwitch( + onCheckedChange = networkItem.onCheckedChange, + checked = networkItem.checked, + ) + is WcNetworkInfoItem.Required -> Text( + text = "Required", + style = TangemTheme.typography.body2, + color = TangemTheme.colors.text.tertiary, + ) + is WcNetworkInfoItem.Checked -> TangemSwitch( + onCheckedChange = {}, + enabled = false, + checked = true, + ) + is WcNetworkInfoItem.ReadOnly -> Unit + } + } +} + +@Composable +private fun NetworkNameAndSymbol(name: String, symbol: String, modifier: Modifier = Modifier) { + Row( + modifier = modifier, + horizontalArrangement = Arrangement.spacedBy(4.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + Text( + modifier = Modifier.weight(1f, fill = false), + text = name, + style = TangemTheme.typography.subtitle2, + color = TangemTheme.colors.text.primary1, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + ) + Text(text = symbol, style = TangemTheme.typography.caption1, color = TangemTheme.colors.text.tertiary) + } +} + +@Preview(showBackground = true, device = Devices.PIXEL_7_PRO) +@Preview(showBackground = true, device = Devices.PIXEL_7_PRO, uiMode = Configuration.UI_MODE_NIGHT_YES) +@Composable +private fun WcSelectNetworksContent_Preview( + @PreviewParameter(WcSelectNetworksProvider::class) + state: WcSelectNetworksUM, +) { + TangemThemePreview { + TangemModalBottomSheet( + containerColor = TangemTheme.colors.background.tertiary, + config = TangemBottomSheetConfig( + isShown = true, + onDismissRequest = {}, + content = TangemBottomSheetConfigContent.Empty, + ), + title = { + TangemModalBottomSheetTitle( + title = stringReference("Choose wallet"), + onEndClick = {}, + endIconRes = R.drawable.ic_close_24, + ) + }, + content = { + WcSelectNetworksContent( + modifier = Modifier + .background(TangemTheme.colors.background.tertiary) + .padding(horizontal = TangemTheme.dimens.spacing16), + state = state, + ) + }, + ) + } +} + +private class WcSelectNetworksProvider : CollectionPreviewParameterProvider( + collection = listOf( + WcSelectNetworksUM( + missing = persistentListOf( + WcNetworkInfoItem.Required( + id = "ethereum", + name = "EthereumEthereumEthereumEthereumEthereumEthereumEthereumEthereum", + symbol = "ETH", + icon = R.drawable.img_eth_22, + ), + WcNetworkInfoItem.Required( + id = "bitcoin", + name = "Bitcoin", + symbol = "BTC", + icon = R.drawable.img_btc_22, + ), + ), + required = persistentListOf( + WcNetworkInfoItem.Checked( + id = "optimism", + name = "Optimism", + symbol = "OP", + icon = R.drawable.img_optimism_22, + ), + ), + available = persistentListOf( + WcNetworkInfoItem.Checkable( + id = "cardano", + name = "Cardano", + symbol = "ADA", + icon = R.drawable.img_cardano_22, + checked = false, + onCheckedChange = {}, + ), + WcNetworkInfoItem.Checkable( + id = "polygon", + name = "Polygon", + symbol = "MATIC", + icon = R.drawable.img_polygon_22, + checked = true, + onCheckedChange = {}, + ), + ), + notAdded = persistentListOf( + WcNetworkInfoItem.ReadOnly( + id = "solana", + name = "SOLANA", + symbol = "SOL", + icon = R.drawable.ic_solana_16, + ), + WcNetworkInfoItem.ReadOnly( + id = "avalanche", + name = "Avalanche", + symbol = "AVAX", + icon = R.drawable.ic_avalanche_22, + ), + ), + onDone = {}, + ), + WcSelectNetworksUM( + missing = persistentListOf(), + required = persistentListOf( + WcNetworkInfoItem.Checked( + id = "optimism", + name = "Optimism", + symbol = "OP", + icon = R.drawable.img_optimism_22, + ), + ), + available = persistentListOf( + WcNetworkInfoItem.Checkable( + id = "cardano", + name = "Cardano", + symbol = "ADA", + icon = R.drawable.img_cardano_22, + checked = false, + onCheckedChange = {}, + ), + WcNetworkInfoItem.Checkable( + id = "polygon", + name = "Polygon", + symbol = "MATIC", + icon = R.drawable.img_polygon_22, + checked = true, + onCheckedChange = {}, + ), + ), + notAdded = persistentListOf( + WcNetworkInfoItem.ReadOnly( + id = "solana", + name = "SOLANA", + symbol = "SOL", + icon = R.drawable.ic_solana_16, + ), + WcNetworkInfoItem.ReadOnly( + id = "avalanche", + name = "Avalanche", + symbol = "AVAX", + icon = R.drawable.ic_avalanche_22, + ), + ), + onDone = {}, + ), + ), +) \ No newline at end of file diff --git a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/components/WcSelectWalletComponent.kt b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/components/WcSelectWalletComponent.kt index d6c7e19964..c810210b3e 100644 --- a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/components/WcSelectWalletComponent.kt +++ b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/components/WcSelectWalletComponent.kt @@ -1,7 +1,6 @@ package com.tangem.features.walletconnect.connections.components import android.content.res.Configuration -import androidx.compose.foundation.background import androidx.compose.foundation.border import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Column @@ -22,12 +21,17 @@ import com.tangem.common.ui.userwallet.UserWalletItem import com.tangem.common.ui.userwallet.state.UserWalletItemUM import com.tangem.core.decompose.context.AppComponentContext import com.tangem.core.ui.components.block.TangemBlockCardColors +import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig +import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfigContent +import com.tangem.core.ui.components.bottomsheets.modal.TangemModalBottomSheet +import com.tangem.core.ui.components.bottomsheets.modal.TangemModalBottomSheetTitle import com.tangem.core.ui.decompose.ComposableContentComponent import com.tangem.core.ui.extensions.stringReference import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreview import com.tangem.domain.wallets.models.UserWalletId import com.tangem.features.walletconnect.connections.model.WcAppInfoModel +import com.tangem.features.walletconnect.impl.R import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.persistentListOf @@ -86,45 +90,60 @@ private fun WcSelectWalletContent( @Preview(showBackground = true, device = Devices.PIXEL_7_PRO, uiMode = Configuration.UI_MODE_NIGHT_YES) private fun WcSelectWalletContent_Preview() { TangemThemePreview { - WcSelectWalletContent( - modifier = Modifier - .background(TangemTheme.colors.background.tertiary) - .padding(start = 16.dp, end = 16.dp, bottom = 16.dp), - selectedWalletId = UserWalletId("user_wallet_1".encodeToByteArray()), - wallets = persistentListOf( - UserWalletItemUM( - id = UserWalletId("user_wallet_1".encodeToByteArray()), - name = stringReference("Tangem 2.0"), - information = stringReference("42 tokens"), - balance = UserWalletItemUM.Balance.Loaded("1 496,34 \$", isFlickering = false), - isEnabled = true, - onClick = {}, - ), - UserWalletItemUM( - id = UserWalletId("user_wallet_2".encodeToByteArray()), - name = stringReference("Tangem White"), - information = stringReference("24 tokens"), - balance = UserWalletItemUM.Balance.Loaded("1 496,34 \$", isFlickering = false), - isEnabled = true, - onClick = {}, - ), - UserWalletItemUM( - id = UserWalletId("user_wallet_3".encodeToByteArray()), - name = stringReference("Bitcoin"), - information = stringReference("1 token"), - balance = UserWalletItemUM.Balance.Loaded("1 496,34 \$", isFlickering = false), - isEnabled = true, - onClick = {}, - ), - UserWalletItemUM( - id = UserWalletId("user_wallet_4".encodeToByteArray()), - name = stringReference("Tangem 1.0"), - information = stringReference("21 tokens"), - balance = UserWalletItemUM.Balance.Loaded("1 496,34 \$", isFlickering = false), - isEnabled = true, - onClick = {}, - ), + TangemModalBottomSheet( + containerColor = TangemTheme.colors.background.primary, + config = TangemBottomSheetConfig( + isShown = true, + onDismissRequest = {}, + content = TangemBottomSheetConfigContent.Empty, ), + title = { + TangemModalBottomSheetTitle( + title = stringReference("Choose wallet"), + onEndClick = {}, + endIconRes = R.drawable.ic_close_24, + ) + }, + content = { + WcSelectWalletContent( + modifier = Modifier.padding(start = 16.dp, end = 16.dp, bottom = 16.dp), + selectedWalletId = UserWalletId("user_wallet_1".encodeToByteArray()), + wallets = persistentListOf( + UserWalletItemUM( + id = UserWalletId("user_wallet_1".encodeToByteArray()), + name = stringReference("Tangem 2.0"), + information = stringReference("42 tokens"), + balance = UserWalletItemUM.Balance.Loaded("1 496,34 \$", isFlickering = false), + isEnabled = true, + onClick = {}, + ), + UserWalletItemUM( + id = UserWalletId("user_wallet_2".encodeToByteArray()), + name = stringReference("Tangem White"), + information = stringReference("24 tokens"), + balance = UserWalletItemUM.Balance.Loaded("1 496,34 \$", isFlickering = false), + isEnabled = true, + onClick = {}, + ), + UserWalletItemUM( + id = UserWalletId("user_wallet_3".encodeToByteArray()), + name = stringReference("Bitcoin"), + information = stringReference("1 token"), + balance = UserWalletItemUM.Balance.Loaded("1 496,34 \$", isFlickering = false), + isEnabled = true, + onClick = {}, + ), + UserWalletItemUM( + id = UserWalletId("user_wallet_4".encodeToByteArray()), + name = stringReference("Tangem 1.0"), + information = stringReference("21 tokens"), + balance = UserWalletItemUM.Balance.Loaded("1 496,34 \$", isFlickering = false), + isEnabled = true, + onClick = {}, + ), + ), + ) + }, ) } } \ No newline at end of file diff --git a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/entity/WcAppInfoUM.kt b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/entity/WcAppInfoUM.kt index 92e385a3fa..6cdcb1729e 100644 --- a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/entity/WcAppInfoUM.kt +++ b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/entity/WcAppInfoUM.kt @@ -28,6 +28,7 @@ internal sealed class WcAppInfoUM : TangemBottomSheetConfigContent { val walletName: String, val onWalletClick: () -> Unit, val networksInfo: WcNetworksInfo, + val onNetworksClick: () -> Unit, override val connectButtonConfig: WcPrimaryButtonConfig, override val onDismiss: () -> Unit, ) : WcAppInfoUM() diff --git a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/entity/WcConnectedAppInfoUM.kt b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/entity/WcConnectedAppInfoUM.kt index 5fe46765ca..a632be44c8 100644 --- a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/entity/WcConnectedAppInfoUM.kt +++ b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/entity/WcConnectedAppInfoUM.kt @@ -9,7 +9,7 @@ data class WcConnectedAppInfoUM( val isVerified: Boolean, val appSubtitle: String, val walletName: String, - val networks: ImmutableList, + val networks: ImmutableList, val disconnectButtonConfig: WcPrimaryButtonConfig, val onDismiss: () -> Unit, ) : TangemBottomSheetConfigContent \ No newline at end of file diff --git a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/entity/WcNetworkInfoItem.kt b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/entity/WcNetworkInfoItem.kt index 498b1b759c..5138a902a0 100644 --- a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/entity/WcNetworkInfoItem.kt +++ b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/entity/WcNetworkInfoItem.kt @@ -2,9 +2,41 @@ package com.tangem.features.walletconnect.connections.entity import androidx.annotation.DrawableRes -data class WcNetworkInfoItem( - val id: String, - @DrawableRes val icon: Int, - val name: String, - val symbol: String, -) \ No newline at end of file +sealed class WcNetworkInfoItem { + abstract val id: String + + @get:DrawableRes + abstract val icon: Int + abstract val name: String + abstract val symbol: String + + data class Required( + override val id: String, + override val icon: Int, + override val name: String, + override val symbol: String, + ) : WcNetworkInfoItem() + + data class Checked( + override val id: String, + override val icon: Int, + override val name: String, + override val symbol: String, + ) : WcNetworkInfoItem() + + data class Checkable( + override val id: String, + override val icon: Int, + override val name: String, + override val symbol: String, + val checked: Boolean, + val onCheckedChange: (Boolean) -> Unit, + ) : WcNetworkInfoItem() + + data class ReadOnly( + override val id: String, + override val icon: Int, + override val name: String, + override val symbol: String, + ) : WcNetworkInfoItem() +} \ No newline at end of file diff --git a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/entity/WcSelectNetworksUM.kt b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/entity/WcSelectNetworksUM.kt new file mode 100644 index 0000000000..a5daa0be5a --- /dev/null +++ b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/entity/WcSelectNetworksUM.kt @@ -0,0 +1,11 @@ +package com.tangem.features.walletconnect.connections.entity + +import kotlinx.collections.immutable.ImmutableList + +internal data class WcSelectNetworksUM( + val missing: ImmutableList, + val required: ImmutableList, + val available: ImmutableList, + val notAdded: ImmutableList, + val onDone: () -> Unit, +) \ No newline at end of file diff --git a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/model/WcAppInfoModel.kt b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/model/WcAppInfoModel.kt index fe9348ef4d..18581326b1 100644 --- a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/model/WcAppInfoModel.kt +++ b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/model/WcAppInfoModel.kt @@ -13,6 +13,7 @@ import com.tangem.core.decompose.ui.UiMessageSender import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase import com.tangem.domain.balancehiding.GetBalanceHidingSettingsUseCase import com.tangem.domain.tokens.GetWalletTotalBalanceUseCase +import com.tangem.domain.tokens.model.Network import com.tangem.domain.walletconnect.model.WcPairRequest import com.tangem.domain.walletconnect.model.WcSessionApprove import com.tangem.domain.walletconnect.model.WcSessionProposal @@ -26,15 +27,20 @@ import com.tangem.features.walletconnect.connections.components.WcAppInfoContain import com.tangem.features.walletconnect.connections.entity.WcAppInfoUM import com.tangem.features.walletconnect.connections.entity.WcAppInfoWalletUM import com.tangem.features.walletconnect.connections.entity.WcPrimaryButtonConfig +import com.tangem.features.walletconnect.connections.entity.WcSelectNetworksUM import com.tangem.features.walletconnect.connections.model.transformers.WcAppInfoTransformer import com.tangem.features.walletconnect.connections.model.transformers.WcAppInfoWalletChangedTransformer import com.tangem.features.walletconnect.connections.model.transformers.WcConnectButtonProgressTransformer +import com.tangem.features.walletconnect.connections.model.transformers.WcSelectNetworksClearCheckedTransformer +import com.tangem.features.walletconnect.connections.model.transformers.WcSelectNetworksCheckedTransformer +import com.tangem.features.walletconnect.connections.model.transformers.WcSelectNetworksTransformer import com.tangem.features.walletconnect.connections.routes.WcAppInfoRoutes import com.tangem.features.walletconnect.connections.utils.WcUserWalletsFetcher import com.tangem.utils.coroutines.CoroutineDispatcherProvider import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.persistentListOf import kotlinx.coroutines.flow.* +import timber.log.Timber import javax.inject.Inject import kotlin.properties.Delegates import com.tangem.utils.transformer.update as transformerUpdate @@ -75,7 +81,6 @@ internal class WcAppInfoModel @Inject constructor( private val selectedUserWalletFlow = MutableStateFlow(getWalletsUseCase.invokeSync().first { it.walletId == params.userWalletId }) - // TODO(wc) Doston: Temp solution, will be fixed in next PR`s private var proposalNetwork by Delegates.notNull() internal var sessionProposal by Delegates.notNull() @@ -85,6 +90,18 @@ internal class WcAppInfoModel @Inject constructor( field = MutableStateFlow(createLoadingState()) val walletsUiState: StateFlow field = MutableStateFlow(WcAppInfoWalletUM(persistentListOf(), selectedUserWalletFlow.value)) + val networksState: StateFlow + field = MutableStateFlow( + WcSelectNetworksUM( + missing = persistentListOf(), + required = persistentListOf(), + available = persistentListOf(), + notAdded = persistentListOf(), + onDone = ::onNetworksDone, + ), + ) + // TODO: [REDACTED_JIRA] Change it to UiManager + private val tempAvailableNetworksState = MutableStateFlow>(setOf()) init { loadDAppInfo() @@ -109,22 +126,24 @@ internal class WcAppInfoModel @Inject constructor( router.pop() } is WcPairState.Error -> { - // TODO: wc show toast/snackbar/alert? + Timber.e(state.error) } is WcPairState.Loading -> appInfoUiState.update { createLoadingState() } is WcPairState.Proposal -> { sessionProposal = state.dAppSession - proposalNetwork = sessionProposal.proposalNetwork.getValue(selectedUserWalletFlow.value) + val proposalNetwork = sessionProposal.proposalNetwork.getValue(selectedUserWalletFlow.value) appInfoUiState.transformerUpdate( WcAppInfoTransformer( dAppSession = state.dAppSession, onDismiss = ::dismiss, onConnect = ::onConnect, onWalletClick = { contentNavigation.pushNew(WcAppInfoRoutes.SelectWallet) }, + onNetworksClick = { contentNavigation.pushNew(WcAppInfoRoutes.SelectNetworks) }, userWallet = selectedUserWalletFlow.value, proposalNetwork = proposalNetwork, ), ) + updateNetworksState(proposalNetwork = proposalNetwork) } } } @@ -136,19 +155,27 @@ internal class WcAppInfoModel @Inject constructor( router.pop() } + fun clearAvailableNetworks() { + networksState.transformerUpdate(WcSelectNetworksClearCheckedTransformer) + tempAvailableNetworksState.update { setOf() } + } + private fun onConnect() { + val enabledAvailableNetworks = + proposalNetwork.available.filter { network -> network.id in tempAvailableNetworksState.value } wcPairUseCase.approve( WcSessionApprove( wallet = selectedUserWalletFlow.value, - network = proposalNetwork.required.plus(proposalNetwork.available).toList(), + network = enabledAvailableNetworks + proposalNetwork.required, ), ) } private fun onWalletSelected(userWalletId: UserWalletId) { val selectedUserWallet = sessionProposal.proposalNetwork.keys.first { it.walletId == userWalletId } + val proposalNetwork = sessionProposal.proposalNetwork.getValue(selectedUserWallet) selectedUserWalletFlow.update { selectedUserWallet } - proposalNetwork = sessionProposal.proposalNetwork.getValue(selectedUserWallet) + updateNetworksState(proposalNetwork) appInfoUiState.transformerUpdate(WcAppInfoWalletChangedTransformer(selectedUserWallet, proposalNetwork)) contentNavigation.pop() } @@ -157,6 +184,23 @@ internal class WcAppInfoModel @Inject constructor( walletsUiState.update { state -> state.copy(wallets = items, selectedUserWallet = userWallet) } } + private fun onNetworksDone() { + contentNavigation.pop() + } + + private fun updateNetworksState(proposalNetwork: WcSessionProposal.ProposalNetwork) { + this.proposalNetwork = proposalNetwork + networksState.transformerUpdate(WcSelectNetworksTransformer(proposalNetwork, ::onCheckedChange)) + tempAvailableNetworksState.update { setOf() } + } + + private fun onCheckedChange(isChecked: Boolean, networkId: String) { + tempAvailableNetworksState.update { + if (isChecked) it.plus(Network.ID(networkId)) else it.minus(Network.ID(networkId)) + } + networksState.transformerUpdate(WcSelectNetworksCheckedTransformer(networkId, isChecked)) + } + private fun createLoadingState(): WcAppInfoUM.Loading { return WcAppInfoUM.Loading( onDismiss = ::dismiss, diff --git a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/model/WcConnectedAppInfoModel.kt b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/model/WcConnectedAppInfoModel.kt index a751963826..0c49bc8dd9 100644 --- a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/model/WcConnectedAppInfoModel.kt +++ b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/model/WcConnectedAppInfoModel.kt @@ -60,7 +60,7 @@ internal class WcConnectedAppInfoModel @Inject constructor( walletName = session.wallet.name, networks = session.networks .map { - WcNetworkInfoItem( + WcNetworkInfoItem.Required( id = it.id.value, icon = it.iconResId, name = it.name, 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 96d5df7e28..0c7c5da768 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 @@ -33,7 +33,6 @@ import com.tangem.utils.transformer.update import kotlinx.collections.immutable.persistentListOf import kotlinx.coroutines.flow.* import kotlinx.coroutines.launch -import timber.log.Timber import javax.inject.Inject @Suppress("LongParameterList") @@ -77,10 +76,8 @@ internal class WcConnectionsModel @Inject constructor( private fun listenWcSessions() { wcSessionsUseCase.invoke() .conflate() - .onEach { Timber.tag("ddk9499").d("listenWcSessions before: ${it.size}") } .distinctUntilChanged() .onEach { sessionsMap -> - Timber.tag("ddk9499").d("listenWcSessions after: ${sessionsMap.size}") uiState.update( WcSessionsTransformer( sessionsMap = sessionsMap, diff --git a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/model/transformers/WcAppInfoTransformer.kt b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/model/transformers/WcAppInfoTransformer.kt index 2249504df5..e83438a16b 100644 --- a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/model/transformers/WcAppInfoTransformer.kt +++ b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/model/transformers/WcAppInfoTransformer.kt @@ -6,11 +6,13 @@ import com.tangem.domain.wallets.models.UserWallet import com.tangem.features.walletconnect.connections.entity.* import com.tangem.utils.transformer.Transformer +@Suppress("LongParameterList") internal class WcAppInfoTransformer( private val dAppSession: WcSessionProposal, private val onDismiss: () -> Unit, private val onConnect: () -> Unit, private val onWalletClick: () -> Unit, + private val onNetworksClick: () -> Unit, private val userWallet: UserWallet, private val proposalNetwork: WcSessionProposal.ProposalNetwork, ) : Transformer { @@ -25,6 +27,7 @@ internal class WcAppInfoTransformer( walletName = userWallet.name, onWalletClick = onWalletClick, networksInfo = WcNetworksInfoConverter.convert(proposalNetwork), + onNetworksClick = onNetworksClick, connectButtonConfig = WcPrimaryButtonConfig( showProgress = false, enabled = proposalNetwork.missingRequired.isEmpty(), diff --git a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/model/transformers/WcNetworksInfoConverter.kt b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/model/transformers/WcNetworksInfoConverter.kt index 8bf63c01cf..3cca2f81ee 100644 --- a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/model/transformers/WcNetworksInfoConverter.kt +++ b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/model/transformers/WcNetworksInfoConverter.kt @@ -18,7 +18,7 @@ internal object WcNetworksInfoConverter : Converter { + override fun transform(prevState: WcSelectNetworksUM): WcSelectNetworksUM { + return prevState.copy( + available = prevState.available + .map { item -> if (item.id == networkId) item.copy(checked = isChecked) else item } + .toImmutableList(), + ) + } +} \ No newline at end of file diff --git a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/model/transformers/WcSelectNetworksClearCheckedTransformer.kt b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/model/transformers/WcSelectNetworksClearCheckedTransformer.kt new file mode 100644 index 0000000000..01f61e377b --- /dev/null +++ b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/model/transformers/WcSelectNetworksClearCheckedTransformer.kt @@ -0,0 +1,13 @@ +package com.tangem.features.walletconnect.connections.model.transformers + +import com.tangem.features.walletconnect.connections.entity.WcSelectNetworksUM +import com.tangem.utils.transformer.Transformer +import kotlinx.collections.immutable.toImmutableList + +internal object WcSelectNetworksClearCheckedTransformer : Transformer { + override fun transform(prevState: WcSelectNetworksUM): WcSelectNetworksUM { + return prevState.copy( + available = prevState.available.map { it.copy(checked = false) }.toImmutableList(), + ) + } +} \ No newline at end of file diff --git a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/model/transformers/WcSelectNetworksTransformer.kt b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/model/transformers/WcSelectNetworksTransformer.kt new file mode 100644 index 0000000000..bb40167fcc --- /dev/null +++ b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/model/transformers/WcSelectNetworksTransformer.kt @@ -0,0 +1,53 @@ +package com.tangem.features.walletconnect.connections.model.transformers + +import com.tangem.core.ui.extensions.getGreyedOutIconRes +import com.tangem.core.ui.extensions.iconResId +import com.tangem.domain.walletconnect.model.WcSessionProposal +import com.tangem.features.walletconnect.connections.entity.WcNetworkInfoItem +import com.tangem.features.walletconnect.connections.entity.WcSelectNetworksUM +import com.tangem.utils.transformer.Transformer +import kotlinx.collections.immutable.toImmutableList + +internal class WcSelectNetworksTransformer( + private val proposalNetwork: WcSessionProposal.ProposalNetwork, + private val onCheckedChange: (Boolean, String) -> Unit, +) : Transformer { + override fun transform(prevState: WcSelectNetworksUM): WcSelectNetworksUM { + return prevState.copy( + missing = proposalNetwork.missingRequired.map { network -> + WcNetworkInfoItem.Required( + id = network.id.value, + icon = network.iconResId, + name = network.name, + symbol = network.currencySymbol, + ) + }.toImmutableList(), + required = proposalNetwork.required.map { network -> + WcNetworkInfoItem.Checked( + id = network.id.value, + icon = network.iconResId, + name = network.name, + symbol = network.currencySymbol, + ) + }.toImmutableList(), + available = proposalNetwork.available.map { network -> + WcNetworkInfoItem.Checkable( + id = network.id.value, + icon = network.iconResId, + name = network.name, + symbol = network.currencySymbol, + checked = false, + onCheckedChange = { onCheckedChange(it, network.id.value) }, + ) + }.toImmutableList(), + notAdded = proposalNetwork.notAdded.map { network -> + WcNetworkInfoItem.ReadOnly( + id = network.id.value, + icon = getGreyedOutIconRes(network.id.value), + name = network.name, + symbol = network.currencySymbol, + ) + }.toImmutableList(), + ) + } +} \ No newline at end of file diff --git a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/ui/WcAppInfoContent.kt b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/ui/WcAppInfoContent.kt index 136d5ac26b..e9cbbe442e 100644 --- a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/ui/WcAppInfoContent.kt +++ b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/ui/WcAppInfoContent.kt @@ -1,7 +1,6 @@ package com.tangem.features.walletconnect.connections.ui import android.content.res.Configuration -import androidx.compose.animation.animateContentSize import androidx.compose.foundation.Image import androidx.compose.foundation.background import androidx.compose.foundation.border @@ -95,7 +94,7 @@ private fun WcAppInfoModalBottomSheetContent(state: WcAppInfoUM.Content, modifie @Composable private fun WcAppInfoFirstBlock(state: WcAppInfoUM.Content, modifier: Modifier = Modifier) { var connectionRequestExpanded by remember { mutableStateOf(false) } - Column(modifier = modifier.animateContentSize()) { + Column(modifier = modifier) { WcAppInfoItem( iconUrl = state.appIcon, title = state.appName, @@ -228,8 +227,10 @@ private fun WcAppInfoSecondBlock(state: WcAppInfoUM.Content, modifier: Modifier ) HorizontalDivider(thickness = 1.dp, color = TangemTheme.colors.stroke.primary) SelectNetworksBlock( + modifier = Modifier + .clickable(onClick = state.onNetworksClick) + .then(itemsModifier), networksInfo = state.networksInfo, - modifier = itemsModifier, ) when (state.networksInfo) { is WcNetworksInfo.ContainsAllRequiredNetworks -> Unit @@ -562,7 +563,7 @@ private fun WcAppInfoBottomSheetPreview(@PreviewParameter(WcAppInfoStateProvider private class WcAppInfoStateProvider : CollectionPreviewParameterProvider( collection = listOf( - // WcAppInfoUM.Loading(onDismiss = {}, WcPrimaryButtonConfig(showProgress = false, enabled = false, onClick = {})), + WcAppInfoUM.Loading(onDismiss = {}, WcPrimaryButtonConfig(showProgress = false, enabled = false, onClick = {})), WcAppInfoUM.Content( appName = "React App", appIcon = "", @@ -573,26 +574,31 @@ private class WcAppInfoStateProvider : CollectionPreviewParameterProvider, modifier: Modifier = Modifier) { +private fun NetworksBlock(networks: ImmutableList, modifier: Modifier = Modifier) { Column(modifier = modifier) { Text( modifier = Modifier @@ -163,14 +163,19 @@ private fun WcConnectedAppInfoBS_Preview() { appSubtitle = "react-app.walletconnect.com", walletName = "Tangem 2.0", networks = persistentListOf( - WcNetworkInfoItem( + WcNetworkInfoItem.Required( id = "1", icon = R.drawable.img_optimism_22, name = "img_optimism_22img_optimism_22", symbol = "optimism", ), - WcNetworkInfoItem(id = "2", icon = R.drawable.img_bsc_22, name = "img_bsc_22", symbol = "bsc"), - WcNetworkInfoItem( + WcNetworkInfoItem.Required( + id = "2", + icon = R.drawable.img_bsc_22, + name = "img_bsc_22", + symbol = "bsc", + ), + WcNetworkInfoItem.Required( id = "3", icon = R.drawable.img_solana_22, name = "img_solana_22", From c470e8ef27bc847bdec507fe6a0dfa0e3b71a2d5 Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 12 May 2025 17:44:40 +0400 Subject: [PATCH 027/165] Updated on 2026-08-14 --- .../domain/token/MockCryptoCurrencyFactory.kt | 8 +- data/networks/build.gradle.kts | 28 +- .../DefaultSingleNetworkStatusFetcher.kt | 10 +- .../networks}/utils/NetworkStatusFactory.kt | 105 ++++--- .../utils/NetworkStatusFactoryTest.kt | 274 ++++++++++++++++++ data/tokens/build.gradle.kts | 38 ++- .../repository/DefaultNetworksRepository.kt | 15 +- .../domain/tokens/model/NetworkStatus.kt | 49 ++-- 8 files changed, 429 insertions(+), 98 deletions(-) rename data/{tokens/src/main/kotlin/com/tangem/data/tokens => networks/src/main/java/com/tangem/data/networks}/utils/NetworkStatusFactory.kt (58%) create mode 100644 data/networks/src/test/java/com/tangem/data/networks/utils/NetworkStatusFactoryTest.kt diff --git a/common/test/src/main/java/com/tangem/common/test/domain/token/MockCryptoCurrencyFactory.kt b/common/test/src/main/java/com/tangem/common/test/domain/token/MockCryptoCurrencyFactory.kt index 9af7183d2a..1c2dc2bf06 100644 --- a/common/test/src/main/java/com/tangem/common/test/domain/token/MockCryptoCurrencyFactory.kt +++ b/common/test/src/main/java/com/tangem/common/test/domain/token/MockCryptoCurrencyFactory.kt @@ -102,7 +102,13 @@ class MockCryptoCurrencyFactory(private val scanResponse: ScanResponse = default fun createToken(blockchain: Blockchain): CryptoCurrency { return factory.createToken( - sdkToken = Token(symbol = "NEVER-MIND", contractAddress = "NEVER-MIND", decimals = 8), + sdkToken = Token( + name = "NEVER-MIND", + symbol = "NEVER-MIND", + contractAddress = "NEVER-MIND", + decimals = 8, + id = "NEVER-MIND", + ), blockchain = blockchain, extraDerivationPath = null, scanResponse = scanResponse, diff --git a/data/networks/build.gradle.kts b/data/networks/build.gradle.kts index 079242811e..1dc873674c 100644 --- a/data/networks/build.gradle.kts +++ b/data/networks/build.gradle.kts @@ -10,36 +10,52 @@ android { } dependencies { - implementation(projects.libs.blockchainSdk) - + // region Project - Core implementation(projects.core.datasource) implementation(projects.core.utils) + // endregion + // region Project - Data implementation(projects.data.common) - implementation(projects.data.tokens) + // endregion + // region Project - Domain implementation(projects.domain.core) implementation(projects.domain.demo) implementation(projects.domain.legacy) implementation(projects.domain.models) implementation(projects.domain.networks) implementation(projects.domain.tokens.models) + implementation(projects.domain.txhistory.models) implementation(projects.domain.wallets) implementation(projects.domain.wallets.models) + // endregion + // region Project - Libs + implementation(projects.libs.blockchainSdk) + // endregion + + // region Tangem libraries + implementation(tangemDeps.blockchain) + // endregion + + // region DI implementation(deps.hilt.android) kapt(deps.hilt.kapt) + // endregion + // region Other libraries implementation(deps.androidx.datastore) implementation(deps.moshi) implementation(deps.timber) + // endregion - implementation(tangemDeps.blockchain) - + // region Tests testImplementation(deps.test.coroutine) testImplementation(deps.test.junit) testImplementation(deps.test.mockk) testImplementation(deps.test.truth) - testImplementation(tangemDeps.card.core) testImplementation(projects.common.test) + testImplementation(tangemDeps.card.core) + // endregion } \ No newline at end of file diff --git a/data/networks/src/main/java/com/tangem/data/networks/single/DefaultSingleNetworkStatusFetcher.kt b/data/networks/src/main/java/com/tangem/data/networks/single/DefaultSingleNetworkStatusFetcher.kt index 1a1ce6a081..ca3b03ba67 100644 --- a/data/networks/src/main/java/com/tangem/data/networks/single/DefaultSingleNetworkStatusFetcher.kt +++ b/data/networks/src/main/java/com/tangem/data/networks/single/DefaultSingleNetworkStatusFetcher.kt @@ -3,7 +3,7 @@ package com.tangem.data.networks.single import arrow.core.Either import com.tangem.data.common.currency.CardCryptoCurrencyFactory import com.tangem.data.networks.store.NetworksStatusesStoreV2 -import com.tangem.data.tokens.utils.NetworkStatusFactory +import com.tangem.data.networks.utils.NetworkStatusFactory import com.tangem.domain.core.utils.catchOn import com.tangem.domain.networks.single.SingleNetworkStatusFetcher import com.tangem.domain.tokens.model.CryptoCurrency @@ -31,8 +31,6 @@ internal class DefaultSingleNetworkStatusFetcher @Inject constructor( private val dispatchers: CoroutineDispatcherProvider, ) : SingleNetworkStatusFetcher { - private val networkStatusFactory = NetworkStatusFactory() - override suspend fun invoke(params: SingleNetworkStatusFetcher.Params) = Either.catchOn(dispatchers.default) { val networkCurrencies = when (params) { is SingleNetworkStatusFetcher.Params.Prepared -> params.addedNetworkCurrencies @@ -56,10 +54,10 @@ internal class DefaultSingleNetworkStatusFetcher @Inject constructor( ) } - val status = networkStatusFactory.createNetworkStatus( + val status = NetworkStatusFactory.create( network = params.network, - result = result, - currencies = networkCurrencies.toSet(), + updatingResult = result, + addedCurrencies = networkCurrencies.toSet(), ) val statusValue = status.value diff --git a/data/tokens/src/main/kotlin/com/tangem/data/tokens/utils/NetworkStatusFactory.kt b/data/networks/src/main/java/com/tangem/data/networks/utils/NetworkStatusFactory.kt similarity index 58% rename from data/tokens/src/main/kotlin/com/tangem/data/tokens/utils/NetworkStatusFactory.kt rename to data/networks/src/main/java/com/tangem/data/networks/utils/NetworkStatusFactory.kt index 5f781ffb46..3d70e5dfad 100644 --- a/data/tokens/src/main/kotlin/com/tangem/data/tokens/utils/NetworkStatusFactory.kt +++ b/data/networks/src/main/java/com/tangem/data/networks/utils/NetworkStatusFactory.kt @@ -1,4 +1,4 @@ -package com.tangem.data.tokens.utils +package com.tangem.data.networks.utils import com.tangem.domain.models.StatusSource import com.tangem.domain.tokens.model.* @@ -9,39 +9,79 @@ import com.tangem.domain.walletmanager.model.CryptoCurrencyTransaction import com.tangem.domain.walletmanager.model.UpdateWalletManagerResult import timber.log.Timber -class NetworkStatusFactory { +/** Factory for creating [NetworkStatus] */ +object NetworkStatusFactory { - fun createNetworkStatus( + /** + * Create [NetworkStatus] + * + * @param network network + * @param updatingResult result of updating wallet manager + * @param addedCurrencies added currencies + */ + fun create( network: Network, - result: UpdateWalletManagerResult, - currencies: Set, + updatingResult: UpdateWalletManagerResult, + addedCurrencies: Set, ): NetworkStatus { return NetworkStatus( network = network, - value = when (result) { + value = when (updatingResult) { is UpdateWalletManagerResult.MissedDerivation -> NetworkStatus.MissedDerivation - is UpdateWalletManagerResult.Unreachable -> NetworkStatus.Unreachable( - address = getNetworkAddressOrNull(result.selectedAddress, result.addresses), - ) - is UpdateWalletManagerResult.NoAccount -> NetworkStatus.NoAccount( - address = getNetworkAddress(result.selectedAddress, result.addresses), - amountToCreateAccount = result.amountToCreateAccount, - errorMessage = result.errorMessage, - source = StatusSource.ACTUAL, - ) - is UpdateWalletManagerResult.Verified -> NetworkStatus.Verified( - address = getNetworkAddress(result.selectedAddress, result.addresses), - amounts = formatAmounts(result.currenciesAmounts, currencies), - pendingTransactions = formatTransactions( - transactions = result.currentTransactions, - currencies = currencies, - ), - source = StatusSource.ACTUAL, - ) + is UpdateWalletManagerResult.Unreachable -> createUnreachableStatus(result = updatingResult) + is UpdateWalletManagerResult.NoAccount -> createNoAccount(result = updatingResult) + is UpdateWalletManagerResult.Verified -> { + createVerifiedStatus(result = updatingResult, addedCurrencies = addedCurrencies) + } }, ) } + private fun createUnreachableStatus(result: UpdateWalletManagerResult.Unreachable): NetworkStatus.Unreachable { + return NetworkStatus.Unreachable( + address = getNetworkAddressOrNull( + selectedAddress = result.selectedAddress, + availableAddresses = result.addresses, + ), + ) + } + + private fun createNoAccount(result: UpdateWalletManagerResult.NoAccount): NetworkStatus.NoAccount { + return NetworkStatus.NoAccount( + address = getNetworkAddress( + selectedAddress = result.selectedAddress, + availableAddresses = result.addresses, + ), + amountToCreateAccount = result.amountToCreateAccount, + errorMessage = result.errorMessage, + source = StatusSource.ACTUAL, + ) + } + + private fun createVerifiedStatus( + result: UpdateWalletManagerResult.Verified, + addedCurrencies: Set, + ): NetworkStatus.Verified { + return NetworkStatus.Verified( + address = getNetworkAddress( + selectedAddress = result.selectedAddress, + availableAddresses = result.addresses, + ), + amounts = formatAmounts(amounts = result.currenciesAmounts, currencies = addedCurrencies), + pendingTransactions = formatTransactions( + transactions = result.currentTransactions, + currencies = addedCurrencies, + ), + source = StatusSource.ACTUAL, + ) + } + + private fun getNetworkAddressOrNull(selectedAddress: String?, availableAddresses: Set
?): NetworkAddress? { + if (selectedAddress.isNullOrBlank() || availableAddresses == null) return null + + return getNetworkAddress(selectedAddress, availableAddresses) + } + private fun formatAmounts( amounts: Set, currencies: Set, @@ -97,23 +137,20 @@ class NetworkStatusFactory { return transactions.mapTo(hashSetOf()) { it.txHistoryItem } } - private fun getNetworkAddressOrNull(selectedAddress: String?, availableAddresses: Set
?): NetworkAddress? { - if (selectedAddress == null || availableAddresses == null) { - return null - } - - return getNetworkAddress(selectedAddress, availableAddresses) - } - private fun getNetworkAddress(selectedAddress: String, availableAddresses: Set
): NetworkAddress { val defaultAddress = availableAddresses .firstOrNull { it.value == selectedAddress } ?.let(::mapToDomainAddress) - requireNotNull(defaultAddress) { "Selected address must not be null" } + require(defaultAddress != null && defaultAddress.value.isNotBlank()) { + "Selected address must not be null" + } return if (availableAddresses.size != 1) { - NetworkAddress.Selectable(defaultAddress, availableAddresses.mapTo(hashSetOf(), ::mapToDomainAddress)) + NetworkAddress.Selectable( + defaultAddress = defaultAddress, + availableAddresses = availableAddresses.mapTo(hashSetOf(), ::mapToDomainAddress), + ) } else { NetworkAddress.Single(defaultAddress) } diff --git a/data/networks/src/test/java/com/tangem/data/networks/utils/NetworkStatusFactoryTest.kt b/data/networks/src/test/java/com/tangem/data/networks/utils/NetworkStatusFactoryTest.kt new file mode 100644 index 0000000000..8e21f85ff3 --- /dev/null +++ b/data/networks/src/test/java/com/tangem/data/networks/utils/NetworkStatusFactoryTest.kt @@ -0,0 +1,274 @@ +package com.tangem.data.networks.utils + +import com.google.common.truth.Truth +import com.tangem.blockchain.common.Blockchain +import com.tangem.common.test.domain.token.MockCryptoCurrencyFactory +import com.tangem.domain.models.StatusSource +import com.tangem.domain.tokens.model.* +import com.tangem.domain.txhistory.models.TxHistoryItem +import com.tangem.domain.walletmanager.model.Address +import com.tangem.domain.walletmanager.model.CryptoCurrencyAmount +import com.tangem.domain.walletmanager.model.CryptoCurrencyTransaction +import com.tangem.domain.walletmanager.model.UpdateWalletManagerResult +import org.junit.Test +import org.junit.runner.RunWith +import org.junit.runners.Parameterized +import java.math.BigDecimal + +/** +[REDACTED_AUTHOR] + */ +@RunWith(Parameterized::class) +internal class NetworkStatusFactoryTest(private val model: Model) { + + @Test + fun test() { + val actual = runCatching { + NetworkStatusFactory.create( + network = model.network, + updatingResult = model.result, + addedCurrencies = model.currencies, + ) + } + + actual + .onSuccess { + Truth.assertThat(actual).isEqualTo(model.expected) + } + .onFailure { + Truth.assertThat(actual.exceptionOrNull()).isInstanceOf(it::class.java) + Truth.assertThat(actual.exceptionOrNull()).hasMessageThat().isEqualTo(it.message) + } + } + + data class Model( + val network: Network, + val result: UpdateWalletManagerResult, + val currencies: Set, + val expected: Result, + ) + + private companion object { + + val selectedAddressThrowable = IllegalArgumentException("Selected address must not be null") + + val currencies = with(MockCryptoCurrencyFactory()) { setOf(ethereum, createToken(Blockchain.Ethereum)) } + + val txHistoryItem = TxHistoryItem( + txHash = "erroribus", + timestampInMillis = 2771, + isOutgoing = false, + destinationType = TxHistoryItem.DestinationType.Single( + addressType = TxHistoryItem.AddressType.User("0x1"), + ), + sourceType = TxHistoryItem.SourceType.Single("0x2"), + interactionAddressType = null, + status = TxHistoryItem.TransactionStatus.Confirmed, + type = TxHistoryItem.TransactionType.Transfer, + amount = BigDecimal.ONE, + ) + + @JvmStatic + @Parameterized.Parameters + fun data(): Collection = listOf( + // region MissedDerivation + createSuccess( + result = UpdateWalletManagerResult.MissedDerivation, + status = NetworkStatus.MissedDerivation, + ), + // endregion + + // region Unreachable + createSuccess( + result = UpdateWalletManagerResult.Unreachable(selectedAddress = null, addresses = null), + status = NetworkStatus.Unreachable(address = null), + ), + createSuccess( + result = UpdateWalletManagerResult.Unreachable( + selectedAddress = "", + addresses = emptySet(), + ), + status = NetworkStatus.Unreachable(address = null), + ), + createSuccess( + result = UpdateWalletManagerResult.Unreachable( + selectedAddress = "", + addresses = setOf(Address(value = "", type = Address.Type.Primary)), + ), + status = NetworkStatus.Unreachable(address = null), + ), + createSuccess( + result = UpdateWalletManagerResult.Unreachable( + selectedAddress = "0x1", + addresses = setOf(Address(value = "0x1", type = Address.Type.Primary)), + ), + status = NetworkStatus.Unreachable( + address = NetworkAddress.Single( + defaultAddress = NetworkAddress.Address( + value = "0x1", + type = NetworkAddress.Address.Type.Primary, + ), + ), + ), + ), + createFailure( + result = UpdateWalletManagerResult.Unreachable( + selectedAddress = "0x1", + addresses = emptySet(), + ), + throwable = selectedAddressThrowable, + ), + // endregion + + // region NoAccount + createFailure( + result = UpdateWalletManagerResult.NoAccount( + selectedAddress = "", + addresses = emptySet(), + amountToCreateAccount = BigDecimal.ZERO, + errorMessage = "", + ), + throwable = selectedAddressThrowable, + ), + createFailure( + result = UpdateWalletManagerResult.NoAccount( + selectedAddress = "", + addresses = setOf(Address(value = "", type = Address.Type.Primary)), + amountToCreateAccount = BigDecimal.ZERO, + errorMessage = "", + ), + throwable = selectedAddressThrowable, + ), + createFailure( + result = UpdateWalletManagerResult.NoAccount( + selectedAddress = "0x1", + addresses = setOf(Address(value = "0x2", type = Address.Type.Primary)), + amountToCreateAccount = BigDecimal.ZERO, + errorMessage = "", + ), + throwable = selectedAddressThrowable, + ), + createSuccess( + result = UpdateWalletManagerResult.NoAccount( + selectedAddress = "0x1", + addresses = setOf(Address(value = "0x1", type = Address.Type.Primary)), + amountToCreateAccount = BigDecimal.ZERO, + errorMessage = "", + ), + status = NetworkStatus.NoAccount( + address = NetworkAddress.Single( + defaultAddress = NetworkAddress.Address( + value = "0x1", + type = NetworkAddress.Address.Type.Primary, + ), + ), + amountToCreateAccount = BigDecimal.ZERO, + errorMessage = "", + source = StatusSource.ACTUAL, + ), + ), + // endregion + + // region Verified + createFailure( + result = UpdateWalletManagerResult.Verified( + selectedAddress = "", + addresses = emptySet(), + currenciesAmounts = emptySet(), + currentTransactions = emptySet(), + ), + throwable = selectedAddressThrowable, + ), + createFailure( + result = UpdateWalletManagerResult.Verified( + selectedAddress = "", + addresses = setOf(Address(value = "", type = Address.Type.Primary)), + currenciesAmounts = emptySet(), + currentTransactions = emptySet(), + ), + throwable = selectedAddressThrowable, + ), + createFailure( + result = UpdateWalletManagerResult.Verified( + selectedAddress = "0x1", + addresses = setOf(Address(value = "0x2", type = Address.Type.Primary)), + currenciesAmounts = emptySet(), + currentTransactions = emptySet(), + ), + throwable = selectedAddressThrowable, + ), + createFailure( + result = UpdateWalletManagerResult.Verified( + selectedAddress = "0x1", + addresses = setOf(Address(value = "0x2", type = Address.Type.Primary)), + currenciesAmounts = emptySet(), + currentTransactions = emptySet(), + ), + throwable = selectedAddressThrowable, + ), + createSuccess( + result = UpdateWalletManagerResult.Verified( + selectedAddress = "0x1", + addresses = setOf(Address(value = "0x1", type = Address.Type.Primary)), + currenciesAmounts = setOf( + CryptoCurrencyAmount.Coin(value = BigDecimal.ONE), + ), + currentTransactions = setOf( + CryptoCurrencyTransaction.Coin(txHistoryItem), + ), + ), + currencies = currencies, + status = NetworkStatus.Verified( + address = NetworkAddress.Single( + defaultAddress = NetworkAddress.Address( + value = "0x1", + type = NetworkAddress.Address.Type.Primary, + ), + ), + amounts = mapOf( + currencies.first().id to CryptoCurrencyAmountStatus.Loaded(BigDecimal.ONE), + currencies.last().id to CryptoCurrencyAmountStatus.NotFound, + ), + pendingTransactions = mapOf( + currencies.first().id to setOf(txHistoryItem), + currencies.last().id to setOf(), + ), + source = StatusSource.ACTUAL, + ), + ), + // endregion + ) + + fun createSuccess( + result: UpdateWalletManagerResult, + currencies: Set = emptySet(), + status: NetworkStatus.Value, + ): Model { + val network = MockCryptoCurrencyFactory().ethereum.network + + return Model( + network = network, + result = result, + currencies = currencies, + expected = Result.success( + value = NetworkStatus(network = network, value = status), + ), + ) + } + + fun createFailure( + result: UpdateWalletManagerResult, + currencies: Set = emptySet(), + throwable: Throwable, + ): Model { + val network = MockCryptoCurrencyFactory().ethereum.network + + return Model( + network = network, + result = result, + currencies = currencies, + expected = Result.failure(throwable), + ) + } + } +} \ No newline at end of file diff --git a/data/tokens/build.gradle.kts b/data/tokens/build.gradle.kts index 8eb2673b05..1a51e2cbc9 100644 --- a/data/tokens/build.gradle.kts +++ b/data/tokens/build.gradle.kts @@ -14,44 +14,52 @@ android { dependencies { - /** Project - Domain */ + // region Project - Data + implementation(projects.data.common) + implementation(projects.data.networks) + // endregion + + // region Project - Domain implementation(projects.domain.core) implementation(projects.domain.demo) + implementation(projects.domain.legacy) implementation(projects.domain.models) + implementation(projects.domain.staking) + implementation(projects.domain.staking.models) implementation(projects.domain.tokens) implementation(projects.domain.tokens.models) implementation(projects.domain.txhistory.models) implementation(projects.domain.wallets.models) - implementation(projects.domain.staking.models) - implementation(projects.domain.staking) + // endregion - /** Project - Data */ + // region Project - Utils implementation(projects.core.datasource) - implementation(projects.data.common) - - /** Project - Utils */ implementation(projects.core.utils) - implementation(projects.domain.legacy) implementation(projects.libs.blockchainSdk) + // endregion - /** Tangem SDKs */ + // region Tangem SDKs implementation(tangemDeps.blockchain) implementation(tangemDeps.card.core) + // endregion - /** AndroidX */ + // region AndroidX implementation(deps.androidx.datastore) + implementation(deps.androidx.paging.runtime) + // endregion - /** DI */ + // region DI implementation(deps.hilt.core) kapt(deps.hilt.kapt) + // endregion - /** Other */ + // region Other + implementation(deps.jodatime) implementation(deps.kotlin.coroutines) implementation(deps.moshi.kotlin) - implementation(deps.jodatime) - implementation(deps.timber) implementation(deps.retrofit) // For HttpException - implementation(deps.androidx.paging.runtime) + implementation(deps.timber) ksp(deps.moshi.kotlin.codegen) kaptForObfuscatingVariants(deps.retrofit.response.type.keeper) + // endregion } \ No newline at end of file diff --git a/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultNetworksRepository.kt b/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultNetworksRepository.kt index 80acea39bf..4426e81e54 100644 --- a/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultNetworksRepository.kt +++ b/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultNetworksRepository.kt @@ -7,7 +7,7 @@ import com.tangem.blockchainsdk.utils.fromNetworkId import com.tangem.data.common.cache.CacheRegistry import com.tangem.data.common.currency.CardCryptoCurrencyFactory import com.tangem.data.common.currency.ResponseCryptoCurrenciesFactory -import com.tangem.data.tokens.utils.NetworkStatusFactory +import com.tangem.data.networks.utils.NetworkStatusFactory import com.tangem.datasource.api.tangemTech.models.UserTokensResponse import com.tangem.datasource.local.network.NetworksStatusesStore import com.tangem.datasource.local.preferences.AppPreferencesStore @@ -43,7 +43,6 @@ internal class DefaultNetworksRepository( ) : NetworksRepository { private val responseCurrenciesFactory = ResponseCryptoCurrenciesFactory(excludedBlockchains) - private val networkStatusFactory = NetworkStatusFactory() override fun getNetworkStatusesUpdates( userWalletId: UserWalletId, @@ -168,10 +167,10 @@ internal class DefaultNetworksRepository( invalidateCacheKeyIfNeeded(userWalletId, network, result) } - val networkStatus = networkStatusFactory.createNetworkStatus( + val networkStatus = NetworkStatusFactory.create( network = network, - result = result, - currencies = networkCurrencies.toSet(), + updatingResult = result, + addedCurrencies = networkCurrencies.toSet(), ) networksStatusesStore.store(userWalletId, networkStatus) @@ -191,10 +190,10 @@ internal class DefaultNetworksRepository( invalidateCacheKeyIfNeeded(userWalletId, network, result) } - val networkStatus = networkStatusFactory.createNetworkStatus( + val networkStatus = NetworkStatusFactory.create( network = network, - result = result, - currencies = currencies.toSet(), + updatingResult = result, + addedCurrencies = currencies.toSet(), ) networksStatusesStore.store(userWalletId, networkStatus) diff --git a/domain/tokens/models/src/main/java/com/tangem/domain/tokens/model/NetworkStatus.kt b/domain/tokens/models/src/main/java/com/tangem/domain/tokens/model/NetworkStatus.kt index 6bacb5067b..5b03354e2d 100644 --- a/domain/tokens/models/src/main/java/com/tangem/domain/tokens/model/NetworkStatus.kt +++ b/domain/tokens/models/src/main/java/com/tangem/domain/tokens/model/NetworkStatus.kt @@ -5,23 +5,18 @@ import com.tangem.domain.txhistory.models.TxHistoryItem import java.math.BigDecimal /** - * Represents the status of a specific blockchain network. + * Represents the status of a specific blockchain network * - * @property network The network for which the status is provided. - * @property value The specific status value, represented as a sealed class to encapsulate the various possible states of the network. + * @property network the network for which the status is provided + * @property value the specific status value, represented as a sealed class to encapsulate the various possible + * states of the network */ -data class NetworkStatus( - val network: Network, - val value: Value, -) { +data class NetworkStatus(val network: Network, val value: Value) { - /** - * Represents the various possible statuses of a network. - * - * This sealed class includes different states like unreachable, missed derivation, verified, and no account. - */ + /** Represents the various possible statuses of a network */ sealed class Value { + /** Status source */ abstract val source: StatusSource fun copySealed(source: StatusSource): Value { @@ -36,30 +31,28 @@ data class NetworkStatus( } /** - * Represents the state where the network is unreachable. + * Represents the state where the network is unreachable * - * @property address Network addresses. + * @property address network address */ data class Unreachable(val address: NetworkAddress?) : Value() { override val source: StatusSource = StatusSource.ACTUAL } - /** - * Represents the state where a derivation has been missed. - */ + /** Represents the state where a derivation has been missed */ data object MissedDerivation : Value() { override val source: StatusSource = StatusSource.ACTUAL } /** * Represents the verified state of the network, including the amounts associated with different cryptocurrencies - * and whether there are transactions in progress. + * and whether there are transactions in progress * - * @property address Network addresses. - * @property amounts A map containing the amounts associated with different cryptocurrencies within the network. - * @property pendingTransactions A map containing pending transactions associated with different cryptocurrencies - * @property source source of data - * within the network. + * @property address network address + * @property amounts a map containing the amounts associated with different cryptocurrencies within the + * network + * @property pendingTransactions a map containing pending transactions associated with different cryptocurrencies + * @property source source of data */ data class Verified( val address: NetworkAddress, @@ -69,12 +62,12 @@ data class NetworkStatus( ) : Value() /** - * Represents the state where there is no account, and an amount is required to create one. + * Represents the state where there is no account, and an amount is required to create one * - * @property address Network addresses. - * @property amountToCreateAccount The amount required to create an account within the network. - * @property errorMessage error message - * @property source source of data + * @property address network address + * @property amountToCreateAccount the amount required to create an account within the network + * @property errorMessage error message + * @property source source of data */ data class NoAccount( val address: NetworkAddress, From 2b12514ee1cabec42fec8e185db1cb57623c475e Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 13 May 2025 16:29:46 +0700 Subject: [PATCH 028/165] Updated on 2026-08-14 --- data/wallet-connect/build.gradle.kts | 1 + .../di/WalletConnectDataModule.kt | 8 +- .../initialize/DefaultWcInitializeUseCase.kt | 12 ++ .../ethereum/WcEthMessageSignUseCase.kt | 3 + .../ethereum/WcEthSendTransactionUseCase.kt | 2 + .../ethereum/WcEthSignTransactionUseCase.kt | 2 + .../ethereum/WcEthSignTypedDataUseCase.kt | 3 + ...efaultWcSolanaSignAllTransactionUseCase.kt | 2 + .../DefaultWcSolanaSignTransactionUseCase.kt | 2 + .../solana/WcSolanaMessageSignUseCase.kt | 2 + .../pair/DefaultWcPairUseCase.kt | 53 +++++- .../request/DefaultWcRequestService.kt | 4 + .../respond/DefaultWcRespondService.kt | 25 +-- .../walletconnect/respond/WcRespondService.kt | 1 - .../sessions/DefaultWcSessionsManager.kt | 2 + .../walletconnect/sign/BaseWcSignUseCase.kt | 26 ++- .../sign/WcSignUseCaseDelegate.kt | 25 +++ .../data/walletconnect/utils/WcSdkObserver.kt | 2 + .../walletconnect/DefaultWcPairUseCaseTest.kt | 3 + .../WcSignUseCaseDelegateTest.kt | 70 ++++++-- domain/wallet-connect/build.gradle.kts | 3 + .../domain/walletconnect/WcAnalyticEvents.kt | 162 ++++++++++++++++++ .../usecase/disconnect/WcDisconnectUseCase.kt | 14 +- .../usecase/method/WcMessageSignUseCase.kt | 2 +- 24 files changed, 381 insertions(+), 48 deletions(-) create mode 100644 domain/wallet-connect/src/main/kotlin/com/tangem/domain/walletconnect/WcAnalyticEvents.kt diff --git a/data/wallet-connect/build.gradle.kts b/data/wallet-connect/build.gradle.kts index 2b9321f602..dfdfe71d2c 100644 --- a/data/wallet-connect/build.gradle.kts +++ b/data/wallet-connect/build.gradle.kts @@ -30,6 +30,7 @@ dependencies { /* Project - Core */ implementation(projects.core.utils) + implementation(projects.core.analytics) /* DI */ implementation(deps.hilt.core) diff --git a/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/di/WalletConnectDataModule.kt b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/di/WalletConnectDataModule.kt index 84a40f03e0..f1fe67b26a 100644 --- a/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/di/WalletConnectDataModule.kt +++ b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/di/WalletConnectDataModule.kt @@ -3,6 +3,7 @@ package com.tangem.data.walletconnect.di import android.app.Application import com.squareup.moshi.Moshi import com.tangem.blockchainsdk.utils.ExcludedBlockchains +import com.tangem.core.analytics.api.AnalyticsEventHandler import com.tangem.data.walletconnect.DefaultWalletConnectRepository import com.tangem.data.walletconnect.initialize.DefaultWcInitializeUseCase import com.tangem.data.walletconnect.network.ethereum.WcEthNetwork @@ -213,8 +214,11 @@ internal object WalletConnectDataModule { @Provides @Singleton - fun providesWcDisconnectUseCase(sessionsManager: WcSessionsManager): WcDisconnectUseCase { - return WcDisconnectUseCase(sessionsManager) + fun providesWcDisconnectUseCase( + sessionsManager: WcSessionsManager, + analytics: AnalyticsEventHandler, + ): WcDisconnectUseCase { + return WcDisconnectUseCase(sessionsManager, analytics) } internal class DiHelperBox( diff --git a/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/initialize/DefaultWcInitializeUseCase.kt b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/initialize/DefaultWcInitializeUseCase.kt index 91db31c6c8..1e6e533e5d 100644 --- a/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/initialize/DefaultWcInitializeUseCase.kt +++ b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/initialize/DefaultWcInitializeUseCase.kt @@ -10,6 +10,7 @@ import com.tangem.data.walletconnect.pair.WcPairSdkDelegate import com.tangem.data.walletconnect.request.DefaultWcRequestService import com.tangem.data.walletconnect.sessions.DefaultWcSessionsManager import com.tangem.data.walletconnect.utils.WcSdkObserver +import com.tangem.data.walletconnect.utils.WC_TAG import com.tangem.domain.walletconnect.usecase.initialize.WcInitializeUseCase import timber.log.Timber @@ -56,6 +57,7 @@ internal class DefaultWcInitializeUseCase( val walletDelegate = defineWalletDelegate() WalletKit.setWalletDelegate(walletDelegate) wcSdkObservers.forEach { it.onWcSdkInit() } + Timber.tag(WC_TAG).i("onWcSdkInit") }, onError = { error -> Timber.e("Error while initializing Web3Wallet: $error") @@ -68,26 +70,32 @@ internal class DefaultWcInitializeUseCase( get() = super.onSessionAuthenticate override fun onConnectionStateChange(state: Wallet.Model.ConnectionState) { + Timber.tag(WC_TAG).i("sdk callback onConnectionStateChange isAvailable=${state.isAvailable}") wcSdkObservers.forEach { it.onConnectionStateChange(state) } } override fun onError(error: Wallet.Model.Error) { + Timber.tag(WC_TAG).e(error.throwable, "sdk callback onError") wcSdkObservers.forEach { it.onError(error) } } override fun onProposalExpired(proposal: Wallet.Model.ExpiredProposal) { + Timber.tag(WC_TAG).i("sdk callback onProposalExpired $proposal") wcSdkObservers.forEach { it.onProposalExpired(proposal) } } override fun onRequestExpired(request: Wallet.Model.ExpiredRequest) { + Timber.tag(WC_TAG).i("sdk callback onRequestExpired $request") wcSdkObservers.forEach { it.onRequestExpired(request) } } override fun onSessionDelete(sessionDelete: Wallet.Model.SessionDelete) { + Timber.tag(WC_TAG).i("sdk callback onSessionDelete $sessionDelete") wcSdkObservers.forEach { it.onSessionDelete(sessionDelete) } } override fun onSessionExtend(session: Wallet.Model.Session) { + Timber.tag(WC_TAG).i("sdk callback onSessionExtend $session") wcSdkObservers.forEach { it.onSessionExtend(session) } } @@ -95,6 +103,7 @@ internal class DefaultWcInitializeUseCase( sessionProposal: Wallet.Model.SessionProposal, verifyContext: Wallet.Model.VerifyContext, ) { + Timber.tag(WC_TAG).i("sdk callback onSessionProposal $sessionProposal") wcSdkObservers.forEach { it.onSessionProposal(sessionProposal, verifyContext) } } @@ -102,14 +111,17 @@ internal class DefaultWcInitializeUseCase( sessionRequest: Wallet.Model.SessionRequest, verifyContext: Wallet.Model.VerifyContext, ) { + Timber.tag(WC_TAG).i("sdk callback onSessionRequest $sessionRequest") wcSdkObservers.forEach { it.onSessionRequest(sessionRequest, verifyContext) } } override fun onSessionSettleResponse(settleSessionResponse: Wallet.Model.SettledSessionResponse) { + Timber.tag(WC_TAG).i("sdk callback onSessionSettleResponse $settleSessionResponse") wcSdkObservers.forEach { it.onSessionSettleResponse(settleSessionResponse) } } override fun onSessionUpdateResponse(sessionUpdateResponse: Wallet.Model.SessionUpdateResponse) { + Timber.tag(WC_TAG).i("sdk callback onSessionUpdateResponse $sessionUpdateResponse") wcSdkObservers.forEach { it.onSessionUpdateResponse(sessionUpdateResponse) } } } diff --git a/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/network/ethereum/WcEthMessageSignUseCase.kt b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/network/ethereum/WcEthMessageSignUseCase.kt index 817e5a2ddc..b1ec5e840d 100644 --- a/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/network/ethereum/WcEthMessageSignUseCase.kt +++ b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/network/ethereum/WcEthMessageSignUseCase.kt @@ -9,6 +9,7 @@ import com.tangem.blockchain.extensions.isAscii import com.tangem.common.extensions.hexToBytes import com.tangem.common.extensions.toDecompressedPublicKey import com.tangem.common.extensions.toHexString +import com.tangem.core.analytics.api.AnalyticsEventHandler import com.tangem.data.walletconnect.network.ethereum.LegacySdkHelper.prepareToSendMessageData import com.tangem.data.walletconnect.respond.WcRespondService import com.tangem.data.walletconnect.sign.BaseWcSignUseCase @@ -28,8 +29,10 @@ import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.emitAll import kotlinx.coroutines.flow.flow +@Suppress("LongParameterList") internal class WcEthMessageSignUseCase @AssistedInject constructor( override val respondService: WcRespondService, + override val analytics: AnalyticsEventHandler, @Assisted override val context: WcMethodUseCaseContext, @Assisted override val method: WcEthMethod.MessageSign, private val walletManagersFacade: WalletManagersFacade, diff --git a/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/network/ethereum/WcEthSendTransactionUseCase.kt b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/network/ethereum/WcEthSendTransactionUseCase.kt index 214c12f74a..3a560bc130 100644 --- a/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/network/ethereum/WcEthSendTransactionUseCase.kt +++ b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/network/ethereum/WcEthSendTransactionUseCase.kt @@ -3,6 +3,7 @@ package com.tangem.data.walletconnect.network.ethereum import arrow.core.left import com.tangem.blockchain.common.transaction.Fee import com.tangem.blockchain.extensions.formatHex +import com.tangem.core.analytics.api.AnalyticsEventHandler import com.tangem.data.walletconnect.utils.BlockAidVerificationDelegate import com.tangem.data.walletconnect.respond.WcRespondService import com.tangem.data.walletconnect.sign.BaseWcSignUseCase @@ -25,6 +26,7 @@ internal class WcEthSendTransactionUseCase @AssistedInject constructor( @Assisted override val context: WcMethodUseCaseContext, @Assisted override val method: WcEthMethod.SendTransaction, override val respondService: WcRespondService, + override val analytics: AnalyticsEventHandler, private val sendTransaction: SendTransactionUseCase, blockAidDelegate: BlockAidVerificationDelegate, ) : BaseWcSignUseCase(), diff --git a/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/network/ethereum/WcEthSignTransactionUseCase.kt b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/network/ethereum/WcEthSignTransactionUseCase.kt index 5033ddac8d..36b54ba3e7 100644 --- a/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/network/ethereum/WcEthSignTransactionUseCase.kt +++ b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/network/ethereum/WcEthSignTransactionUseCase.kt @@ -4,6 +4,7 @@ import arrow.core.left import com.tangem.blockchain.common.transaction.Fee import com.tangem.blockchain.extensions.formatHex import com.tangem.common.extensions.toHexString +import com.tangem.core.analytics.api.AnalyticsEventHandler import com.tangem.data.walletconnect.utils.BlockAidVerificationDelegate import com.tangem.data.walletconnect.respond.WcRespondService import com.tangem.data.walletconnect.sign.BaseWcSignUseCase @@ -25,6 +26,7 @@ import kotlinx.coroutines.flow.flow internal class WcEthSignTransactionUseCase @AssistedInject constructor( override val respondService: WcRespondService, + override val analytics: AnalyticsEventHandler, private val prepareForSend: PrepareForSendUseCase, @Assisted override val context: WcMethodUseCaseContext, @Assisted override val method: WcEthMethod.SignTransaction, diff --git a/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/network/ethereum/WcEthSignTypedDataUseCase.kt b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/network/ethereum/WcEthSignTypedDataUseCase.kt index 61ad1718c5..8e961267e1 100644 --- a/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/network/ethereum/WcEthSignTypedDataUseCase.kt +++ b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/network/ethereum/WcEthSignTypedDataUseCase.kt @@ -2,6 +2,7 @@ package com.tangem.data.walletconnect.network.ethereum import arrow.core.left import com.tangem.blockchain.blockchains.ethereum.EthereumUtils +import com.tangem.core.analytics.api.AnalyticsEventHandler import com.tangem.data.walletconnect.network.ethereum.LegacySdkHelper.prepareToSendMessageData import com.tangem.data.walletconnect.respond.WcRespondService import com.tangem.data.walletconnect.sign.BaseWcSignUseCase @@ -21,8 +22,10 @@ import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.emitAll import kotlinx.coroutines.flow.flow +@Suppress("LongParameterList") internal class WcEthSignTypedDataUseCase @AssistedInject constructor( override val respondService: WcRespondService, + override val analytics: AnalyticsEventHandler, @Assisted override val context: WcMethodUseCaseContext, @Assisted override val method: WcEthMethod.SignTypedData, private val walletManagersFacade: WalletManagersFacade, diff --git a/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/network/solana/DefaultWcSolanaSignAllTransactionUseCase.kt b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/network/solana/DefaultWcSolanaSignAllTransactionUseCase.kt index 0db6558250..c2fb4099c9 100644 --- a/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/network/solana/DefaultWcSolanaSignAllTransactionUseCase.kt +++ b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/network/solana/DefaultWcSolanaSignAllTransactionUseCase.kt @@ -3,6 +3,7 @@ package com.tangem.data.walletconnect.network.solana import arrow.core.left import com.tangem.blockchain.common.TransactionData import com.tangem.blockchain.extensions.encodeBase64 +import com.tangem.core.analytics.api.AnalyticsEventHandler import com.tangem.data.walletconnect.respond.WcRespondService import com.tangem.data.walletconnect.sign.BaseWcSignUseCase import com.tangem.data.walletconnect.sign.SignCollector @@ -23,6 +24,7 @@ import org.json.JSONObject internal class DefaultWcSolanaSignAllTransactionUseCase @AssistedInject constructor( override val respondService: WcRespondService, + override val analytics: AnalyticsEventHandler, private val prepareForSend: PrepareForSendUseCase, @Assisted override val context: WcMethodUseCaseContext, @Assisted override val method: WcSolanaMethod.SignAllTransaction, diff --git a/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/network/solana/DefaultWcSolanaSignTransactionUseCase.kt b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/network/solana/DefaultWcSolanaSignTransactionUseCase.kt index 946dd9d620..ee268c3122 100644 --- a/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/network/solana/DefaultWcSolanaSignTransactionUseCase.kt +++ b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/network/solana/DefaultWcSolanaSignTransactionUseCase.kt @@ -3,6 +3,7 @@ package com.tangem.data.walletconnect.network.solana import arrow.core.left import com.tangem.blockchain.common.TransactionData import com.tangem.blockchain.extensions.encodeBase58 +import com.tangem.core.analytics.api.AnalyticsEventHandler import com.tangem.data.walletconnect.respond.WcRespondService import com.tangem.data.walletconnect.sign.BaseWcSignUseCase import com.tangem.data.walletconnect.sign.SignCollector @@ -21,6 +22,7 @@ import okio.ByteString.Companion.decodeBase64 internal class DefaultWcSolanaSignTransactionUseCase @AssistedInject constructor( override val respondService: WcRespondService, + override val analytics: AnalyticsEventHandler, private val prepareForSend: PrepareForSendUseCase, @Assisted override val context: WcMethodUseCaseContext, @Assisted override val method: WcSolanaMethod.SignTransaction, diff --git a/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/network/solana/WcSolanaMessageSignUseCase.kt b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/network/solana/WcSolanaMessageSignUseCase.kt index 20a23c46e0..e82c5601ea 100644 --- a/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/network/solana/WcSolanaMessageSignUseCase.kt +++ b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/network/solana/WcSolanaMessageSignUseCase.kt @@ -4,6 +4,7 @@ import arrow.core.left import com.domain.blockaid.models.transaction.CheckTransactionResult import com.tangem.blockchain.extensions.decodeBase58 import com.tangem.blockchain.extensions.encodeBase58 +import com.tangem.core.analytics.api.AnalyticsEventHandler import com.tangem.data.walletconnect.respond.WcRespondService import com.tangem.data.walletconnect.sign.BaseWcSignUseCase import com.tangem.data.walletconnect.sign.SignCollector @@ -25,6 +26,7 @@ internal class WcSolanaMessageSignUseCase @AssistedInject constructor( @Assisted override val method: WcSolanaMethod.SignMessage, private val signUseCase: SignUseCase, override val respondService: WcRespondService, + override val analytics: AnalyticsEventHandler, ) : BaseWcSignUseCase(), WcMessageSignUseCase { diff --git a/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/pair/DefaultWcPairUseCase.kt b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/pair/DefaultWcPairUseCase.kt index 5bcbce241a..fc52444c3b 100644 --- a/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/pair/DefaultWcPairUseCase.kt +++ b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/pair/DefaultWcPairUseCase.kt @@ -7,6 +7,9 @@ import arrow.core.right import com.domain.blockaid.models.dapp.CheckDAppResult import com.domain.blockaid.models.dapp.DAppData import com.reown.walletkit.client.Wallet +import com.tangem.core.analytics.api.AnalyticsEventHandler +import com.tangem.domain.walletconnect.WcAnalyticEvents +import com.tangem.data.walletconnect.utils.WC_TAG import com.tangem.data.walletconnect.utils.WcSdkSessionConverter import com.tangem.domain.blockaid.BlockAidVerifier import com.tangem.domain.walletconnect.model.* @@ -18,20 +21,19 @@ import dagger.assisted.Assisted import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject import kotlinx.coroutines.channels.Channel -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.first -import kotlinx.coroutines.flow.flow -import kotlinx.coroutines.flow.receiveAsFlow +import kotlinx.coroutines.flow.* import timber.log.Timber val unsupportedDApps = listOf("dYdX", "dYdX v4", "Apex Pro", "The Sandbox") +@Suppress("LongParameterList") internal class DefaultWcPairUseCase @AssistedInject constructor( private val sessionsManager: WcSessionsManager, private val associateNetworksDelegate: AssociateNetworksDelegate, private val caipNamespaceDelegate: CaipNamespaceDelegate, private val sdkDelegate: WcPairSdkDelegate, private val blockAidVerifier: BlockAidVerifier, + private val analytics: AnalyticsEventHandler, @Assisted private val pairRequest: WcPairRequest, ) : WcPairUseCase { @@ -40,26 +42,35 @@ internal class DefaultWcPairUseCase @AssistedInject constructor( override operator fun invoke(): Flow { val (uri: String, source: WcPairRequest.Source) = pairRequest return flow { + Timber.tag(WC_TAG).i("start pair flow $pairRequest") + analytics.send(WcAnalyticEvents.NewPairInitiated(source)) emit(WcPairState.Loading) val sdkSessionProposal = sdkDelegate.pair(uri) - .onLeft { emit(WcPairState.Error(it)) } + .onLeft { + Timber.tag(WC_TAG).e(it, "Failed to call pair $pairRequest") + emit(WcPairState.Error(it)) + } .getOrNull() ?: return@flow // check unsupported dApps, just local constant for now, finish if unsupported if (sdkSessionProposal.name in unsupportedDApps) { - Timber.i("Unsupported DApp") + Timber.tag(WC_TAG).i("Unsupported DApp ${sdkSessionProposal.name}") val error = WcPairState.Error(WcPairError.UnsupportedDApp) emit(error) return@flow } val proposalState = buildProposalState(sdkSessionProposal) - .onLeft { emit(WcPairState.Error(it)) } + .onLeft { + analytics.send(WcAnalyticEvents.PairFailed) + emit(WcPairState.Error(it)) + } .getOrNull() ?: return@flow emit(proposalState) // wait first terminal action and continue WC pair flow + Timber.tag(WC_TAG).i("pair wait terminal action ${sdkSessionProposal.name}") val terminalAction = onCallTerminalAction.receiveAsFlow().first() val sessionForApprove: WcSessionApprove? = when (terminalAction) { is TerminalAction.Approve -> terminalAction.sessionForApprove @@ -67,6 +78,7 @@ internal class DefaultWcPairUseCase @AssistedInject constructor( } // finish flow if rejected above if (sessionForApprove == null) { + analytics.send(WcAnalyticEvents.SessionDisconnected(proposalState.dAppSession)) sdkDelegate.rejectSession(sdkSessionProposal.proposerPublicKey) return@flow } @@ -84,17 +96,34 @@ internal class DefaultWcPairUseCase @AssistedInject constructor( networks = sessionForApprove.network.toSet(), ) sessionsManager.saveSession(newSession) + analytics.send( + WcAnalyticEvents.DAppConnected( + proposalState.dAppSession, + sessionForApprove, + ), + ) newSession + }.onLeft { + analytics.send(WcAnalyticEvents.DAppConnectionFailed(it.message)) + Timber.tag(WC_TAG).e(it, "Failed to approve session ${sdkSessionProposal.name}") } emit(WcPairState.Approving.Result(sessionForApprove, either)) + }.onCompletion { + if (it != null) { + Timber.tag(WC_TAG).e(it, "Completed with error $pairRequest") + } else { + Timber.tag(WC_TAG).i("Completed successfully $pairRequest") + } } } override fun approve(sessionForApprove: WcSessionApprove) { + analytics.send(WcAnalyticEvents.PairButtonConnect) onCallTerminalAction.trySend(TerminalAction.Approve(sessionForApprove)) } override fun reject() { + analytics.send(WcAnalyticEvents.ButtonCancel(WcAnalyticEvents.ButtonCancel.Type.Connection)) onCallTerminalAction.trySend(TerminalAction.Reject) } @@ -119,9 +148,17 @@ internal class DefaultWcPairUseCase @AssistedInject constructor( ): Either = runCatching { val proposalNetwork = associateNetworksDelegate.associate(sessionProposal) val verificationInfo = blockAidVerifier.verifyDApp(DAppData(sessionProposal.url)).getOrElse { - Timber.e("Failed to verify DApp: ${it.localizedMessage}") + Timber.tag(WC_TAG).e(it, "Failed to verify DApp ${sessionProposal.name}") CheckDAppResult.FAILED_TO_VERIFY } + val requestedNetworks = proposalNetwork + .values.map { it.available.plus(it.required) }.flatten().toSet() + analytics.send( + WcAnalyticEvents.PairRequested( + network = requestedNetworks, + verificationInfo.name, + ), + ) val appMetaData = WcAppMetaData( name = sessionProposal.name, description = sessionProposal.description, diff --git a/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/request/DefaultWcRequestService.kt b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/request/DefaultWcRequestService.kt index cb39b52a5e..ddfb53cc8e 100644 --- a/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/request/DefaultWcRequestService.kt +++ b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/request/DefaultWcRequestService.kt @@ -3,12 +3,14 @@ package com.tangem.data.walletconnect.request import com.reown.walletkit.client.Wallet import com.tangem.data.walletconnect.utils.WcSdkObserver import com.tangem.data.walletconnect.utils.WcSdkSessionRequestConverter +import com.tangem.data.walletconnect.utils.WC_TAG import com.tangem.domain.walletconnect.WcRequestService import com.tangem.domain.walletconnect.model.WcMethodName import com.tangem.domain.walletconnect.model.sdkcopy.WcSdkSessionRequest import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.receiveAsFlow +import timber.log.Timber internal class DefaultWcRequestService( private val requestConverters: Set, @@ -23,8 +25,10 @@ internal class DefaultWcRequestService( ) { // Triggered when a Dapp sends SessionRequest to sign a transaction or a message val sr = WcSdkSessionRequestConverter.convert(sessionRequest) + Timber.tag(WC_TAG).i("handle request $sr") val name = requestConverters.firstNotNullOfOrNull { it.toWcMethodName(sr) } ?: WcMethodName.Unsupported(sr.request.method) + Timber.tag(WC_TAG).i("handle request name $name") _wcRequest.trySend(name to sr) } } \ No newline at end of file diff --git a/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/respond/DefaultWcRespondService.kt b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/respond/DefaultWcRespondService.kt index 025e9834b2..57961795e5 100644 --- a/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/respond/DefaultWcRespondService.kt +++ b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/respond/DefaultWcRespondService.kt @@ -5,8 +5,10 @@ import arrow.core.left import arrow.core.right import com.reown.walletkit.client.Wallet import com.reown.walletkit.client.WalletKit +import com.tangem.data.walletconnect.utils.WC_TAG import com.tangem.domain.walletconnect.model.sdkcopy.WcSdkSessionRequest import kotlinx.coroutines.suspendCancellableCoroutine +import timber.log.Timber import kotlin.coroutines.resume internal class DefaultWcRespondService : WcRespondService { @@ -22,35 +24,18 @@ internal class DefaultWcRespondService : WcRespondService { ), ), onSuccess = { + Timber.tag(WC_TAG).i("Successful respond for request $request") continuation.resume(Unit.right()) }, onError = { - continuation.resume(it.throwable.left()) - }, - ) - } - - override suspend fun rejectRequest(request: WcSdkSessionRequest, message: String) = - suspendCancellableCoroutine { continuation -> - WalletKit.respondSessionRequest( - params = Wallet.Params.SessionRequestResponse( - sessionTopic = request.topic, - jsonRpcResponse = Wallet.Model.JsonRpcResponse.JsonRpcError( - id = request.request.id, - code = 0, - message = message, - ), - ), - onSuccess = { - continuation.resume(Unit.right()) - }, - onError = { + Timber.tag(WC_TAG).e(it.throwable, "Failed respond for request $request") continuation.resume(it.throwable.left()) }, ) } override fun rejectRequestNonBlock(request: WcSdkSessionRequest, message: String) { + Timber.tag(WC_TAG).i("reject request $request") WalletKit.respondSessionRequest( params = Wallet.Params.SessionRequestResponse( sessionTopic = request.topic, diff --git a/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/respond/WcRespondService.kt b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/respond/WcRespondService.kt index 0eb8948d59..be259755f2 100644 --- a/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/respond/WcRespondService.kt +++ b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/respond/WcRespondService.kt @@ -5,6 +5,5 @@ import com.tangem.domain.walletconnect.model.sdkcopy.WcSdkSessionRequest interface WcRespondService { suspend fun respond(request: WcSdkSessionRequest, response: String): Either - suspend fun rejectRequest(request: WcSdkSessionRequest, message: String = ""): Either fun rejectRequestNonBlock(request: WcSdkSessionRequest, message: String = "") } \ No newline at end of file diff --git a/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/sessions/DefaultWcSessionsManager.kt b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/sessions/DefaultWcSessionsManager.kt index 1c537ef70d..c6239ed16c 100644 --- a/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/sessions/DefaultWcSessionsManager.kt +++ b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/sessions/DefaultWcSessionsManager.kt @@ -9,6 +9,7 @@ import com.reown.walletkit.client.WalletKit import com.tangem.data.walletconnect.pair.AssociateNetworksDelegate import com.tangem.data.walletconnect.utils.WcSdkObserver import com.tangem.data.walletconnect.utils.WcSdkSessionConverter +import com.tangem.data.walletconnect.utils.WC_TAG import com.tangem.datasource.local.walletconnect.WalletConnectStore import com.tangem.domain.walletconnect.model.WcSession import com.tangem.domain.walletconnect.model.WcSessionDTO @@ -143,6 +144,7 @@ internal class DefaultWcSessionsManager( val haveSomeUnknown = unknownStoredSessions.isNotEmpty() if (haveSomeUnknown) { + Timber.tag(WC_TAG).i("removeUnknownSessions $unknownStoredSessions") store.removeSessions(unknownStoredSessions.toSet()) } return haveSomeUnknown diff --git a/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/sign/BaseWcSignUseCase.kt b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/sign/BaseWcSignUseCase.kt index ded283fe3e..55ea7f8786 100644 --- a/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/sign/BaseWcSignUseCase.kt +++ b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/sign/BaseWcSignUseCase.kt @@ -1,7 +1,9 @@ package com.tangem.data.walletconnect.sign +import com.tangem.core.analytics.api.AnalyticsEventHandler import com.tangem.data.walletconnect.respond.WcRespondService import com.tangem.domain.tokens.model.Network +import com.tangem.domain.walletconnect.WcAnalyticEvents import com.tangem.domain.walletconnect.model.WcSession import com.tangem.domain.walletconnect.model.sdkcopy.WcSdkSessionRequest import com.tangem.domain.walletconnect.usecase.method.WcMethodUseCase @@ -16,6 +18,7 @@ internal abstract class BaseWcSignUseCase : MiddleActionCollector { abstract val respondService: WcRespondService + abstract val analytics: AnalyticsEventHandler abstract val context: WcMethodUseCaseContext override val network: Network get() = context.network @@ -25,17 +28,36 @@ internal abstract class BaseWcSignUseCase : protected val delegate by lazy { WcSignUseCaseDelegate( + analytics = analytics, + context = context, finalActionCollector = this, middleActionCollector = this, ) } + init { + analytics.send( + WcAnalyticEvents.SignatureRequestReceived( + session = context.session, + rawRequest = context.rawSdkRequest, + network = context.network, + ), + ) + } + override suspend fun onCancel(currentState: WcSignState) { defaultReject() } - override fun sign() = delegate.sign() - override fun cancel() = delegate.cancel() + override fun sign() { + analytics.send(WcAnalyticEvents.ButtonSign(context.rawSdkRequest)) + delegate.sign() + } + + override fun cancel() { + analytics.send(WcAnalyticEvents.ButtonCancel(WcAnalyticEvents.ButtonCancel.Type.Sign)) + delegate.cancel() + } protected fun middleAction(action: MiddleAction) = delegate.middleAction(action) protected fun defaultReject() { diff --git a/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/sign/WcSignUseCaseDelegate.kt b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/sign/WcSignUseCaseDelegate.kt index 3428b16217..55c77f3de3 100644 --- a/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/sign/WcSignUseCaseDelegate.kt +++ b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/sign/WcSignUseCaseDelegate.kt @@ -1,9 +1,11 @@ package com.tangem.data.walletconnect.sign import arrow.core.left +import com.tangem.core.analytics.api.AnalyticsEventHandler import com.tangem.data.walletconnect.sign.SignStateConverter.toPreSign import com.tangem.data.walletconnect.sign.SignStateConverter.toResult import com.tangem.data.walletconnect.sign.SignStateConverter.toSigning +import com.tangem.domain.walletconnect.WcAnalyticEvents import com.tangem.domain.walletconnect.usecase.method.WcSignState import com.tangem.domain.walletconnect.usecase.method.WcSignStep import kotlinx.coroutines.Job @@ -13,6 +15,8 @@ import kotlinx.coroutines.flow.* import kotlinx.coroutines.launch internal class WcSignUseCaseDelegate( + private val analytics: AnalyticsEventHandler, + private val context: WcMethodUseCaseContext, private val finalActionCollector: FinalActionCollector, private val middleActionCollector: MiddleActionCollector, ) : FinalActionCollector by finalActionCollector, @@ -54,6 +58,27 @@ internal class WcSignUseCaseDelegate( val errorResult = state.value.toResult(exception.left()) state.update { errorResult } } + .onEach { state -> + val step = state.domainStep as? WcSignStep.Result ?: return@onEach + val event = step.result.fold( + ifLeft = { + WcAnalyticEvents.SignatureRequestFailed( + session = context.session, + rawRequest = context.rawSdkRequest, + network = context.network, + it.message.orEmpty(), + ) + }, + ifRight = { + WcAnalyticEvents.SignatureRequestHandled( + session = context.session, + rawRequest = context.rawSdkRequest, + network = context.network, + ) + }, + ) + analytics.send(event) + } var signJob: Job? = null diff --git a/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/utils/WcSdkObserver.kt b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/utils/WcSdkObserver.kt index 4c70570c4d..06637ecefd 100644 --- a/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/utils/WcSdkObserver.kt +++ b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/utils/WcSdkObserver.kt @@ -3,6 +3,8 @@ package com.tangem.data.walletconnect.utils import com.reown.walletkit.client.Wallet import com.reown.walletkit.client.WalletKit +const val WC_TAG = "Wallet Connect" + internal interface WcSdkObserver : WalletKit.WalletDelegate { override val onSessionAuthenticate: ((Wallet.Model.SessionAuthenticate, Wallet.Model.VerifyContext) -> Unit)? diff --git a/data/wallet-connect/src/test/kotlin/com/tangem/domain/walletconnect/DefaultWcPairUseCaseTest.kt b/data/wallet-connect/src/test/kotlin/com/tangem/domain/walletconnect/DefaultWcPairUseCaseTest.kt index 6a86ee96f2..bd8e58f43b 100644 --- a/data/wallet-connect/src/test/kotlin/com/tangem/domain/walletconnect/DefaultWcPairUseCaseTest.kt +++ b/data/wallet-connect/src/test/kotlin/com/tangem/domain/walletconnect/DefaultWcPairUseCaseTest.kt @@ -8,6 +8,7 @@ import com.domain.blockaid.models.dapp.CheckDAppResult import com.domain.blockaid.models.dapp.DAppData import com.reown.walletkit.client.Wallet import com.tangem.common.test.domain.wallet.MockUserWalletFactory +import com.tangem.core.analytics.api.AnalyticsEventHandler import com.tangem.data.walletconnect.pair.AssociateNetworksDelegate import com.tangem.data.walletconnect.pair.CaipNamespaceDelegate import com.tangem.data.walletconnect.pair.DefaultWcPairUseCase @@ -34,6 +35,7 @@ internal class DefaultWcPairUseCaseTest { private val sessionsManager: WcSessionsManager = mockk() private val associateNetworksDelegate: AssociateNetworksDelegate = mockk() private val caipNamespaceDelegate: CaipNamespaceDelegate = mockk() + private val analytics: AnalyticsEventHandler = mockk(relaxed = true) private val sdkDelegate: WcPairSdkDelegate = mockk() private val blockAidVerifier: BlockAidVerifier = mockk() @@ -101,6 +103,7 @@ internal class DefaultWcPairUseCaseTest { caipNamespaceDelegate = caipNamespaceDelegate, sdkDelegate = sdkDelegate, blockAidVerifier = blockAidVerifier, + analytics = analytics, pairRequest = WcPairRequest(userWalletId = UserWalletId(""), uri = url, source = source), ) diff --git a/data/wallet-connect/src/test/kotlin/com/tangem/domain/walletconnect/WcSignUseCaseDelegateTest.kt b/data/wallet-connect/src/test/kotlin/com/tangem/domain/walletconnect/WcSignUseCaseDelegateTest.kt index b993555c51..4251b79b72 100644 --- a/data/wallet-connect/src/test/kotlin/com/tangem/domain/walletconnect/WcSignUseCaseDelegateTest.kt +++ b/data/wallet-connect/src/test/kotlin/com/tangem/domain/walletconnect/WcSignUseCaseDelegateTest.kt @@ -3,14 +3,20 @@ package com.tangem.domain.walletconnect import app.cash.turbine.test import arrow.core.left import arrow.core.right -import com.tangem.data.walletconnect.sign.FinalActionCollector -import com.tangem.data.walletconnect.sign.MiddleActionCollector -import com.tangem.data.walletconnect.sign.SignCollector +import com.domain.blockaid.models.dapp.CheckDAppResult +import com.tangem.common.test.domain.token.MockCryptoCurrencyFactory +import com.tangem.common.test.domain.wallet.MockUserWalletFactory +import com.tangem.core.analytics.api.AnalyticsEventHandler +import com.tangem.data.walletconnect.sign.* import com.tangem.data.walletconnect.sign.SignStateConverter.toResult import com.tangem.data.walletconnect.sign.SignStateConverter.toSigning -import com.tangem.data.walletconnect.sign.WcSignUseCaseDelegate +import com.tangem.domain.walletconnect.model.WcSession +import com.tangem.domain.walletconnect.model.sdkcopy.WcAppMetaData +import com.tangem.domain.walletconnect.model.sdkcopy.WcSdkSession +import com.tangem.domain.walletconnect.model.sdkcopy.WcSdkSessionRequest import com.tangem.domain.walletconnect.usecase.method.WcSignState import com.tangem.domain.walletconnect.usecase.method.WcSignStep +import io.mockk.mockk import junit.framework.TestCase.assertEquals import kotlinx.coroutines.delay import kotlinx.coroutines.flow.FlowCollector @@ -25,6 +31,36 @@ internal class WcSignUseCaseDelegateTest { private var finalActionCollector: FinalActionCollector = object : FinalActionCollector {} private val initSignModel = TestSignModel() + private val analytics: AnalyticsEventHandler = mockk(relaxed = true) + private val rawRequestMock = WcSdkSessionRequest( + topic = "", + chainId = "", + request = WcSdkSessionRequest.JSONRPCRequest( + id = 0L, + method = "", + params = "", + ), + ) + private val context: WcMethodUseCaseContext = WcMethodUseCaseContext( + network = MockCryptoCurrencyFactory().ethereum.network, + accountAddress = "", + rawSdkRequest = rawRequestMock, + session = WcSession( + wallet = MockUserWalletFactory.create(), + networks = setOf(), + securityStatus = CheckDAppResult.FAILED_TO_VERIFY, + sdkModel = WcSdkSession( + topic = "", + appMetaData = WcAppMetaData( + name = "", + description = "", + url = "", + icons = listOf(), + redirect = "", + ), + ), + ), + ) private val initState = WcSignState(initSignModel, WcSignStep.PreSign) private val signing = initState.toSigning() @@ -46,6 +82,16 @@ internal class WcSignUseCaseDelegateTest { emit(state.toResult(testException.left())) } + private fun createUseCaseDelegate( + middleActionCollector: MiddleActionCollector, + finalActionCollector: FinalActionCollector, + ) = WcSignUseCaseDelegate( + analytics = analytics, + context = context, + finalActionCollector = finalActionCollector, + middleActionCollector = middleActionCollector, + ) + @Before fun setup() { middleActionCollector = object : MiddleActionCollector {} @@ -59,7 +105,7 @@ internal class WcSignUseCaseDelegateTest { successSign(state) } } - val useCase = WcSignUseCaseDelegate( + val useCase = createUseCaseDelegate( finalActionCollector = finalActionCollector, middleActionCollector = middleActionCollector, ) @@ -77,7 +123,7 @@ internal class WcSignUseCaseDelegateTest { successSign(state) } } - val useCase = WcSignUseCaseDelegate( + val useCase = createUseCaseDelegate( finalActionCollector = finalActionCollector, middleActionCollector = middleActionCollector, ) @@ -99,7 +145,7 @@ internal class WcSignUseCaseDelegateTest { failedSign(state) } } - val useCase = WcSignUseCaseDelegate( + val useCase = createUseCaseDelegate( finalActionCollector = finalActionCollector, middleActionCollector = middleActionCollector, ) @@ -123,7 +169,7 @@ internal class WcSignUseCaseDelegateTest { throw exception } } - val useCase = WcSignUseCaseDelegate( + val useCase = createUseCaseDelegate( finalActionCollector = finalActionCollector, middleActionCollector = middleActionCollector, ) @@ -145,7 +191,7 @@ internal class WcSignUseCaseDelegateTest { emit(result) } } - val useCase = WcSignUseCaseDelegate( + val useCase = createUseCaseDelegate( finalActionCollector = finalActionCollector, middleActionCollector = middleActionCollector, ) @@ -176,7 +222,7 @@ internal class WcSignUseCaseDelegateTest { emit(expectedSignResult) } } - val useCase = WcSignUseCaseDelegate( + val useCase = createUseCaseDelegate( finalActionCollector = finalActionCollector, middleActionCollector = middleActionCollector, ) @@ -229,7 +275,7 @@ internal class WcSignUseCaseDelegateTest { emit(signModel.copy(testStr = middleAction.newTestStr)) } } - val useCase = WcSignUseCaseDelegate( + val useCase = createUseCaseDelegate( finalActionCollector = finalActionCollector, middleActionCollector = middleActionCollector, ) @@ -276,7 +322,7 @@ internal class WcSignUseCaseDelegateTest { delay(4) } } - val useCase = WcSignUseCaseDelegate( + val useCase = createUseCaseDelegate( finalActionCollector = finalActionCollector, middleActionCollector = middleActionCollector, ) diff --git a/domain/wallet-connect/build.gradle.kts b/domain/wallet-connect/build.gradle.kts index a2178f32a7..a79409d531 100644 --- a/domain/wallet-connect/build.gradle.kts +++ b/domain/wallet-connect/build.gradle.kts @@ -16,6 +16,9 @@ dependencies { implementation(projects.domain.walletConnect.models) implementation(projects.domain.blockaid.models) + /* Project - Core */ + implementation(projects.core.analytics) + /* Other */ implementation(deps.moshi.adapters) diff --git a/domain/wallet-connect/src/main/kotlin/com/tangem/domain/walletconnect/WcAnalyticEvents.kt b/domain/wallet-connect/src/main/kotlin/com/tangem/domain/walletconnect/WcAnalyticEvents.kt new file mode 100644 index 0000000000..03f186a19c --- /dev/null +++ b/domain/wallet-connect/src/main/kotlin/com/tangem/domain/walletconnect/WcAnalyticEvents.kt @@ -0,0 +1,162 @@ +package com.tangem.domain.walletconnect + +import com.tangem.core.analytics.models.AnalyticsEvent +import com.tangem.core.analytics.models.AnalyticsParam +import com.tangem.domain.tokens.model.Network +import com.tangem.domain.walletconnect.model.WcPairRequest +import com.tangem.domain.walletconnect.model.WcSession +import com.tangem.domain.walletconnect.model.WcSessionApprove +import com.tangem.domain.walletconnect.model.WcSessionProposal +import com.tangem.domain.walletconnect.model.sdkcopy.WcSdkSessionRequest + +sealed class WcAnalyticEvents( + event: String, + params: Map = mapOf(), +) : AnalyticsEvent(category = "Wallet Connect", event = event, params = params) { + + class NewPairInitiated(source: WcPairRequest.Source) : WcAnalyticEvents( + event = "Session Initiated", + params = mapOf( + AnalyticsParam.Key.SOURCE to when (source) { + WcPairRequest.Source.QR -> "QR" + WcPairRequest.Source.DEEPLINK -> "DeepLink" + WcPairRequest.Source.CLIPBOARD -> "Clipboard" + WcPairRequest.Source.ETC -> "etc" + }, + ), + ) + + data object PairButtonConnect : WcAnalyticEvents( + event = "Button - Connect", + ) + + class PairRequested( + network: Set, + domainVerification: String, + ) : WcAnalyticEvents( + event = "dApp Connection Requested", + params = mapOf( + NETWORKS to network.joinToString(",") { it.name }, + DOMAIN_VERIFICATION to domainVerification, + ), + ) + + data object PairFailed : WcAnalyticEvents( + event = "Session Failed", + ) + + class DAppConnected( + sessionProposal: WcSessionProposal, + sessionForApprove: WcSessionApprove, + ) : WcAnalyticEvents( + event = "dApp Connected", + params = mapOf( + AnalyticsParam.Key.DAPP_NAME to sessionProposal.dAppMetaData.name, + AnalyticsParam.Key.DAPP_URL to sessionProposal.dAppMetaData.url, + AnalyticsParam.Key.BLOCKCHAIN to sessionForApprove.network.joinToString(",") { it.name }, + ), + ) + + class DAppConnectionFailed( + errorCode: String, + ) : WcAnalyticEvents( + event = "dApp Connection Failed", + params = mapOf( + AnalyticsParam.Key.ERROR_CODE to errorCode, + ), + ) + + class SessionDisconnected(sessionProposal: WcSessionProposal) : WcAnalyticEvents( + event = "dApp Disconnected", + params = mapOf( + AnalyticsParam.Key.DAPP_NAME to sessionProposal.dAppMetaData.name, + AnalyticsParam.Key.DAPP_URL to sessionProposal.dAppMetaData.url, + ), + ) + + class SignatureRequestReceived( + session: WcSession, + rawRequest: WcSdkSessionRequest, + network: Network, + ) : WcAnalyticEvents( + event = "Signature Request Received", + params = mapOf( + AnalyticsParam.Key.DAPP_NAME to session.sdkModel.appMetaData.name, + AnalyticsParam.Key.DAPP_URL to session.sdkModel.appMetaData.url, + AnalyticsParam.Key.METHOD_NAME to rawRequest.request.method, + AnalyticsParam.Key.BLOCKCHAIN to network.name, + ), + ) + + class SignatureRequestHandled( + session: WcSession, + rawRequest: WcSdkSessionRequest, + network: Network, + ) : WcAnalyticEvents( + event = "Signature Request Handled", + params = mapOf( + AnalyticsParam.Key.DAPP_NAME to session.sdkModel.appMetaData.name, + AnalyticsParam.Key.DAPP_URL to session.sdkModel.appMetaData.url, + AnalyticsParam.Key.METHOD_NAME to rawRequest.request.method, + AnalyticsParam.Key.BLOCKCHAIN to network.name, + ), + ) + + class SignatureRequestFailed( + session: WcSession, + rawRequest: WcSdkSessionRequest, + network: Network, + errorCode: String, + ) : WcAnalyticEvents( + event = "Signature Request Failed", + params = mapOf( + AnalyticsParam.Key.DAPP_NAME to session.sdkModel.appMetaData.name, + AnalyticsParam.Key.DAPP_URL to session.sdkModel.appMetaData.url, + AnalyticsParam.Key.METHOD_NAME to rawRequest.request.method, + AnalyticsParam.Key.BLOCKCHAIN to network.name, + AnalyticsParam.Key.ERROR_CODE to errorCode, + ), + ) + + class ButtonSign( + rawRequest: WcSdkSessionRequest, + ) : WcAnalyticEvents( + event = "Button - Sign", + params = mapOf( + AnalyticsParam.Key.METHOD_NAME to rawRequest.request.method, + ), + ) + + class ButtonCancel( + type: Type, + ) : WcAnalyticEvents( + event = "Button - Sign", + params = mapOf( + AnalyticsParam.Key.TYPE to type.type, + ), + ) { + enum class Type(val type: String) { + Connection("Connection"), + Sign("Sign"), + } + } + + data object ButtonDisconnectAll : WcAnalyticEvents( + event = "Button - Disconnect All", + ) + + class ButtonDisconnect( + session: WcSession, + ) : WcAnalyticEvents( + event = "Button - Disconnect", + params = mapOf( + AnalyticsParam.Key.DAPP_NAME to session.sdkModel.appMetaData.name, + AnalyticsParam.Key.DAPP_URL to session.sdkModel.appMetaData.url, + ), + ) + + companion object { + const val NETWORKS = "Networks" + const val DOMAIN_VERIFICATION = "Domain Verification" + } +} \ No newline at end of file diff --git a/domain/wallet-connect/src/main/kotlin/com/tangem/domain/walletconnect/usecase/disconnect/WcDisconnectUseCase.kt b/domain/wallet-connect/src/main/kotlin/com/tangem/domain/walletconnect/usecase/disconnect/WcDisconnectUseCase.kt index 707dc20180..cb6c3a2497 100644 --- a/domain/wallet-connect/src/main/kotlin/com/tangem/domain/walletconnect/usecase/disconnect/WcDisconnectUseCase.kt +++ b/domain/wallet-connect/src/main/kotlin/com/tangem/domain/walletconnect/usecase/disconnect/WcDisconnectUseCase.kt @@ -1,5 +1,7 @@ package com.tangem.domain.walletconnect.usecase.disconnect +import com.tangem.core.analytics.api.AnalyticsEventHandler +import com.tangem.domain.walletconnect.WcAnalyticEvents import com.tangem.domain.walletconnect.model.WcSession import com.tangem.domain.walletconnect.repository.WcSessionsManager import kotlinx.coroutines.flow.first @@ -9,22 +11,30 @@ import kotlinx.coroutines.flow.toList class WcDisconnectUseCase( private val sessionsManager: WcSessionsManager, + private val analytics: AnalyticsEventHandler, ) { suspend fun disconnectAll() { + analytics.send(WcAnalyticEvents.ButtonDisconnectAll) sessionsManager.sessions.first() .flatMap { it.value } - .map { session -> flow { emit(disconnect(session)) } } + .map { session -> flow { emit(internalDisconnect(session)) } } .merge() .toList() } suspend fun disconnect(topic: String) { val session = sessionsManager.findSessionByTopic(topic) ?: return - disconnect(session) + analytics.send(WcAnalyticEvents.ButtonDisconnect(session)) + internalDisconnect(session) } suspend fun disconnect(session: WcSession) { + analytics.send(WcAnalyticEvents.ButtonDisconnect(session)) + internalDisconnect(session) + } + + private suspend fun internalDisconnect(session: WcSession) { sessionsManager.removeSession(session) } } \ No newline at end of file diff --git a/domain/wallet-connect/src/main/kotlin/com/tangem/domain/walletconnect/usecase/method/WcMessageSignUseCase.kt b/domain/wallet-connect/src/main/kotlin/com/tangem/domain/walletconnect/usecase/method/WcMessageSignUseCase.kt index 594ffa0213..07b48dff2d 100644 --- a/domain/wallet-connect/src/main/kotlin/com/tangem/domain/walletconnect/usecase/method/WcMessageSignUseCase.kt +++ b/domain/wallet-connect/src/main/kotlin/com/tangem/domain/walletconnect/usecase/method/WcMessageSignUseCase.kt @@ -8,7 +8,7 @@ import com.tangem.domain.walletconnect.usecase.blockaid.WcBlockAidEligibleTransa * ## Ethereum * personal_sign https://docs.reown.com/advanced/multichain/rpc-reference/ethereum-rpc#personal-sign * eth_sign https://docs.reown.com/advanced/multichain/rpc-reference/ethereum-rpc#eth-sign - * todo() eth_signTypedData https://docs.reown.com/advanced/multichain/rpc-reference/ethereum-rpc#eth-signtypeddata + * eth_signTypedData https://docs.reown.com/advanced/multichain/rpc-reference/ethereum-rpc#eth-signtypeddata * * ## Solana * solana_signMessage https://docs.reown.com/advanced/multichain/rpc-reference/solana-rpc#solana-signmessage From e5d588030b3914f538a399d9f3b91a0a3cfd6196 Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 13 May 2025 19:34:01 +0400 Subject: [PATCH 029/165] Updated on 2026-08-14 --- core/res/src/main/res/values-ja/strings.xml | 32 +-- core/res/src/main/res/values-ru/strings.xml | 30 +-- core/res/src/main/res/values/strings.xml | 36 +-- .../entity/approve/WcApproveTransactionUM.kt | 2 +- .../ui/approve/WcCustomAllowanceContent.kt | 218 ++++++++++++++++++ 5 files changed, 271 insertions(+), 47 deletions(-) create mode 100644 features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/ui/approve/WcCustomAllowanceContent.kt diff --git a/core/res/src/main/res/values-ja/strings.xml b/core/res/src/main/res/values-ja/strings.xml index 3dde89a4c9..7979f3a2f3 100644 --- a/core/res/src/main/res/values-ja/strings.xml +++ b/core/res/src/main/res/values-ja/strings.xml @@ -292,25 +292,25 @@ 確認するには、プロバイダーのウェブサイトにアクセスしてください。 プロバイダーによる本人確認手続きが必要です。 買付完了 - 買付中 + 買付待ち 買付中... - キャンセルされました - 確認済み - 確認中 - 確認中... - 交換済み - 交換中 - 交換中... - 失敗しました - 停止中 + 取引はキャンセルされました + 入金確認済み + 確認待ち + 確認待ち... + 交換完了 + 交換待ち + スワップ待ち... + 交換に失敗しました + 交換は一時停止されました 入金済み 入金待ち 入金待ち... - 返金済み - 処理中 + 払い戻し完了 + 払い戻し待ち あなたへ送金しています - 送金中... - 送金済み + 資金の送金中... + 送金済み資金 プロバイダー提供のデータ。推定額は市場動向により変更される場合があります。 交換ステータス 確認が必要です @@ -334,6 +334,7 @@ この取引を非表示にする 一度非表示にすると、取引状況を再度表示することはできません。代わりに、スワイプして閉じることができます。 取引状況を非表示にしますか? + スワップする トークンが見つかりません。別のリクエストをお試しください。 ID: %s 取引IDをコピーしました @@ -1244,6 +1245,7 @@ 検証済みドメイン アプリで間違ったカードまたはリングが選択されました 問題が起きています + 使用を許可する アドレス 接続する ネットワーク @@ -1259,6 +1261,7 @@ 接続 内容 データをコピー + 使用可能量の設定 すべての接続を解除する すべてのdAppsの接続解除に関するテキスト すべてのdAppを接続解除する @@ -1274,6 +1277,7 @@ 宛先 取引リクエスト 取引リクエスト + 無制限 ウォレットコネクト 破棄 バックアップが中断されました。再開しますか? diff --git a/core/res/src/main/res/values-ru/strings.xml b/core/res/src/main/res/values-ru/strings.xml index 930c417497..8689c2d7ff 100644 --- a/core/res/src/main/res/values-ru/strings.xml +++ b/core/res/src/main/res/values-ru/strings.xml @@ -297,26 +297,26 @@ Сумма была возвращена в %1$s (%2$s сети) Посетите сайт провайдера для проверки Провайдер запрашивает прохождение верификации - Куплено - Покупка - Покупка... - Отменен - Подтверждено - Подтверждение - Подтверждение... - Обменяно - Обмен - Обмен... - Неудачно - Приостановлен + Покупка завершена + Ожидание покупки + Ожидание покупки... + Транзакция отменена + Депозит подтверждён + Ожидает подтверждения + Ожидает подтверждения... + Обмен завершён + Ожидает обмена + Ожидает обмена... + Ошибка обмена + Обмен остановлен Депозит получен Ожидание депозита Ожидаем пополнения... - Возвращено + Средства возвращены Возврат средств - Отправляем + Отправка средств Отправка средств... - Отправлено + Средства отправлены Данные провайдера. Сумма к получению может измениться в зависимости от рыночных условий. Статус обмена Требуется верификация diff --git a/core/res/src/main/res/values/strings.xml b/core/res/src/main/res/values/strings.xml index 869cf9bf8b..aefa4e7626 100644 --- a/core/res/src/main/res/values/strings.xml +++ b/core/res/src/main/res/values/strings.xml @@ -295,26 +295,26 @@ The amount was refunded in %1$s (%2$s network) Visit provider’s website for verification KYC verification required by provider - Bought - Buying - Buying... - Canceled - Confirmed - Confirming - Confirming... - Exchanged - Exchanging - Exchanging... - Failed - Paused + Purchase completed + Awaiting Purchase + Awaiting Purchase... + Transaction canceled + Deposit confirmed + Waiting for confirmation + Waiting for confirmation... + Exchange completed + Waiting for exchange + Waiting for exchange... + Exchange failed + Exchange paused Deposit received - Awaiting deposit + Waiting for deposit Awaiting deposit... - Refunded - Refunding + Refund completed + Waiting for refund Sending to you - Sending to you... - Sent + Sending funds... + Funds sent Provider-sourced data. Estimated amount subject to change due to market conditions. Exchange status Verification required @@ -338,6 +338,7 @@ Hide this transaction Once hidden, the transaction status cannot be viewed again. You can simply swipe to dismiss instead. Hide Transaction Status? + Swap with No tokens found. Please try another request ID: %s Transaction ID copied @@ -1337,6 +1338,7 @@ To Transaction request Transaction request + Unlimited Amount Wallet connect Discard You have an interrupted backup. Do you want to resume? diff --git a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/entity/approve/WcApproveTransactionUM.kt b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/entity/approve/WcApproveTransactionUM.kt index 42c283e2e1..b8107b23c1 100644 --- a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/entity/approve/WcApproveTransactionUM.kt +++ b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/entity/approve/WcApproveTransactionUM.kt @@ -28,4 +28,4 @@ internal data class WcCustomAllowanceUM( val tokenIconUrl: String, val amountText: String, val isUnlimited: Boolean, -) \ No newline at end of file +) : TangemBottomSheetConfigContent \ No newline at end of file diff --git a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/ui/approve/WcCustomAllowanceContent.kt b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/ui/approve/WcCustomAllowanceContent.kt new file mode 100644 index 0000000000..08f144454d --- /dev/null +++ b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/ui/approve/WcCustomAllowanceContent.kt @@ -0,0 +1,218 @@ +package com.tangem.features.walletconnect.transaction.ui.approve + +import android.content.res.Configuration +import androidx.annotation.DrawableRes +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.material3.Text +import androidx.compose.material3.TextField +import androidx.compose.material3.TextFieldDefaults +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.text.input.KeyboardType +import androidx.compose.ui.tooling.preview.Devices +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.tooling.preview.PreviewParameter +import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider +import coil.compose.AsyncImage +import com.tangem.core.ui.components.PrimaryButton +import com.tangem.core.ui.components.TangemSwitch +import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig +import com.tangem.core.ui.components.bottomsheets.modal.TangemModalBottomSheet +import com.tangem.core.ui.components.bottomsheets.modal.TangemModalBottomSheetTitle +import com.tangem.core.ui.extensions.resourceReference +import com.tangem.core.ui.extensions.stringResourceSafe +import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.res.TangemThemePreview +import com.tangem.features.walletconnect.impl.R +import com.tangem.features.walletconnect.transaction.entity.approve.WcCustomAllowanceUM + +@Composable +internal fun WcCustomAllowanceContent( + allowance: WcCustomAllowanceUM, + onAmountChange: (String) -> Unit, + onToggleChange: (Boolean) -> Unit, + onClickDone: () -> Unit, + modifier: Modifier = Modifier, +) { + Column( + modifier = modifier + .background(TangemTheme.colors.background.tertiary) + .fillMaxWidth() + .padding(TangemTheme.dimens.size16), + ) { + AmountTextField( + allowance = allowance, + onAmountChange = onAmountChange, + ) + + Spacer(modifier = Modifier.height(TangemTheme.dimens.spacing16)) + + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier + .clip(RoundedCornerShape(TangemTheme.dimens.radius14)) + .background(color = TangemTheme.colors.background.action) + .padding(TangemTheme.dimens.spacing16) + .fillMaxWidth(), + ) { + Text( + text = stringResourceSafe(R.string.wc_unlimited_amount), + style = TangemTheme.typography.body1, + color = TangemTheme.colors.text.primary1, + modifier = Modifier.weight(1f), + ) + TangemSwitch( + checked = allowance.isUnlimited, + onCheckedChange = onToggleChange, + ) + } + + Spacer(modifier = Modifier.height(TangemTheme.dimens.spacing60)) + + PrimaryButton( + modifier = Modifier.fillMaxWidth(), + text = stringResourceSafe(R.string.common_done), + onClick = onClickDone, + ) + } +} + +@Composable +private fun AmountTextField( + allowance: WcCustomAllowanceUM, + onAmountChange: (String) -> Unit, + modifier: Modifier = Modifier, +) { + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = modifier + .clip(RoundedCornerShape(TangemTheme.dimens.radius14)) + .background(color = TangemTheme.colors.background.action) + .padding( + end = TangemTheme.dimens.size8, + top = TangemTheme.dimens.size4, + bottom = TangemTheme.dimens.size4, + ) + .fillMaxWidth(), + ) { + TextField( + value = allowance.amountText, + onValueChange = onAmountChange, + label = { + Text( + text = stringResourceSafe(R.string.send_amount_label), + style = TangemTheme.typography.subtitle2, + color = TangemTheme.colors.text.tertiary, + ) + }, + textStyle = TangemTheme.typography.body1, + modifier = Modifier.weight(1f), + keyboardOptions = KeyboardOptions.Default.copy( + keyboardType = KeyboardType.Number, + ), + colors = TextFieldDefaults.colors( + unfocusedContainerColor = Color.Transparent, + focusedContainerColor = Color.Transparent, + focusedTextColor = TangemTheme.colors.text.primary1, + unfocusedTextColor = TangemTheme.colors.text.primary1, + focusedPlaceholderColor = TangemTheme.colors.text.disabled, + unfocusedPlaceholderColor = TangemTheme.colors.text.disabled, + focusedIndicatorColor = Color.Transparent, + unfocusedIndicatorColor = Color.Transparent, + cursorColor = TangemTheme.colors.icon.primary1, + ), + ) + + TokenWithNetworkIcon( + tokenIconUrl = allowance.tokenIconUrl, + networkIconRes = allowance.networkIconRes, + ) + } +} + +@Composable +private fun TokenWithNetworkIcon( + tokenIconUrl: String, + @DrawableRes networkIconRes: Int, + modifier: Modifier = Modifier, +) { + Box( + modifier = modifier + .size(TangemTheme.dimens.size48), + ) { + AsyncImage( + modifier = Modifier + .aspectRatio(1f) + .clip(CircleShape) + .align(Alignment.Center) + .padding(TangemTheme.dimens.spacing8), + model = tokenIconUrl, + contentDescription = null, + ) + Image( + modifier = Modifier + .align(Alignment.TopEnd) + .size(TangemTheme.dimens.size18) + .border(TangemTheme.dimens.size2, TangemTheme.colors.background.action, CircleShape) + .padding(TangemTheme.dimens.spacing2), + painter = painterResource(id = networkIconRes), + contentDescription = null, + ) + } +} + +@Composable +@Preview(showBackground = true, device = Devices.PIXEL_7_PRO) +@Preview(showBackground = true, device = Devices.PIXEL_7_PRO, uiMode = Configuration.UI_MODE_NIGHT_YES) +internal fun PreviewWcCustomAllowanceContent( + @PreviewParameter(WcCustomAllowanceStateProvider::class) state: WcCustomAllowanceUM, +) { + TangemThemePreview { + TangemModalBottomSheet( + config = TangemBottomSheetConfig( + isShown = true, + onDismissRequest = {}, + content = state, + ), + containerColor = TangemTheme.colors.background.tertiary, + title = { + TangemModalBottomSheetTitle( + title = resourceReference(R.string.wc_custom_allowance_title), + endIconRes = null, + onEndClick = {}, + startIconRes = R.drawable.ic_back_24, + onStartClick = {}, + ) + }, + content = { + WcCustomAllowanceContent( + allowance = state, + onAmountChange = {}, + onToggleChange = {}, + onClickDone = {}, + ) + }, + ) + } +} + +private class WcCustomAllowanceStateProvider : CollectionPreviewParameterProvider( + listOf( + WcCustomAllowanceUM( + networkIconRes = R.drawable.img_eth_22, + tokenIconUrl = "https://tangem.com", + amountText = "100", + isUnlimited = false, + ), + ), +) \ No newline at end of file From ff60ddae9f12e29e517aa6181ffad7f5c3230a54 Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 13 May 2025 21:05:15 +0500 Subject: [PATCH 030/165] Updated on 2026-08-14 --- app/build.gradle.kts | 1 + .../tangem/tap/di/domain/NotificationsDomainModule.kt | 9 +++++++++ .../DefaultNotificationsFeatureToggles.kt | 11 +++++++++++ .../main/assets/configs/feature_toggles_config.json | 4 ++++ .../api/tangemTech/models/UserTokensResponse.kt | 2 ++ domain/notifications/build.gradle.kts | 1 - domain/notifications/toggles/.gitignore | 1 + domain/notifications/toggles/build.gradle.kts | 4 ++++ .../toggles/NotificationsFeatureToggles.kt | 5 +++++ settings.gradle.kts | 1 + 10 files changed, 38 insertions(+), 1 deletion(-) create mode 100644 app/src/main/java/com/tangem/tap/domain/notifications/DefaultNotificationsFeatureToggles.kt create mode 100644 domain/notifications/toggles/.gitignore create mode 100644 domain/notifications/toggles/build.gradle.kts create mode 100644 domain/notifications/toggles/src/main/java/com/tangem/domain/notifications/toggles/NotificationsFeatureToggles.kt diff --git a/app/build.gradle.kts b/app/build.gradle.kts index e666baaa4d..37e399bc70 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -111,6 +111,7 @@ dependencies { implementation(projects.domain.networks) implementation(projects.domain.quotes) implementation(projects.domain.notifications) + implementation(projects.domain.notifications.toggles) implementation(projects.common) implementation(projects.common.routing) diff --git a/app/src/main/java/com/tangem/tap/di/domain/NotificationsDomainModule.kt b/app/src/main/java/com/tangem/tap/di/domain/NotificationsDomainModule.kt index c6f217e01c..706ef792e1 100644 --- a/app/src/main/java/com/tangem/tap/di/domain/NotificationsDomainModule.kt +++ b/app/src/main/java/com/tangem/tap/di/domain/NotificationsDomainModule.kt @@ -1,10 +1,13 @@ package com.tangem.tap.di.domain +import com.tangem.core.configtoggle.feature.FeatureTogglesManager import com.tangem.domain.notifications.GetApplicationIdUseCase import com.tangem.domain.notifications.GetTronFeeNotificationShowCountUseCase import com.tangem.domain.notifications.IncrementNotificationsShowCountUseCase import com.tangem.domain.notifications.SendPushTokenUseCase import com.tangem.domain.notifications.repository.NotificationsRepository +import com.tangem.domain.notifications.toggles.NotificationsFeatureToggles +import com.tangem.tap.domain.notifications.DefaultNotificationsFeatureToggles import com.tangem.utils.notifications.PushNotificationsTokenProvider import dagger.Module import dagger.Provides @@ -57,4 +60,10 @@ internal object NotificationsDomainModule { notificationsRepository = notificationsRepository, ) } + + @Provides + @Singleton + fun provideNotificationsFeatureToggles(featureTogglesManager: FeatureTogglesManager): NotificationsFeatureToggles { + return DefaultNotificationsFeatureToggles(featureTogglesManager = featureTogglesManager) + } } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/domain/notifications/DefaultNotificationsFeatureToggles.kt b/app/src/main/java/com/tangem/tap/domain/notifications/DefaultNotificationsFeatureToggles.kt new file mode 100644 index 0000000000..5a58923bb9 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/domain/notifications/DefaultNotificationsFeatureToggles.kt @@ -0,0 +1,11 @@ +package com.tangem.tap.domain.notifications + +import com.tangem.core.configtoggle.feature.FeatureTogglesManager +import com.tangem.domain.notifications.toggles.NotificationsFeatureToggles + +internal class DefaultNotificationsFeatureToggles( + private val featureTogglesManager: FeatureTogglesManager, +) : NotificationsFeatureToggles { + override val isNotificationsEnabled: Boolean + get() = featureTogglesManager.isFeatureEnabled("PUSH_NOTIFICATIONS_ENABLED") +} \ No newline at end of file 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 6a3b525e20..4d74917c2a 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 @@ -58,5 +58,9 @@ { "name": "STAKING_LOADING_REFACTORING_ENABLED", "version": "5.25.0" + }, + { + "name": "PUSH_NOTIFICATIONS_ENABLED", + "version": "undefined" } ] diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/models/UserTokensResponse.kt b/core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/models/UserTokensResponse.kt index 80e222a51b..ca02c9f581 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/models/UserTokensResponse.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/models/UserTokensResponse.kt @@ -9,6 +9,7 @@ data class UserTokensResponse( @Json(name = "version") val version: Int = 0, @Json(name = "group") val group: GroupType, @Json(name = "sort") val sort: SortType, + @Json(name = "notifyStatus") val notifyStatus: Boolean? = null, @Json(name = "tokens") val tokens: List = emptyList(), ) { @@ -21,6 +22,7 @@ data class UserTokensResponse( @Json(name = "symbol") val symbol: String, @Json(name = "decimals") val decimals: Int, @Json(name = "contractAddress") val contractAddress: String?, + @Json(name = "addresses") val list: List? = null, ) { override fun equals(other: Any?): Boolean { val otherToken = other as? Token ?: return false diff --git a/domain/notifications/build.gradle.kts b/domain/notifications/build.gradle.kts index a81496ca84..1b34deba2f 100644 --- a/domain/notifications/build.gradle.kts +++ b/domain/notifications/build.gradle.kts @@ -16,7 +16,6 @@ dependencies { implementation(projects.domain.wallets.models) implementation(projects.domain.tokens.models) implementation(projects.domain.notifications.models) - implementation(projects.domain.wallets) implementation(projects.libs.crypto) // region DI diff --git a/domain/notifications/toggles/.gitignore b/domain/notifications/toggles/.gitignore new file mode 100644 index 0000000000..42afabfd2a --- /dev/null +++ b/domain/notifications/toggles/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/domain/notifications/toggles/build.gradle.kts b/domain/notifications/toggles/build.gradle.kts new file mode 100644 index 0000000000..7ff7fb7522 --- /dev/null +++ b/domain/notifications/toggles/build.gradle.kts @@ -0,0 +1,4 @@ +plugins { + alias(deps.plugins.kotlin.jvm) + id("configuration") +} \ No newline at end of file diff --git a/domain/notifications/toggles/src/main/java/com/tangem/domain/notifications/toggles/NotificationsFeatureToggles.kt b/domain/notifications/toggles/src/main/java/com/tangem/domain/notifications/toggles/NotificationsFeatureToggles.kt new file mode 100644 index 0000000000..22bbec7d10 --- /dev/null +++ b/domain/notifications/toggles/src/main/java/com/tangem/domain/notifications/toggles/NotificationsFeatureToggles.kt @@ -0,0 +1,5 @@ +package com.tangem.domain.notifications.toggles + +interface NotificationsFeatureToggles { + val isNotificationsEnabled: Boolean +} \ No newline at end of file diff --git a/settings.gradle.kts b/settings.gradle.kts index 385d8b2dc8..80cd025090 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -292,6 +292,7 @@ include(":domain:blockaid") include(":domain:blockaid:models") include(":domain:notifications") include(":domain:notifications:models") +include(":domain:notifications:toggles") // endregion Domain modules // region Data modules From be43a77a08b2532dace1c1719e51caa63b1249fa Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 14 May 2025 13:55:36 +0400 Subject: [PATCH 031/165] Updated on 2026-08-14 --- .../tap/di/domain/TokensDomainModule.kt | 4 +- common/test/build.gradle.kts | 1 + .../MockUpdateWalletManagerResultFactory.kt | 64 +++++++++++++++++++ .../utils/NetworkStatusFactoryTest.kt | 30 ++------- .../repository/DefaultNetworksRepository.kt | 12 ---- .../tokens/GetCurrencyWarningsUseCase.kt | 5 +- .../tokens/repository/NetworksRepository.kt | 2 - .../repository/MockNetworksRepository.kt | 49 -------------- .../tangem/blockchainsdk/utils/Blockchain.kt | 11 +++- 9 files changed, 85 insertions(+), 93 deletions(-) create mode 100644 common/test/src/main/java/com/tangem/common/test/domain/walletmanager/MockUpdateWalletManagerResultFactory.kt delete mode 100644 domain/tokens/src/test/kotlin/com/tangem/domain/tokens/repository/MockNetworksRepository.kt diff --git a/app/src/main/java/com/tangem/tap/di/domain/TokensDomainModule.kt b/app/src/main/java/com/tangem/tap/di/domain/TokensDomainModule.kt index d6e0a771ad..f1b9bdeabc 100644 --- a/app/src/main/java/com/tangem/tap/di/domain/TokensDomainModule.kt +++ b/app/src/main/java/com/tangem/tap/di/domain/TokensDomainModule.kt @@ -143,7 +143,6 @@ internal object TokensDomainModule { fun provideGetCurrencyWarningsUseCase( walletManagersFacade: WalletManagersFacade, currenciesRepository: CurrenciesRepository, - networksRepository: NetworksRepository, currencyChecksRepository: CurrencyChecksRepository, dispatchers: CoroutineDispatcherProvider, baseCurrencyStatusOperations: BaseCurrencyStatusOperations, @@ -151,9 +150,8 @@ internal object TokensDomainModule { return GetCurrencyWarningsUseCase( walletManagersFacade = walletManagersFacade, currenciesRepository = currenciesRepository, - networksRepository = networksRepository, - currencyChecksRepository = currencyChecksRepository, dispatchers = dispatchers, + currencyChecksRepository = currencyChecksRepository, currencyStatusOperations = baseCurrencyStatusOperations, ) } diff --git a/common/test/build.gradle.kts b/common/test/build.gradle.kts index 62d076fdff..3013d947d3 100644 --- a/common/test/build.gradle.kts +++ b/common/test/build.gradle.kts @@ -19,6 +19,7 @@ dependencies { implementation(projects.domain.models) implementation(projects.domain.staking.models) implementation(projects.domain.tokens.models) + implementation(projects.domain.txhistory.models) implementation(projects.domain.wallets) implementation(projects.domain.wallets.models) diff --git a/common/test/src/main/java/com/tangem/common/test/domain/walletmanager/MockUpdateWalletManagerResultFactory.kt b/common/test/src/main/java/com/tangem/common/test/domain/walletmanager/MockUpdateWalletManagerResultFactory.kt new file mode 100644 index 0000000000..b7684db809 --- /dev/null +++ b/common/test/src/main/java/com/tangem/common/test/domain/walletmanager/MockUpdateWalletManagerResultFactory.kt @@ -0,0 +1,64 @@ +package com.tangem.common.test.domain.walletmanager + +import com.tangem.domain.txhistory.models.TxHistoryItem +import com.tangem.domain.walletmanager.model.Address +import com.tangem.domain.walletmanager.model.CryptoCurrencyAmount +import com.tangem.domain.walletmanager.model.CryptoCurrencyTransaction +import com.tangem.domain.walletmanager.model.UpdateWalletManagerResult +import java.math.BigDecimal + +/** +[REDACTED_AUTHOR] + */ +class MockUpdateWalletManagerResultFactory { + + fun createUnreachable(): UpdateWalletManagerResult { + return UpdateWalletManagerResult.Unreachable(selectedAddress = null, addresses = null) + } + + fun createUnreachableWithAddress(): UpdateWalletManagerResult { + return UpdateWalletManagerResult.Unreachable( + selectedAddress = "0x1", + addresses = setOf(Address(value = "0x1", type = Address.Type.Primary)), + ) + } + + fun createNoAccount(): UpdateWalletManagerResult { + return UpdateWalletManagerResult.NoAccount( + selectedAddress = "0x1", + addresses = setOf(Address(value = "0x1", type = Address.Type.Primary)), + amountToCreateAccount = BigDecimal.ZERO, + errorMessage = "", + ) + } + + fun createVerified(): UpdateWalletManagerResult { + return UpdateWalletManagerResult.Verified( + selectedAddress = "0x1", + addresses = setOf(Address(value = "0x1", type = Address.Type.Primary)), + currenciesAmounts = setOf( + CryptoCurrencyAmount.Coin(value = BigDecimal.ONE), + ), + currentTransactions = setOf( + CryptoCurrencyTransaction.Coin(txHistoryItem), + ), + ) + } + + private companion object { + + val txHistoryItem = TxHistoryItem( + txHash = "erroribus", + timestampInMillis = 2771, + isOutgoing = false, + destinationType = TxHistoryItem.DestinationType.Single( + addressType = TxHistoryItem.AddressType.User(address = "0x1"), + ), + sourceType = TxHistoryItem.SourceType.Single(address = "0x2"), + interactionAddressType = null, + status = TxHistoryItem.TransactionStatus.Confirmed, + type = TxHistoryItem.TransactionType.Transfer, + amount = BigDecimal.ONE, + ) + } +} \ No newline at end of file diff --git a/data/networks/src/test/java/com/tangem/data/networks/utils/NetworkStatusFactoryTest.kt b/data/networks/src/test/java/com/tangem/data/networks/utils/NetworkStatusFactoryTest.kt index 8e21f85ff3..531dbb5987 100644 --- a/data/networks/src/test/java/com/tangem/data/networks/utils/NetworkStatusFactoryTest.kt +++ b/data/networks/src/test/java/com/tangem/data/networks/utils/NetworkStatusFactoryTest.kt @@ -3,12 +3,11 @@ package com.tangem.data.networks.utils import com.google.common.truth.Truth import com.tangem.blockchain.common.Blockchain import com.tangem.common.test.domain.token.MockCryptoCurrencyFactory +import com.tangem.common.test.domain.walletmanager.MockUpdateWalletManagerResultFactory import com.tangem.domain.models.StatusSource import com.tangem.domain.tokens.model.* import com.tangem.domain.txhistory.models.TxHistoryItem import com.tangem.domain.walletmanager.model.Address -import com.tangem.domain.walletmanager.model.CryptoCurrencyAmount -import com.tangem.domain.walletmanager.model.CryptoCurrencyTransaction import com.tangem.domain.walletmanager.model.UpdateWalletManagerResult import org.junit.Test import org.junit.runner.RunWith @@ -68,6 +67,8 @@ internal class NetworkStatusFactoryTest(private val model: Model) { amount = BigDecimal.ONE, ) + val updateWalletManagerResultFactory = MockUpdateWalletManagerResultFactory() + @JvmStatic @Parameterized.Parameters fun data(): Collection = listOf( @@ -80,7 +81,7 @@ internal class NetworkStatusFactoryTest(private val model: Model) { // region Unreachable createSuccess( - result = UpdateWalletManagerResult.Unreachable(selectedAddress = null, addresses = null), + result = updateWalletManagerResultFactory.createUnreachable(), status = NetworkStatus.Unreachable(address = null), ), createSuccess( @@ -98,10 +99,7 @@ internal class NetworkStatusFactoryTest(private val model: Model) { status = NetworkStatus.Unreachable(address = null), ), createSuccess( - result = UpdateWalletManagerResult.Unreachable( - selectedAddress = "0x1", - addresses = setOf(Address(value = "0x1", type = Address.Type.Primary)), - ), + result = updateWalletManagerResultFactory.createUnreachableWithAddress(), status = NetworkStatus.Unreachable( address = NetworkAddress.Single( defaultAddress = NetworkAddress.Address( @@ -149,12 +147,7 @@ internal class NetworkStatusFactoryTest(private val model: Model) { throwable = selectedAddressThrowable, ), createSuccess( - result = UpdateWalletManagerResult.NoAccount( - selectedAddress = "0x1", - addresses = setOf(Address(value = "0x1", type = Address.Type.Primary)), - amountToCreateAccount = BigDecimal.ZERO, - errorMessage = "", - ), + result = updateWalletManagerResultFactory.createNoAccount(), status = NetworkStatus.NoAccount( address = NetworkAddress.Single( defaultAddress = NetworkAddress.Address( @@ -207,16 +200,7 @@ internal class NetworkStatusFactoryTest(private val model: Model) { throwable = selectedAddressThrowable, ), createSuccess( - result = UpdateWalletManagerResult.Verified( - selectedAddress = "0x1", - addresses = setOf(Address(value = "0x1", type = Address.Type.Primary)), - currenciesAmounts = setOf( - CryptoCurrencyAmount.Coin(value = BigDecimal.ONE), - ), - currentTransactions = setOf( - CryptoCurrencyTransaction.Coin(txHistoryItem), - ), - ), + result = updateWalletManagerResultFactory.createVerified(), currencies = currencies, status = NetworkStatus.Verified( address = NetworkAddress.Single( diff --git a/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultNetworksRepository.kt b/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultNetworksRepository.kt index 4426e81e54..46ee114672 100644 --- a/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultNetworksRepository.kt +++ b/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultNetworksRepository.kt @@ -1,9 +1,7 @@ package com.tangem.data.tokens.repository -import com.tangem.blockchain.common.Blockchain import com.tangem.blockchain.common.address.AddressType import com.tangem.blockchainsdk.utils.ExcludedBlockchains -import com.tangem.blockchainsdk.utils.fromNetworkId import com.tangem.data.common.cache.CacheRegistry import com.tangem.data.common.currency.CardCryptoCurrencyFactory import com.tangem.data.common.currency.ResponseCryptoCurrenciesFactory @@ -75,11 +73,6 @@ internal class DefaultNetworksRepository( networksStatusesStore.getSyncOrNull(userWalletId).orEmpty() } - override fun isNeedToCreateAccountWithoutReserve(network: Network): Boolean { - val blockchain = Blockchain.fromNetworkId(network.id.value) - return REQUIRED_ACCOUNT_WITHOUT_RESERVE_BLOCKCHAINS.contains(blockchain) - } - override suspend fun getNetworkAddresses( userWalletId: UserWalletId, network: Network, @@ -264,9 +257,4 @@ internal class DefaultNetworksRepository( private fun getNetworksStatusesCacheKey(userWalletId: UserWalletId, network: Network): String { return "network_status_${userWalletId}_${network.id.value}_${network.derivationPath.value}" } - - private companion object { - - val REQUIRED_ACCOUNT_WITHOUT_RESERVE_BLOCKCHAINS = listOf(Blockchain.Aptos, Blockchain.Filecoin) - } } \ No newline at end of file diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetCurrencyWarningsUseCase.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetCurrencyWarningsUseCase.kt index 00526a46a7..87c17435f0 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetCurrencyWarningsUseCase.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetCurrencyWarningsUseCase.kt @@ -1,5 +1,6 @@ package com.tangem.domain.tokens +import com.tangem.blockchainsdk.utils.isNeedToCreateAccountWithoutReserve import com.tangem.domain.models.StatusSource import com.tangem.domain.tokens.model.* import com.tangem.domain.tokens.model.warnings.CryptoCurrencyWarning @@ -8,7 +9,6 @@ import com.tangem.domain.tokens.model.warnings.KaspaWarnings import com.tangem.domain.tokens.operations.BaseCurrencyStatusOperations import com.tangem.domain.tokens.repository.CurrenciesRepository import com.tangem.domain.tokens.repository.CurrencyChecksRepository -import com.tangem.domain.tokens.repository.NetworksRepository import com.tangem.domain.transaction.models.AssetRequirementsCondition import com.tangem.domain.walletmanager.WalletManagersFacade import com.tangem.domain.wallets.models.UserWalletId @@ -21,7 +21,6 @@ import java.math.BigDecimal class GetCurrencyWarningsUseCase( private val walletManagersFacade: WalletManagersFacade, private val currenciesRepository: CurrenciesRepository, - private val networksRepository: NetworksRepository, private val dispatchers: CoroutineDispatcherProvider, private val currencyChecksRepository: CurrencyChecksRepository, private val currencyStatusOperations: BaseCurrencyStatusOperations, @@ -191,7 +190,7 @@ class GetCurrencyWarningsUseCase( private fun getNetworkNoAccountWarning(currencyStatus: CryptoCurrencyStatus): CryptoCurrencyWarning? { return (currencyStatus.value as? CryptoCurrencyStatus.NoAccount)?.let { - if (networksRepository.isNeedToCreateAccountWithoutReserve(network = currencyStatus.currency.network)) { + if (isNeedToCreateAccountWithoutReserve(networkId = currencyStatus.currency.network.id.value)) { CryptoCurrencyWarning.TopUpWithoutReserve } else { CryptoCurrencyWarning.SomeNetworksNoAccount( diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/repository/NetworksRepository.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/repository/NetworksRepository.kt index 1b66ccde06..47f82bff87 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/repository/NetworksRepository.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/repository/NetworksRepository.kt @@ -59,8 +59,6 @@ interface NetworksRepository { refresh: Boolean = false, ): Set - fun isNeedToCreateAccountWithoutReserve(network: Network): Boolean - /** * Returns list of addresses and crypto currency info of added currencies of [network] in selected wallet [userWalletId] */ diff --git a/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/repository/MockNetworksRepository.kt b/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/repository/MockNetworksRepository.kt deleted file mode 100644 index c7866234bf..0000000000 --- a/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/repository/MockNetworksRepository.kt +++ /dev/null @@ -1,49 +0,0 @@ -package com.tangem.domain.tokens.repository - -import arrow.core.Either -import arrow.core.getOrElse -import com.tangem.domain.core.error.DataError -import com.tangem.domain.tokens.model.CryptoCurrencyAddress -import com.tangem.domain.tokens.model.Network -import com.tangem.domain.tokens.model.NetworkStatus -import com.tangem.domain.wallets.models.UserWalletId -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.first -import kotlinx.coroutines.flow.map - -internal class MockNetworksRepository( - private val statuses: Flow>>, -) : NetworksRepository { - - override fun getNetworkStatusesUpdates( - userWalletId: UserWalletId, - networks: Set, - ): Flow> { - return statuses.map { it.getOrElse { e -> throw e } } - } - - override suspend fun fetchNetworkStatuses(userWalletId: UserWalletId, networks: Set, refresh: Boolean) { - /* no-op */ - } - - override suspend fun fetchNetworkPendingTransactions(userWalletId: UserWalletId, networks: Set) { - // no-op - } - - override suspend fun getNetworkStatusesSync( - userWalletId: UserWalletId, - networks: Set, - refresh: Boolean, - ): Set { - return statuses.map { it.getOrElse { e -> throw e } }.first() - } - - override fun isNeedToCreateAccountWithoutReserve(network: Network) = false - - override suspend fun getNetworkAddresses( - userWalletId: UserWalletId, - network: Network, - ): List { - return emptyList() - } -} \ No newline at end of file diff --git a/libs/blockchain-sdk/src/main/java/com/tangem/blockchainsdk/utils/Blockchain.kt b/libs/blockchain-sdk/src/main/java/com/tangem/blockchainsdk/utils/Blockchain.kt index 39409aaddb..c7512e7946 100644 --- a/libs/blockchain-sdk/src/main/java/com/tangem/blockchainsdk/utils/Blockchain.kt +++ b/libs/blockchain-sdk/src/main/java/com/tangem/blockchainsdk/utils/Blockchain.kt @@ -468,8 +468,17 @@ fun Blockchain.amountToCreateAccount(walletManager: WalletManager, token: Token? } } +/** Returns flag that determines whether account should be created without reserve by [networkId] */ +fun isNeedToCreateAccountWithoutReserve(networkId: String): Boolean { + val blockchain = Blockchain.fromNetworkId(networkId = networkId) + + return REQUIRED_ACCOUNT_WITHOUT_RESERVE_BLOCKCHAINS.contains(blockchain) +} + const val OLD_POLYGON_NAME = "matic-network" const val NEW_POLYGON_NAME = "polygon-ecosystem-token" private const val NODL = "NODL" -private const val NODL_AMOUNT_TO_CREATE_ACCOUNT = 1.5 \ No newline at end of file +private const val NODL_AMOUNT_TO_CREATE_ACCOUNT = 1.5 + +private val REQUIRED_ACCOUNT_WITHOUT_RESERVE_BLOCKCHAINS = listOf(Blockchain.Aptos, Blockchain.Filecoin) \ No newline at end of file From 8fa305a162e03af90cec44ab16cce472a3e35c2b Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 14 May 2025 17:08:46 +0500 Subject: [PATCH 032/165] Updated on 2026-08-14 --- app/src/main/assets/tangem-app-config | 2 +- .../environment/converter/BlockchainSDKConfigConverter.kt | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/app/src/main/assets/tangem-app-config b/app/src/main/assets/tangem-app-config index a20134ee4b..0828475441 160000 --- a/app/src/main/assets/tangem-app-config +++ b/app/src/main/assets/tangem-app-config @@ -1 +1 @@ -Subproject commit a20134ee4be4d7e1e34c6f44089b3cb0713eacc3 +Subproject commit 08284754412397ae4ad9eb5b0073d7d7db1e1059 diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/config/environment/converter/BlockchainSDKConfigConverter.kt b/core/datasource/src/main/java/com/tangem/datasource/local/config/environment/converter/BlockchainSDKConfigConverter.kt index b44911fe1e..c19f0204c0 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/local/config/environment/converter/BlockchainSDKConfigConverter.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/local/config/environment/converter/BlockchainSDKConfigConverter.kt @@ -43,6 +43,7 @@ internal object BlockchainSDKConfigConverter : Converter Date: Wed, 14 May 2025 17:13:57 +0400 Subject: [PATCH 033/165] Updated on 2026-08-14 --- .../network/MockNetworkStatusFactory.kt | 8 +- .../multi/DefaultMultiNetworkStatusFetcher.kt | 12 +- .../DefaultSingleNetworkStatusFetcher.kt | 28 +- .../store/DefaultNetworksStatusesStoreV2.kt | 87 +--- .../networks/store/NetworkStatusesStoreExt.kt | 104 ++++ .../networks/store/NetworksStatusesStoreV2.kt | 39 +- .../DefaultSingleNetworkStatusFetcherTest.kt | 12 +- .../com/tangem/data/networks/store/GetTest.kt | 158 ++++++ ...alizationTest.kt => InitializationTest.kt} | 2 +- .../NetworkStatusesStoreUpdateMethodsTest.kt | 336 ------------ .../NetworksStatusesStoreGetMethodTest.kt | 83 --- .../store/ParameterizedStoreStatusTest.kt | 131 +++++ .../store/ParameterizedStoreSuccessTest.kt | 129 +++++ .../networks/store/ParameterizedStoreTest.kt | 95 ++++ .../networks/store/SetSourceAsCacheTest.kt | 154 ++++++ .../store/SetSourceAsOnlyCacheTest.kt | 214 ++++++++ .../store/StoreActualNetworkStatusTest.kt | 105 ---- .../data/networks/store/StoreStatusTest.kt | 247 +++++++++ .../data/networks/store/StoreSuccessTest.kt | 132 +++++ .../tangem/data/networks/store/StoreTest.kt | 132 +++++ .../networks/store/UpdateStatusSourceTest.kt | 477 ++++++++++++++++++ 21 files changed, 2046 insertions(+), 639 deletions(-) create mode 100644 data/networks/src/main/java/com/tangem/data/networks/store/NetworkStatusesStoreExt.kt create mode 100644 data/networks/src/test/java/com/tangem/data/networks/store/GetTest.kt rename data/networks/src/test/java/com/tangem/data/networks/store/{NetworksStatusesStoreInitializationTest.kt => InitializationTest.kt} (98%) delete mode 100644 data/networks/src/test/java/com/tangem/data/networks/store/NetworkStatusesStoreUpdateMethodsTest.kt delete mode 100644 data/networks/src/test/java/com/tangem/data/networks/store/NetworksStatusesStoreGetMethodTest.kt create mode 100644 data/networks/src/test/java/com/tangem/data/networks/store/ParameterizedStoreStatusTest.kt create mode 100644 data/networks/src/test/java/com/tangem/data/networks/store/ParameterizedStoreSuccessTest.kt create mode 100644 data/networks/src/test/java/com/tangem/data/networks/store/ParameterizedStoreTest.kt create mode 100644 data/networks/src/test/java/com/tangem/data/networks/store/SetSourceAsCacheTest.kt create mode 100644 data/networks/src/test/java/com/tangem/data/networks/store/SetSourceAsOnlyCacheTest.kt delete mode 100644 data/networks/src/test/java/com/tangem/data/networks/store/StoreActualNetworkStatusTest.kt create mode 100644 data/networks/src/test/java/com/tangem/data/networks/store/StoreStatusTest.kt create mode 100644 data/networks/src/test/java/com/tangem/data/networks/store/StoreSuccessTest.kt create mode 100644 data/networks/src/test/java/com/tangem/data/networks/store/StoreTest.kt create mode 100644 data/networks/src/test/java/com/tangem/data/networks/store/UpdateStatusSourceTest.kt diff --git a/common/test/src/main/java/com/tangem/common/test/domain/network/MockNetworkStatusFactory.kt b/common/test/src/main/java/com/tangem/common/test/domain/network/MockNetworkStatusFactory.kt index 34153765ce..6a1da92250 100644 --- a/common/test/src/main/java/com/tangem/common/test/domain/network/MockNetworkStatusFactory.kt +++ b/common/test/src/main/java/com/tangem/common/test/domain/network/MockNetworkStatusFactory.kt @@ -14,7 +14,7 @@ object MockNetworkStatusFactory { private val defaultNetwork = MockCryptoCurrencyFactory().ethereum.network - fun createVerified(network: Network = defaultNetwork): NetworkStatus { + fun createVerified(network: Network = defaultNetwork, source: StatusSource = StatusSource.ACTUAL): NetworkStatus { return NetworkStatus( network = network, value = NetworkStatus.Verified( @@ -26,12 +26,12 @@ object MockNetworkStatusFactory { ), amounts = mapOf(), pendingTransactions = mapOf(), - source = StatusSource.ACTUAL, + source = source, ), ) } - fun createNoAccount(network: Network = defaultNetwork): NetworkStatus { + fun createNoAccount(network: Network = defaultNetwork, source: StatusSource = StatusSource.ACTUAL): NetworkStatus { return NetworkStatus( network = network, value = NetworkStatus.NoAccount( @@ -43,7 +43,7 @@ object MockNetworkStatusFactory { ), amountToCreateAccount = BigDecimal.ONE, errorMessage = "", - source = StatusSource.ACTUAL, + source = source, ), ) } diff --git a/data/networks/src/main/java/com/tangem/data/networks/multi/DefaultMultiNetworkStatusFetcher.kt b/data/networks/src/main/java/com/tangem/data/networks/multi/DefaultMultiNetworkStatusFetcher.kt index c606c84658..67a3ba3d44 100644 --- a/data/networks/src/main/java/com/tangem/data/networks/multi/DefaultMultiNetworkStatusFetcher.kt +++ b/data/networks/src/main/java/com/tangem/data/networks/multi/DefaultMultiNetworkStatusFetcher.kt @@ -8,6 +8,8 @@ import com.tangem.blockchainsdk.utils.fromNetworkId import com.tangem.data.common.currency.CardCryptoCurrencyFactory import com.tangem.data.common.currency.ResponseCryptoCurrenciesFactory import com.tangem.data.networks.store.NetworksStatusesStoreV2 +import com.tangem.data.networks.store.setSourceAsCache +import com.tangem.data.networks.store.setSourceAsOnlyCache import com.tangem.datasource.api.tangemTech.models.UserTokensResponse import com.tangem.datasource.local.preferences.AppPreferencesStore import com.tangem.datasource.local.preferences.PreferencesKeys @@ -51,12 +53,16 @@ internal class DefaultMultiNetworkStatusFetcher @Inject constructor( // Optimization! // Every singleNetworkStatusFetcher with applyRefresh as true will refresh every network in the store. // So if we update all networks at once, it will be more efficient. - networksStatusesStore.refresh(userWalletId = params.userWalletId, networks = params.networks) + networksStatusesStore.setSourceAsCache(userWalletId = params.userWalletId, networks = params.networks) val userWallet = catch( block = { userWalletsStore.getSyncStrict(key = params.userWalletId) }, catch = { - networksStatusesStore.storeError(userWalletId = params.userWalletId, networks = params.networks) + networksStatusesStore.setSourceAsOnlyCache( + userWalletId = params.userWalletId, + networks = params.networks, + ) + raise(it) }, ) @@ -66,7 +72,7 @@ internal class DefaultMultiNetworkStatusFetcher @Inject constructor( } ensure(isNotSingleWallet) { - networksStatusesStore.storeError(userWalletId = params.userWalletId, networks = params.networks) + networksStatusesStore.setSourceAsOnlyCache(userWalletId = params.userWalletId, networks = params.networks) IllegalStateException("User wallet is not multi-currency") } diff --git a/data/networks/src/main/java/com/tangem/data/networks/single/DefaultSingleNetworkStatusFetcher.kt b/data/networks/src/main/java/com/tangem/data/networks/single/DefaultSingleNetworkStatusFetcher.kt index ca3b03ba67..ef23a8c4fc 100644 --- a/data/networks/src/main/java/com/tangem/data/networks/single/DefaultSingleNetworkStatusFetcher.kt +++ b/data/networks/src/main/java/com/tangem/data/networks/single/DefaultSingleNetworkStatusFetcher.kt @@ -3,11 +3,13 @@ package com.tangem.data.networks.single import arrow.core.Either import com.tangem.data.common.currency.CardCryptoCurrencyFactory import com.tangem.data.networks.store.NetworksStatusesStoreV2 +import com.tangem.data.networks.store.setSourceAsCache +import com.tangem.data.networks.store.setSourceAsOnlyCache +import com.tangem.data.networks.store.storeStatus import com.tangem.data.networks.utils.NetworkStatusFactory import com.tangem.domain.core.utils.catchOn import com.tangem.domain.networks.single.SingleNetworkStatusFetcher import com.tangem.domain.tokens.model.CryptoCurrency -import com.tangem.domain.tokens.model.NetworkStatus import com.tangem.domain.walletmanager.WalletManagersFacade import com.tangem.utils.coroutines.CoroutineDispatcherProvider import kotlinx.coroutines.withContext @@ -35,7 +37,7 @@ internal class DefaultSingleNetworkStatusFetcher @Inject constructor( val networkCurrencies = when (params) { is SingleNetworkStatusFetcher.Params.Prepared -> params.addedNetworkCurrencies is SingleNetworkStatusFetcher.Params.Simple -> { - networksStatusesStore.refresh(userWalletId = params.userWalletId, network = params.network) + networksStatusesStore.setSourceAsCache(userWalletId = params.userWalletId, network = params.network) cardCryptoCurrencyFactory.create( userWalletId = params.userWalletId, @@ -60,28 +62,10 @@ internal class DefaultSingleNetworkStatusFetcher @Inject constructor( addedCurrencies = networkCurrencies.toSet(), ) - val statusValue = status.value - if (statusValue is NetworkStatus.Unreachable) { - val prevStatus = networksStatusesStore.getSyncOrNull( - userWalletId = params.userWalletId, - network = params.network, - ) - - if (prevStatus?.value is NetworkStatus.MissedDerivation) { - networksStatusesStore.storeUnreachableStatus(userWalletId = params.userWalletId, value = status) - } else { - networksStatusesStore.storeError( - userWalletId = params.userWalletId, - network = params.network, - value = statusValue, - ) - } - } else { - networksStatusesStore.storeSuccess(userWalletId = params.userWalletId, value = status) - } + networksStatusesStore.storeStatus(userWalletId = params.userWalletId, status = status) } .onLeft { Timber.e("Failed to fetch network status for $params: $it") - networksStatusesStore.storeError(userWalletId = params.userWalletId, network = params.network) + networksStatusesStore.setSourceAsOnlyCache(userWalletId = params.userWalletId, network = params.network) } } \ No newline at end of file diff --git a/data/networks/src/main/java/com/tangem/data/networks/store/DefaultNetworksStatusesStoreV2.kt b/data/networks/src/main/java/com/tangem/data/networks/store/DefaultNetworksStatusesStoreV2.kt index 943e6d1400..7bc6845b13 100644 --- a/data/networks/src/main/java/com/tangem/data/networks/store/DefaultNetworksStatusesStoreV2.kt +++ b/data/networks/src/main/java/com/tangem/data/networks/store/DefaultNetworksStatusesStoreV2.kt @@ -61,78 +61,43 @@ internal class DefaultNetworksStatusesStoreV2( return runtimeStore.getSyncOrNull()?.get(userWalletId.stringValue)?.firstOrNull { it.id == simpleStatusId } } - override suspend fun refresh(userWalletId: UserWalletId, network: Network) { - refresh(userWalletId = userWalletId, networks = setOf(network)) + override suspend fun updateStatusSource( + userWalletId: UserWalletId, + network: Network, + source: StatusSource, + ifNotFound: (SimpleNetworkStatus.Id) -> SimpleNetworkStatus?, + ) { + updateStatusSource( + userWalletId = userWalletId, + networks = setOf(network), + source = source, + ifNotFound = ifNotFound, + ) } - override suspend fun refresh(userWalletId: UserWalletId, networks: Set) { + override suspend fun updateStatusSource( + userWalletId: UserWalletId, + networks: Set, + source: StatusSource, + ifNotFound: (SimpleNetworkStatus.Id) -> SimpleNetworkStatus?, + ) { if (networks.isEmpty()) { - Timber.d("Nothing to refresh: networks is empty") + Timber.d("Nothing to update: networks is empty") return } - updateInRuntime(userWalletId = userWalletId, networks = networks) { status -> - status.copy(value = status.value.copySealed(source = StatusSource.CACHE)) + updateInRuntime(userWalletId = userWalletId, networks = networks, ifNotFound = ifNotFound) { status -> + status.copy(value = status.value.copySealed(source = source)) } } - override suspend fun storeSuccess(userWalletId: UserWalletId, value: NetworkStatus) { - if (value.value is NetworkStatus.Unreachable) { - val message = "Use storeError method to save unreachable status" - Timber.d(message) - - error(message) - } - - if (value.value.source != StatusSource.ACTUAL) { - val message = "Method storeActual can be called only with StatusSource.ACTUAL" - Timber.d(message) - - error(message) - } - + override suspend fun store(userWalletId: UserWalletId, status: NetworkStatus) { coroutineScope { - launch { storeInRuntime(userWalletId = userWalletId, status = value) } - launch { storeInPersistence(userWalletId = userWalletId, status = value) } + launch { storeInRuntime(userWalletId = userWalletId, status = status) } + launch { storeInPersistence(userWalletId = userWalletId, status = status) } } } - override suspend fun storeError(userWalletId: UserWalletId, network: Network, value: NetworkStatus.Unreachable?) { - updateInRuntime( - userWalletId = userWalletId, - networks = setOf(network), - ifNotFound = { id -> - value?.let { SimpleNetworkStatus(id = id, value = value) } - ?: createUnreachableStatus(id = id) - }, - update = { status -> - status.copy(value = status.value.copySealed(source = StatusSource.ONLY_CACHE)) - }, - ) - } - - override suspend fun storeError(userWalletId: UserWalletId, networks: Set) { - updateInRuntime( - userWalletId = userWalletId, - networks = networks, - ifNotFound = ::createUnreachableStatus, - update = { status -> - status.copy(value = status.value.copySealed(source = StatusSource.ONLY_CACHE)) - }, - ) - } - - override suspend fun storeUnreachableStatus(userWalletId: UserWalletId, value: NetworkStatus) { - if (value.value !is NetworkStatus.Unreachable) { - val message = "Use storeError method to save unreachable status" - Timber.d(message) - - error(message) - } - - storeInRuntime(userWalletId = userWalletId, status = value) - } - private suspend fun updateInRuntime( userWalletId: UserWalletId, networks: Set, @@ -189,8 +154,4 @@ internal class DefaultNetworksStatusesStoreV2( } } } - - private fun createUnreachableStatus(id: SimpleNetworkStatus.Id): SimpleNetworkStatus { - return SimpleNetworkStatus(id = id, value = NetworkStatus.Unreachable(address = null)) - } } \ No newline at end of file diff --git a/data/networks/src/main/java/com/tangem/data/networks/store/NetworkStatusesStoreExt.kt b/data/networks/src/main/java/com/tangem/data/networks/store/NetworkStatusesStoreExt.kt new file mode 100644 index 0000000000..3d7cd30e19 --- /dev/null +++ b/data/networks/src/main/java/com/tangem/data/networks/store/NetworkStatusesStoreExt.kt @@ -0,0 +1,104 @@ +package com.tangem.data.networks.store + +import com.tangem.data.networks.models.SimpleNetworkStatus +import com.tangem.domain.models.StatusSource +import com.tangem.domain.tokens.model.Network +import com.tangem.domain.tokens.model.NetworkStatus +import com.tangem.domain.wallets.models.UserWalletId +import timber.log.Timber + +/** + * Store actual network [status] by [userWalletId]. + * If [status]'s source is not [StatusSource.ACTUAL], throws exception. + */ +internal suspend fun NetworksStatusesStoreV2.storeStatus(userWalletId: UserWalletId, status: NetworkStatus) { + if (status.value is NetworkStatus.Unreachable) { + val prevStatus = getSyncOrNull(userWalletId = userWalletId, network = status.network) + + /** + * Specific logic!!! + * If the previous status is [NetworkStatus.MissedDerivation] and current status is [NetworkStatus.Unreachable], + * then just store [NetworkStatus.MissedDerivation]. + */ + if (prevStatus?.value is NetworkStatus.MissedDerivation) { + store(userWalletId = userWalletId, status = status) + } else { + setSourceAsOnlyCache( + userWalletId = userWalletId, + network = status.network, + value = requireNotNull(status.value as? NetworkStatus.Unreachable), + ) + } + } else { + storeSuccess(userWalletId = userWalletId, status = status) + } +} + +/** + * Store actual success [status] by [userWalletId]. + * If [status]'s value is [NetworkStatus.Unreachable] and/or source is not [StatusSource.ACTUAL], throws exception. + */ +internal suspend fun NetworksStatusesStoreV2.storeSuccess(userWalletId: UserWalletId, status: NetworkStatus) { + if (status.value is NetworkStatus.Unreachable) { + val message = "Use storeError method to save unreachable status" + Timber.d(message) + + error(message) + } + + if (status.value.source != StatusSource.ACTUAL) { + val message = "Method storeActual can be called only with StatusSource.ACTUAL" + Timber.d(message) + + error(message) + } + + store(userWalletId = userWalletId, status = status) +} + +/** Set [StatusSource] as [StatusSource.CACHE] for [network] by [userWalletId] */ +internal suspend fun NetworksStatusesStoreV2.setSourceAsCache(userWalletId: UserWalletId, network: Network) { + setSourceAsCache(userWalletId = userWalletId, networks = setOf(network)) +} + +/** Set [StatusSource] as [StatusSource.CACHE] for [networks] by [userWalletId] */ +internal suspend fun NetworksStatusesStoreV2.setSourceAsCache(userWalletId: UserWalletId, networks: Set) { + updateStatusSource(userWalletId = userWalletId, networks = networks, source = StatusSource.CACHE) +} + +/** + * Set [StatusSource] as [StatusSource.ONLY_CACHE] for [network] by [userWalletId]. + * If the stored status is not found, store the [value] or a default [NetworkStatus.Unreachable] status. + */ +internal suspend fun NetworksStatusesStoreV2.setSourceAsOnlyCache( + userWalletId: UserWalletId, + network: Network, + value: NetworkStatus.Unreachable? = null, +) { + updateStatusSource( + userWalletId = userWalletId, + network = network, + source = StatusSource.ONLY_CACHE, + ifNotFound = { id -> + value?.let { SimpleNetworkStatus(id = id, value = value) } + ?: createUnreachableStatus(id = id) + }, + ) +} + +/** + * Set [StatusSource] as [StatusSource.ONLY_CACHE] for [networks] by [userWalletId]. + * If the stored status is not found, store a default [NetworkStatus.Unreachable] status. + */ +internal suspend fun NetworksStatusesStoreV2.setSourceAsOnlyCache(userWalletId: UserWalletId, networks: Set) { + updateStatusSource( + userWalletId = userWalletId, + networks = networks, + source = StatusSource.ONLY_CACHE, + ifNotFound = ::createUnreachableStatus, + ) +} + +private fun createUnreachableStatus(id: SimpleNetworkStatus.Id): SimpleNetworkStatus { + return SimpleNetworkStatus(id = id, value = NetworkStatus.Unreachable(address = null)) +} \ No newline at end of file diff --git a/data/networks/src/main/java/com/tangem/data/networks/store/NetworksStatusesStoreV2.kt b/data/networks/src/main/java/com/tangem/data/networks/store/NetworksStatusesStoreV2.kt index fb40156849..af6f25e12d 100644 --- a/data/networks/src/main/java/com/tangem/data/networks/store/NetworksStatusesStoreV2.kt +++ b/data/networks/src/main/java/com/tangem/data/networks/store/NetworksStatusesStoreV2.kt @@ -1,6 +1,7 @@ package com.tangem.data.networks.store import com.tangem.data.networks.models.SimpleNetworkStatus +import com.tangem.domain.models.StatusSource import com.tangem.domain.tokens.model.Network import com.tangem.domain.tokens.model.NetworkStatus import com.tangem.domain.wallets.models.UserWalletId @@ -15,25 +16,29 @@ internal interface NetworksStatusesStoreV2 { /** Get status of [network] by [userWalletId] synchronously or null */ suspend fun getSyncOrNull(userWalletId: UserWalletId, network: Network): SimpleNetworkStatus? - /** Refresh status of [network] by [userWalletId] */ - suspend fun refresh(userWalletId: UserWalletId, network: Network) + /** + * Update [source] of [network] by [userWalletId]. + * If the status is not found, create a new one by [ifNotFound]. + */ + suspend fun updateStatusSource( + userWalletId: UserWalletId, + network: Network, + source: StatusSource, + ifNotFound: (SimpleNetworkStatus.Id) -> SimpleNetworkStatus? = { null }, + ) - /** Refresh statuses of [networks] by [userWalletId] */ - suspend fun refresh(userWalletId: UserWalletId, networks: Set) - - /** Store success [value] by [userWalletId]. If status's value is unreachable, throws exception */ - @Throws - suspend fun storeSuccess(userWalletId: UserWalletId, value: NetworkStatus) + /** Update [source] of [networks] by [userWalletId]. If the status is not found, create a new one by [ifNotFound] */ + suspend fun updateStatusSource( + userWalletId: UserWalletId, + networks: Set, + source: StatusSource, + ifNotFound: (SimpleNetworkStatus.Id) -> SimpleNetworkStatus? = { null }, + ) /** - * Store error [value] by [userWalletId] and [network]. - * If [value] is null, default unreachable status will be stored. + * Store [status] by [userWalletId]. Rewrite the stored status with a new [status]. + * + * See complex methods in `NetworksStatusesStoreExt`. */ - suspend fun storeError(userWalletId: UserWalletId, network: Network, value: NetworkStatus.Unreachable? = null) - - /** Store unreachable [value] by [userWalletId] */ - suspend fun storeUnreachableStatus(userWalletId: UserWalletId, value: NetworkStatus) - - /** Store error for [networks] by [userWalletId] */ - suspend fun storeError(userWalletId: UserWalletId, networks: Set) + suspend fun store(userWalletId: UserWalletId, status: NetworkStatus) } \ No newline at end of file diff --git a/data/networks/src/test/java/com/tangem/data/networks/single/DefaultSingleNetworkStatusFetcherTest.kt b/data/networks/src/test/java/com/tangem/data/networks/single/DefaultSingleNetworkStatusFetcherTest.kt index bbd429ae23..813f2d20c1 100644 --- a/data/networks/src/test/java/com/tangem/data/networks/single/DefaultSingleNetworkStatusFetcherTest.kt +++ b/data/networks/src/test/java/com/tangem/data/networks/single/DefaultSingleNetworkStatusFetcherTest.kt @@ -4,6 +4,8 @@ import com.google.common.truth.Truth import com.tangem.common.test.domain.token.MockCryptoCurrencyFactory import com.tangem.data.common.currency.CardCryptoCurrencyFactory import com.tangem.data.networks.store.NetworksStatusesStoreV2 +import com.tangem.data.networks.store.setSourceAsCache +import com.tangem.data.networks.store.storeSuccess import com.tangem.domain.networks.single.SingleNetworkStatusFetcher import com.tangem.domain.tokens.model.NetworkStatus import com.tangem.domain.walletmanager.WalletManagersFacade @@ -45,12 +47,12 @@ internal class DefaultSingleNetworkStatusFetcherTest { val actual = fetcher(params) coVerifyOrder { - networksStatusesStore.refresh(params.userWalletId, params.network) + networksStatusesStore.setSourceAsCache(params.userWalletId, params.network) cardCryptoCurrencyFactory.create(params.userWalletId, params.network) walletManagersFacade.update(params.userWalletId, params.network, emptySet()) networksStatusesStore.storeSuccess( userWalletId = params.userWalletId, - value = NetworkStatus(params.network, NetworkStatus.MissedDerivation), + status = NetworkStatus(params.network, NetworkStatus.MissedDerivation), ) } @@ -67,14 +69,14 @@ internal class DefaultSingleNetworkStatusFetcherTest { val actual = fetcher(params) coVerifyOrder { - networksStatusesStore.refresh(userWalletId = params.userWalletId, network = params.network) + networksStatusesStore.setSourceAsCache(userWalletId = params.userWalletId, network = params.network) cardCryptoCurrencyFactory.create(userWalletId = params.userWalletId, network = params.network) - networksStatusesStore.storeError(userWalletId = params.userWalletId, network = params.network) + // networksStatusesStore.setSourceAsOnlyCache(userWalletId = params.userWalletId, network = params.network) } coVerify(inverse = true) { walletManagersFacade.update(userWalletId = any(), network = any(), extraTokens = any()) - networksStatusesStore.storeSuccess(userWalletId = any(), value = any()) + // networksStatusesStore.storeSuccess(userWalletId = any(), status = any()) } Truth.assertThat(actual.isLeft()).isTrue() diff --git a/data/networks/src/test/java/com/tangem/data/networks/store/GetTest.kt b/data/networks/src/test/java/com/tangem/data/networks/store/GetTest.kt new file mode 100644 index 0000000000..eca488b451 --- /dev/null +++ b/data/networks/src/test/java/com/tangem/data/networks/store/GetTest.kt @@ -0,0 +1,158 @@ +package com.tangem.data.networks.store + +import com.google.common.truth.Truth +import com.tangem.blockchain.common.Blockchain +import com.tangem.common.test.datastore.MockStateDataStore +import com.tangem.common.test.domain.network.MockNetworkStatusFactory +import com.tangem.common.test.domain.token.MockCryptoCurrencyFactory +import com.tangem.common.test.utils.getEmittedValues +import com.tangem.data.networks.models.SimpleNetworkStatus +import com.tangem.data.networks.toSimple +import com.tangem.datasource.local.datastore.RuntimeSharedStore +import com.tangem.domain.wallets.models.UserWalletId +import com.tangem.utils.coroutines.TestingCoroutineDispatcherProvider +import kotlinx.coroutines.test.runTest +import org.junit.Test + +/** +[REDACTED_AUTHOR] + */ +internal class GetTest { + + private val runtimeStore = RuntimeSharedStore() + private val persistenceStore = MockStateDataStore(default = emptyMap()) + + private val store = DefaultNetworksStatusesStoreV2( + runtimeStore = runtimeStore, + persistenceDataStore = persistenceStore, + dispatchers = TestingCoroutineDispatcherProvider(), + ) + + @Test + fun `get if runtime store is empty`() = runTest { + val actual = store.get(userWalletId = userWalletId) + + val values = getEmittedValues(flow = actual) + + Truth.assertThat(values).isEqualTo(emptyList>()) + } + + @Test + fun `get if runtime store contains empty map`() = runTest { + runtimeStore.store(value = emptyMap()) + + val actual = store.get(userWalletId = userWalletId) + + val values = getEmittedValues(flow = actual) + + Truth.assertThat(values).isEqualTo(emptyList>()) + } + + @Test + fun `get if runtime store contains portfolio with empty statuses`() = runTest { + runtimeStore.store( + value = mapOf(userWalletId.stringValue to emptySet()), + ) + + val actual = store.get(userWalletId = userWalletId) + + val values = getEmittedValues(flow = actual) + + Truth.assertThat(values.size).isEqualTo(1) + Truth.assertThat(values).isEqualTo(listOf(emptySet())) + } + + @Test + fun `get if runtime store is not empty`() = runTest { + val status = MockNetworkStatusFactory.createVerified() + + runtimeStore.store( + value = mapOf(userWalletId.stringValue to setOf(status.toSimple())), + ) + + val actual = store.get(userWalletId = userWalletId) + + val values = getEmittedValues(flow = actual) + + Truth.assertThat(values.size).isEqualTo(1) + Truth.assertThat(values).isEqualTo(listOf(setOf(status.toSimple()))) + } + + @Test + fun `getSyncOrNull if runtime store is empty`() = runTest { + val network = MockNetworkStatusFactory.createVerified().network + + val actual = store.getSyncOrNull(userWalletId = userWalletId, network = network) + + Truth.assertThat(actual).isEqualTo(null) + } + + @Test + fun `getSyncOrNull if runtime store contains empty map`() = runTest { + val network = MockNetworkStatusFactory.createVerified().network + + runtimeStore.store(value = emptyMap()) + + val actual = store.getSyncOrNull(userWalletId = userWalletId, network = network) + + Truth.assertThat(actual).isEqualTo(null) + } + + @Test + fun `getSyncOrNull if runtime store contains portfolio with empty statuses`() = runTest { + val network = MockNetworkStatusFactory.createVerified().network + + runtimeStore.store( + value = mapOf(userWalletId.stringValue to emptySet()), + ) + + val actual = store.getSyncOrNull(userWalletId = userWalletId, network = network) + + Truth.assertThat(actual).isEqualTo(null) + } + + @Test + fun `getSyncOrNull if runtime store is not empty`() = runTest { + val unreachableStatus = MockNetworkStatusFactory.createUnreachable( + network = MockCryptoCurrencyFactory().createCoin(Blockchain.Ethereum).network, + ) + val missedDerivationStatus = MockNetworkStatusFactory.createMissedDerivation( + network = MockCryptoCurrencyFactory().createCoin(Blockchain.Bitcoin).network, + ) + val noAccountStatus = MockNetworkStatusFactory.createNoAccount( + network = MockCryptoCurrencyFactory().createCoin(Blockchain.Solana).network, + ) + val verifiedStatus = MockNetworkStatusFactory.createVerified( + network = MockCryptoCurrencyFactory().createCoin(Blockchain.Stellar).network, + ) + + runtimeStore.store( + value = mapOf( + userWalletId.stringValue to setOf( + unreachableStatus.toSimple(), + missedDerivationStatus.toSimple(), + noAccountStatus.toSimple(), + verifiedStatus.toSimple(), + ), + ), + ) + + val actual = listOf(unreachableStatus, missedDerivationStatus, noAccountStatus, verifiedStatus).map { + store.getSyncOrNull(userWalletId = userWalletId, network = it.network) + } + + val expected = listOf( + unreachableStatus.toSimple(), + missedDerivationStatus.toSimple(), + noAccountStatus.toSimple(), + verifiedStatus.toSimple(), + ) + + Truth.assertThat(actual).isEqualTo(expected) + } + + private companion object { + + val userWalletId = UserWalletId(stringValue = "011") + } +} \ No newline at end of file diff --git a/data/networks/src/test/java/com/tangem/data/networks/store/NetworksStatusesStoreInitializationTest.kt b/data/networks/src/test/java/com/tangem/data/networks/store/InitializationTest.kt similarity index 98% rename from data/networks/src/test/java/com/tangem/data/networks/store/NetworksStatusesStoreInitializationTest.kt rename to data/networks/src/test/java/com/tangem/data/networks/store/InitializationTest.kt index 94d819b827..8f17de78e9 100644 --- a/data/networks/src/test/java/com/tangem/data/networks/store/NetworksStatusesStoreInitializationTest.kt +++ b/data/networks/src/test/java/com/tangem/data/networks/store/InitializationTest.kt @@ -20,7 +20,7 @@ import org.junit.Test /** [REDACTED_AUTHOR] */ -internal class NetworksStatusesStoreInitializationTest { +internal class InitializationTest { @Test fun `test initialization if cache store is empty`() = runTest { diff --git a/data/networks/src/test/java/com/tangem/data/networks/store/NetworkStatusesStoreUpdateMethodsTest.kt b/data/networks/src/test/java/com/tangem/data/networks/store/NetworkStatusesStoreUpdateMethodsTest.kt deleted file mode 100644 index d1f2080794..0000000000 --- a/data/networks/src/test/java/com/tangem/data/networks/store/NetworkStatusesStoreUpdateMethodsTest.kt +++ /dev/null @@ -1,336 +0,0 @@ -package com.tangem.data.networks.store - -import com.google.common.truth.Truth -import com.tangem.common.test.datastore.MockStateDataStore -import com.tangem.common.test.domain.network.MockNetworkStatusFactory -import com.tangem.common.test.domain.token.MockCryptoCurrencyFactory -import com.tangem.data.networks.models.SimpleNetworkStatus -import com.tangem.data.networks.toDataModel -import com.tangem.data.networks.toSimple -import com.tangem.datasource.local.datastore.RuntimeSharedStore -import com.tangem.datasource.local.network.entity.NetworkStatusDM -import com.tangem.domain.models.StatusSource -import com.tangem.domain.tokens.model.NetworkStatus -import com.tangem.domain.wallets.models.UserWalletId -import com.tangem.utils.coroutines.TestingCoroutineDispatcherProvider -import kotlinx.coroutines.flow.firstOrNull -import kotlinx.coroutines.test.runTest -import org.junit.Test - -/** -[REDACTED_AUTHOR] - */ -internal class NetworkStatusesStoreUpdateMethodsTest { - - private val runtimeStore = RuntimeSharedStore() - private val persistenceStore = MockStateDataStore(default = emptyMap()) - - private val store = DefaultNetworksStatusesStoreV2( - runtimeStore = runtimeStore, - persistenceDataStore = persistenceStore, - dispatchers = TestingCoroutineDispatcherProvider(), - ) - - @Test - fun `refresh the single network if runtime store is empty`() = runTest { - store.refresh(userWalletId = userWalletId, network = network) - - val runtimeExpected = mapOf(userWalletId.stringValue to emptySet()) - - Truth.assertThat(runtimeStore.getSyncOrNull()).isEqualTo(runtimeExpected) - Truth.assertThat(persistenceStore.data.firstOrNull()).isEqualTo(emptyMap>()) - } - - @Test - fun `refresh the single network if runtime store contains status with this network`() = runTest { - val status = MockNetworkStatusFactory.createVerified(network).toSimple() - - runtimeStore.store( - value = mapOf(userWalletId.stringValue to setOf(status)), - ) - - store.refresh(userWalletId = userWalletId, network = network) - - val runtimeExpected = mapOf( - userWalletId.stringValue to setOf( - status.copy(value = status.value.copySealed(source = StatusSource.CACHE)), - ), - ) - - Truth.assertThat(runtimeStore.getSyncOrNull()).isEqualTo(runtimeExpected) - Truth.assertThat(persistenceStore.data.firstOrNull()).isEqualTo(emptyMap>()) - } - - @Test - fun `refresh the multi networks if runtime store is empty`() = runTest { - store.refresh(userWalletId = userWalletId, networks = networks) - - val runtimeExpected = mapOf(userWalletId.stringValue to emptySet()) - - Truth.assertThat(runtimeStore.getSyncOrNull()).isEqualTo(runtimeExpected) - Truth.assertThat(persistenceStore.data.firstOrNull()).isEqualTo(emptyMap>()) - } - - @Test - fun `refresh the multi networks if runtime store contains status with this network`() = runTest { - val firstStatus = MockNetworkStatusFactory.createVerified(network = networks.first()).toSimple() - val secondStatus = MockNetworkStatusFactory.createVerified(network = networks.last()).toSimple() - - runtimeStore.store( - value = mapOf(userWalletId.stringValue to setOf(firstStatus, secondStatus)), - ) - - store.refresh(userWalletId = userWalletId, networks = networks) - - val runtimeExpected = mapOf( - userWalletId.stringValue to setOf( - firstStatus.copy(value = firstStatus.value.copySealed(source = StatusSource.CACHE)), - secondStatus.copy(value = secondStatus.value.copySealed(source = StatusSource.CACHE)), - ), - ) - - Truth.assertThat(runtimeStore.getSyncOrNull()).isEqualTo(runtimeExpected) - Truth.assertThat(persistenceStore.data.firstOrNull()).isEqualTo(emptyMap>()) - } - - @Test - fun `store actual with any status sources`() = runTest { - val expectedErrorMessage = "Method storeActual can be called only with StatusSource.ACTUAL" - - // #1: StatusSource.ACTUAL - val status = MockNetworkStatusFactory.createVerified(network).copy( - value = MockNetworkStatusFactory.createVerified(network).value.copySealed(source = StatusSource.ACTUAL), - ) - - val actual = runCatching { store.storeSuccess(userWalletId, status) } - - val runtimeExpected = mapOf(userWalletId.stringValue to setOf(status.toSimple())) - val persistenceExpected = mapOf(userWalletId.stringValue to setOf(status.toDataModel()!!)) - - Truth.assertThat(actual.isSuccess).isTrue() - Truth.assertThat(runtimeStore.getSyncOrNull()).isEqualTo(runtimeExpected) - Truth.assertThat(persistenceStore.data.firstOrNull()).isEqualTo(persistenceExpected) - - // #2: StatusSource.CACHE - val cacheStatus = MockNetworkStatusFactory.createVerified(network).copy( - value = MockNetworkStatusFactory.createVerified(network).value.copySealed(source = StatusSource.CACHE), - ) - - val cacheActual = runCatching { store.storeSuccess(userWalletId, cacheStatus) } - - Truth.assertThat(cacheActual.isFailure).isTrue() - Truth.assertThat(cacheActual.exceptionOrNull()).isInstanceOf(IllegalStateException::class.java) - Truth.assertThat(cacheActual.exceptionOrNull()).hasMessageThat().isEqualTo(expectedErrorMessage) - Truth.assertThat(runtimeStore.getSyncOrNull()).isEqualTo(runtimeExpected) - Truth.assertThat(persistenceStore.data.firstOrNull()).isEqualTo(persistenceExpected) - - // #3: StatusSource.ONLY_CACHE - val onlyCacheStatus = MockNetworkStatusFactory.createVerified(network).copy( - value = MockNetworkStatusFactory.createVerified(network).value.copySealed(source = StatusSource.ONLY_CACHE), - ) - - val onlyCacheActual = runCatching { store.storeSuccess(userWalletId, onlyCacheStatus) } - - Truth.assertThat(onlyCacheActual.isFailure).isTrue() - Truth.assertThat(onlyCacheActual.exceptionOrNull()).isInstanceOf(IllegalStateException::class.java) - Truth.assertThat(onlyCacheActual.exceptionOrNull()).hasMessageThat().isEqualTo(expectedErrorMessage) - Truth.assertThat(runtimeStore.getSyncOrNull()).isEqualTo(runtimeExpected) - Truth.assertThat(persistenceStore.data.firstOrNull()).isEqualTo(persistenceExpected) - } - - @Test - fun `store actual if runtime and cache stores contain status with this network`() = runTest { - val prevStatus = MockNetworkStatusFactory.createVerified(network).copy( - value = MockNetworkStatusFactory.createVerified(network).value.copySealed(source = StatusSource.ONLY_CACHE), - ) - - runtimeStore.store( - value = mapOf(userWalletId.stringValue to setOf(prevStatus.toSimple())), - ) - - persistenceStore.updateData { - it.toMutableMap().apply { - put(userWalletId.stringValue, setOf(prevStatus.toDataModel()!!)) - } - } - - val newStatus = MockNetworkStatusFactory.createVerified(network).copy( - value = MockNetworkStatusFactory.createVerified(network).value.copySealed(source = StatusSource.ACTUAL), - ) - - store.storeSuccess(userWalletId, newStatus) - - val runtimeExpected = mapOf( - userWalletId.stringValue to setOf(newStatus.toSimple()), - ) - - val persistenceExpected = mapOf( - userWalletId.stringValue to setOf(newStatus.toDataModel()!!), - ) - - Truth.assertThat(runtimeStore.getSyncOrNull()).isEqualTo(runtimeExpected) - Truth.assertThat(persistenceStore.data.firstOrNull()).isEqualTo(persistenceExpected) - } - - @Test - fun `store actual unreachable status if cache store contains verified status`() = runTest { - val prevStatus = MockNetworkStatusFactory.createVerified(network) - - runtimeStore.store( - value = mapOf(userWalletId.stringValue to setOf(prevStatus.toSimple())), - ) - - persistenceStore.updateData { - it.toMutableMap().apply { - put(userWalletId.stringValue, setOf(prevStatus.toDataModel()!!)) - } - } - - val newStatus = MockNetworkStatusFactory.createUnreachable(network) - - val actual = runCatching { store.storeSuccess(userWalletId, newStatus) } - - val expectedErrorMessage = "Use storeError method to save unreachable status" - val runtimeExpected = mapOf(userWalletId.stringValue to setOf(prevStatus.toSimple())) - val persistenceExpected = mapOf(userWalletId.stringValue to setOf(prevStatus.toDataModel()!!)) - - Truth.assertThat(actual.isFailure).isTrue() - Truth.assertThat(actual.exceptionOrNull()).isInstanceOf(IllegalStateException::class.java) - Truth.assertThat(actual.exceptionOrNull()).hasMessageThat().isEqualTo(expectedErrorMessage) - Truth.assertThat(runtimeStore.getSyncOrNull()).isEqualTo(runtimeExpected) - Truth.assertThat(persistenceStore.data.firstOrNull()).isEqualTo(persistenceExpected) - } - - @Test - fun `store error if runtime store is empty`() = runTest { - store.storeError(userWalletId = userWalletId, network = network) - - val runtimeExpected = mapOf( - userWalletId.stringValue to setOf( - SimpleNetworkStatus( - id = SimpleNetworkStatus.Id(network), - value = NetworkStatus.Unreachable(address = null), - ), - ), - ) - - Truth.assertThat(runtimeStore.getSyncOrNull()).isEqualTo(runtimeExpected) - Truth.assertThat(persistenceStore.data.firstOrNull()).isEqualTo(emptyMap>()) - } - - @Test - fun `store error if runtime store contains status with this network`() = runTest { - val status = MockNetworkStatusFactory.createVerified(network).toSimple() - - runtimeStore.store( - value = mapOf(userWalletId.stringValue to setOf(status)), - ) - - store.storeError(userWalletId = userWalletId, network = network) - - val runtimeExpected = mapOf( - userWalletId.stringValue to setOf( - status.copy(value = status.value.copySealed(source = StatusSource.ONLY_CACHE)), - ), - ) - - Truth.assertThat(runtimeStore.getSyncOrNull()).isEqualTo(runtimeExpected) - Truth.assertThat(persistenceStore.data.firstOrNull()).isEqualTo(emptyMap>()) - } - - @Test - fun `store error if runtime store is empty and unreachable status was passed`() = runTest { - val status = MockNetworkStatusFactory.createUnreachable(network) - - store.storeError( - userWalletId = userWalletId, - network = network, - value = status.value as NetworkStatus.Unreachable, - ) - - val runtimeExpected = mapOf( - userWalletId.stringValue to setOf(status.toSimple()), - ) - - Truth.assertThat(runtimeStore.getSyncOrNull()).isEqualTo(runtimeExpected) - Truth.assertThat(persistenceStore.data.firstOrNull()).isEqualTo(emptyMap>()) - } - - @Test - fun `store error if runtime store contains status with this network and unreachable status was passed`() = runTest { - val status = MockNetworkStatusFactory.createVerified(network) - - runtimeStore.store( - value = mapOf(userWalletId.stringValue to setOf(status.toSimple())), - ) - - store.storeError( - userWalletId = userWalletId, - network = network, - value = MockNetworkStatusFactory.createUnreachable(network).value as NetworkStatus.Unreachable, - ) - - val runtimeExpected = mapOf( - userWalletId.stringValue to setOf( - status.copy(value = status.value.copySealed(source = StatusSource.ONLY_CACHE)).toSimple(), - ), - ) - - Truth.assertThat(runtimeStore.getSyncOrNull()).isEqualTo(runtimeExpected) - Truth.assertThat(persistenceStore.data.firstOrNull()).isEqualTo(emptyMap>()) - } - - @Test - fun `store error for networks if runtime store is empty`() = runTest { - store.storeError(userWalletId = userWalletId, networks = networks) - - val runtimeExpected = mapOf( - userWalletId.stringValue to setOf( - SimpleNetworkStatus( - id = SimpleNetworkStatus.Id(networks.first()), - value = NetworkStatus.Unreachable(address = null), - ), - SimpleNetworkStatus( - id = SimpleNetworkStatus.Id(networks.last()), - value = NetworkStatus.Unreachable(address = null), - ), - ), - ) - - Truth.assertThat(runtimeStore.getSyncOrNull()).isEqualTo(runtimeExpected) - Truth.assertThat(persistenceStore.data.firstOrNull()).isEqualTo(emptyMap>()) - } - - @Test - fun `store error for networks if runtime store contains status with this network`() = runTest { - val status = MockNetworkStatusFactory.createVerified(networks.first()).toSimple() - - runtimeStore.store( - value = mapOf(userWalletId.stringValue to setOf(status)), - ) - - store.storeError(userWalletId = userWalletId, networks = networks) - - val runtimeExpected = mapOf( - userWalletId.stringValue to setOf( - status.copy(value = status.value.copySealed(source = StatusSource.ONLY_CACHE)), - SimpleNetworkStatus( - id = SimpleNetworkStatus.Id(networks.last()), - value = NetworkStatus.Unreachable(address = null), - ), - ), - ) - - Truth.assertThat(runtimeStore.getSyncOrNull()).isEqualTo(runtimeExpected) - Truth.assertThat(persistenceStore.data.firstOrNull()).isEqualTo(emptyMap>()) - } - - private companion object { - - val userWalletId = UserWalletId(stringValue = "011") - - val network = MockCryptoCurrencyFactory().ethereum.network - - val networks = MockCryptoCurrencyFactory().ethereumAndStellar.mapTo(hashSetOf()) { it.network } - } -} \ No newline at end of file diff --git a/data/networks/src/test/java/com/tangem/data/networks/store/NetworksStatusesStoreGetMethodTest.kt b/data/networks/src/test/java/com/tangem/data/networks/store/NetworksStatusesStoreGetMethodTest.kt deleted file mode 100644 index 522fcbeacc..0000000000 --- a/data/networks/src/test/java/com/tangem/data/networks/store/NetworksStatusesStoreGetMethodTest.kt +++ /dev/null @@ -1,83 +0,0 @@ -package com.tangem.data.networks.store - -import com.google.common.truth.Truth -import com.tangem.common.test.datastore.MockStateDataStore -import com.tangem.common.test.domain.network.MockNetworkStatusFactory -import com.tangem.common.test.utils.getEmittedValues -import com.tangem.data.networks.models.SimpleNetworkStatus -import com.tangem.data.networks.toSimple -import com.tangem.datasource.local.datastore.RuntimeSharedStore -import com.tangem.domain.wallets.models.UserWalletId -import com.tangem.utils.coroutines.TestingCoroutineDispatcherProvider -import kotlinx.coroutines.test.runTest -import org.junit.Test - -/** -[REDACTED_AUTHOR] - */ -internal class NetworksStatusesStoreGetMethodTest { - - private val runtimeStore = RuntimeSharedStore() - private val persistenceStore = MockStateDataStore(default = emptyMap()) - - private val store = DefaultNetworksStatusesStoreV2( - runtimeStore = runtimeStore, - persistenceDataStore = persistenceStore, - dispatchers = TestingCoroutineDispatcherProvider(), - ) - - @Test - fun `test get if runtime store is empty`() = runTest { - val actual = store.get(userWalletId = userWalletId) - - val values = getEmittedValues(flow = actual) - - Truth.assertThat(values).isEqualTo(emptyList>()) - } - - @Test - fun `test get if runtime store contains empty map`() = runTest { - runtimeStore.store(value = emptyMap()) - - val actual = store.get(userWalletId = userWalletId) - - val values = getEmittedValues(flow = actual) - - Truth.assertThat(values).isEqualTo(emptyList>()) - } - - @Test - fun `test get if runtime store contains portfolio with empty statuses`() = runTest { - runtimeStore.store( - value = mapOf(userWalletId.stringValue to emptySet()), - ) - - val actual = store.get(userWalletId = userWalletId) - - val values = getEmittedValues(flow = actual) - - Truth.assertThat(values.size).isEqualTo(1) - Truth.assertThat(values).isEqualTo(listOf(emptySet())) - } - - @Test - fun `test get if runtime store is not empty`() = runTest { - val status = MockNetworkStatusFactory.createVerified() - - runtimeStore.store( - value = mapOf(userWalletId.stringValue to setOf(status.toSimple())), - ) - - val actual = store.get(userWalletId = userWalletId) - - val values = getEmittedValues(flow = actual) - - Truth.assertThat(values.size).isEqualTo(1) - Truth.assertThat(values).isEqualTo(listOf(setOf(status.toSimple()))) - } - - private companion object { - - val userWalletId = UserWalletId(stringValue = "011") - } -} \ No newline at end of file diff --git a/data/networks/src/test/java/com/tangem/data/networks/store/ParameterizedStoreStatusTest.kt b/data/networks/src/test/java/com/tangem/data/networks/store/ParameterizedStoreStatusTest.kt new file mode 100644 index 0000000000..aca2beaf62 --- /dev/null +++ b/data/networks/src/test/java/com/tangem/data/networks/store/ParameterizedStoreStatusTest.kt @@ -0,0 +1,131 @@ +package com.tangem.data.networks.store + +import com.google.common.truth.Truth +import com.tangem.common.test.datastore.MockStateDataStore +import com.tangem.common.test.domain.network.MockNetworkStatusFactory +import com.tangem.data.networks.models.SimpleNetworkStatus +import com.tangem.data.networks.toDataModel +import com.tangem.data.networks.toSimple +import com.tangem.datasource.local.datastore.RuntimeSharedStore +import com.tangem.domain.models.StatusSource +import com.tangem.domain.tokens.model.NetworkStatus +import com.tangem.domain.wallets.models.UserWalletId +import com.tangem.utils.coroutines.TestingCoroutineDispatcherProvider +import kotlinx.coroutines.flow.firstOrNull +import kotlinx.coroutines.test.runTest +import org.junit.Test +import org.junit.runner.RunWith +import org.junit.runners.Parameterized + +/** +[REDACTED_AUTHOR] + */ +@RunWith(Parameterized::class) +internal class ParameterizedStoreStatusTest(private val model: Model) { + + private val runtimeStore = RuntimeSharedStore() + private val persistenceStore = MockStateDataStore(default = emptyMap()) + + private val store = DefaultNetworksStatusesStoreV2( + runtimeStore = runtimeStore, + persistenceDataStore = persistenceStore, + dispatchers = TestingCoroutineDispatcherProvider(), + ) + + @Test + fun `test store success`() = runTest { + val actual = runCatching { store.storeStatus(userWalletId = userWalletId, status = model.status) } + + Truth.assertThat(actual.isSuccess).isEqualTo(model.isSuccess) + Truth.assertThat(runtimeStore.getSyncOrNull()).isEqualTo(model.runtimeExpected) + Truth.assertThat(persistenceStore.data.firstOrNull()).isEqualTo(model.persistenceExpected) + } + + data class Model( + val status: NetworkStatus, + val isSuccess: Boolean, + val runtimeExpected: Map>, + val persistenceExpected: WalletIdWithStatusDM, + ) + + private companion object { + + val userWalletId = UserWalletId(stringValue = "011") + + @JvmStatic + @Parameterized.Parameters + fun data(): Collection { + return listOf( + // region any network statuses with StatusSource.ACTUAL + MockNetworkStatusFactory.createVerified(source = StatusSource.ACTUAL).let { status -> + Model( + status = status, + isSuccess = true, + runtimeExpected = userWalletIdWithSimple(status), + persistenceExpected = userWalletIdWithData(status), + ) + }, + MockNetworkStatusFactory.createNoAccount(source = StatusSource.ACTUAL).let { status -> + Model( + status = status, + isSuccess = true, + runtimeExpected = userWalletIdWithSimple(status), + persistenceExpected = userWalletIdWithData(status), + ) + }, + MockNetworkStatusFactory.createMissedDerivation().let { status -> + Model( + status = status, + isSuccess = true, + runtimeExpected = userWalletIdWithSimple(status), + persistenceExpected = emptyMap(), + ) + }, + MockNetworkStatusFactory.createUnreachable().let { status -> + Model( + status = status, + isSuccess = true, + runtimeExpected = userWalletIdWithSimple(status), + persistenceExpected = emptyMap(), + ) + }, + // endregion + + // region any status sources + Model( + status = MockNetworkStatusFactory.createVerified(source = StatusSource.CACHE), + isSuccess = false, + runtimeExpected = emptyMap(), + persistenceExpected = emptyMap(), + ), + Model( + status = MockNetworkStatusFactory.createNoAccount(source = StatusSource.CACHE), + isSuccess = false, + runtimeExpected = emptyMap(), + persistenceExpected = emptyMap(), + ), + Model( + status = MockNetworkStatusFactory.createVerified(source = StatusSource.ONLY_CACHE), + isSuccess = false, + runtimeExpected = emptyMap(), + persistenceExpected = emptyMap(), + ), + Model( + status = MockNetworkStatusFactory.createNoAccount(source = StatusSource.ONLY_CACHE), + isSuccess = false, + runtimeExpected = emptyMap(), + persistenceExpected = emptyMap(), + ), + // endregion + ) + } + + fun userWalletIdWithSimple(status: NetworkStatus) = mapOf( + userWalletId.stringValue to setOf(status.toSimple()), + ) + + fun userWalletIdWithData(status: NetworkStatus) = mapOf( + userWalletId.stringValue to setOf(status.toDataModel()!!), + ) + } +} \ No newline at end of file diff --git a/data/networks/src/test/java/com/tangem/data/networks/store/ParameterizedStoreSuccessTest.kt b/data/networks/src/test/java/com/tangem/data/networks/store/ParameterizedStoreSuccessTest.kt new file mode 100644 index 0000000000..f22b72e0d6 --- /dev/null +++ b/data/networks/src/test/java/com/tangem/data/networks/store/ParameterizedStoreSuccessTest.kt @@ -0,0 +1,129 @@ +package com.tangem.data.networks.store + +import com.google.common.truth.Truth +import com.tangem.common.test.datastore.MockStateDataStore +import com.tangem.common.test.domain.network.MockNetworkStatusFactory +import com.tangem.data.networks.models.SimpleNetworkStatus +import com.tangem.data.networks.toDataModel +import com.tangem.data.networks.toSimple +import com.tangem.datasource.local.datastore.RuntimeSharedStore +import com.tangem.domain.models.StatusSource +import com.tangem.domain.tokens.model.NetworkStatus +import com.tangem.domain.wallets.models.UserWalletId +import com.tangem.utils.coroutines.TestingCoroutineDispatcherProvider +import kotlinx.coroutines.flow.firstOrNull +import kotlinx.coroutines.test.runTest +import org.junit.Test +import org.junit.runner.RunWith +import org.junit.runners.Parameterized + +/** +[REDACTED_AUTHOR] + */ +@RunWith(Parameterized::class) +internal class ParameterizedStoreSuccessTest(private val model: Model) { + + private val runtimeStore = RuntimeSharedStore() + private val persistenceStore = MockStateDataStore(default = emptyMap()) + + private val store = DefaultNetworksStatusesStoreV2( + runtimeStore = runtimeStore, + persistenceDataStore = persistenceStore, + dispatchers = TestingCoroutineDispatcherProvider(), + ) + + @Test + fun `test store success`() = runTest { + val actual = runCatching { store.storeSuccess(userWalletId = userWalletId, status = model.status) } + + Truth.assertThat(actual.isSuccess).isEqualTo(model.isSuccess) + Truth.assertThat(runtimeStore.getSyncOrNull()).isEqualTo(model.runtimeExpected) + Truth.assertThat(persistenceStore.data.firstOrNull()).isEqualTo(model.persistenceExpected) + } + + data class Model( + val status: NetworkStatus, + val isSuccess: Boolean, + val runtimeExpected: Map>, + val persistenceExpected: WalletIdWithStatusDM, + ) + + private companion object { + + val userWalletId = UserWalletId(stringValue = "011") + + @JvmStatic + @Parameterized.Parameters + fun data(): Collection { + return listOf( + // region any network statuses with StatusSource.ACTUAL + MockNetworkStatusFactory.createVerified(source = StatusSource.ACTUAL).let { status -> + Model( + status = status, + isSuccess = true, + runtimeExpected = userWalletIdWithSimple(status), + persistenceExpected = userWalletIdWithData(status), + ) + }, + MockNetworkStatusFactory.createNoAccount(source = StatusSource.ACTUAL).let { status -> + Model( + status = status, + isSuccess = true, + runtimeExpected = userWalletIdWithSimple(status), + persistenceExpected = userWalletIdWithData(status), + ) + }, + MockNetworkStatusFactory.createMissedDerivation().let { status -> + Model( + status = status, + isSuccess = true, + runtimeExpected = userWalletIdWithSimple(status), + persistenceExpected = emptyMap(), + ) + }, + Model( + status = MockNetworkStatusFactory.createUnreachable(), + isSuccess = false, + runtimeExpected = emptyMap(), + persistenceExpected = emptyMap(), + ), + // endregion + + // region any status sources + Model( + status = MockNetworkStatusFactory.createVerified(source = StatusSource.CACHE), + isSuccess = false, + runtimeExpected = emptyMap(), + persistenceExpected = emptyMap(), + ), + Model( + status = MockNetworkStatusFactory.createNoAccount(source = StatusSource.CACHE), + isSuccess = false, + runtimeExpected = emptyMap(), + persistenceExpected = emptyMap(), + ), + Model( + status = MockNetworkStatusFactory.createVerified(source = StatusSource.ONLY_CACHE), + isSuccess = false, + runtimeExpected = emptyMap(), + persistenceExpected = emptyMap(), + ), + Model( + status = MockNetworkStatusFactory.createNoAccount(source = StatusSource.ONLY_CACHE), + isSuccess = false, + runtimeExpected = emptyMap(), + persistenceExpected = emptyMap(), + ), + // endregion + ) + } + + fun userWalletIdWithSimple(status: NetworkStatus) = mapOf( + userWalletId.stringValue to setOf(status.toSimple()), + ) + + fun userWalletIdWithData(status: NetworkStatus) = mapOf( + userWalletId.stringValue to setOf(status.toDataModel()!!), + ) + } +} \ No newline at end of file diff --git a/data/networks/src/test/java/com/tangem/data/networks/store/ParameterizedStoreTest.kt b/data/networks/src/test/java/com/tangem/data/networks/store/ParameterizedStoreTest.kt new file mode 100644 index 0000000000..bc68472740 --- /dev/null +++ b/data/networks/src/test/java/com/tangem/data/networks/store/ParameterizedStoreTest.kt @@ -0,0 +1,95 @@ +package com.tangem.data.networks.store + +import com.google.common.truth.Truth +import com.tangem.common.test.datastore.MockStateDataStore +import com.tangem.common.test.domain.network.MockNetworkStatusFactory +import com.tangem.data.networks.models.SimpleNetworkStatus +import com.tangem.data.networks.toDataModel +import com.tangem.data.networks.toSimple +import com.tangem.datasource.local.datastore.RuntimeSharedStore +import com.tangem.domain.tokens.model.NetworkStatus +import com.tangem.domain.wallets.models.UserWalletId +import com.tangem.utils.coroutines.TestingCoroutineDispatcherProvider +import kotlinx.coroutines.flow.firstOrNull +import kotlinx.coroutines.test.runTest +import org.junit.Test +import org.junit.runner.RunWith +import org.junit.runners.Parameterized + +/** +[REDACTED_AUTHOR] + */ +@RunWith(Parameterized::class) +internal class ParameterizedStoreTest(private val model: Model) { + + private val runtimeStore = RuntimeSharedStore() + private val persistenceStore = MockStateDataStore(default = emptyMap()) + + private val store = DefaultNetworksStatusesStoreV2( + runtimeStore = runtimeStore, + persistenceDataStore = persistenceStore, + dispatchers = TestingCoroutineDispatcherProvider(), + ) + + @Test + fun `test store method`() = runTest { + store.store(userWalletId = userWalletId, status = model.status) + + Truth.assertThat(runtimeStore.getSyncOrNull()).isEqualTo(model.runtimeExpected) + Truth.assertThat(persistenceStore.data.firstOrNull()).isEqualTo(model.persistenceExpected) + } + + data class Model( + val status: NetworkStatus, + val runtimeExpected: Map>, + val persistenceExpected: WalletIdWithStatusDM, + ) + + private companion object { + + val userWalletId = UserWalletId(stringValue = "011") + + @JvmStatic + @Parameterized.Parameters + fun data(): Collection { + return listOf( + MockNetworkStatusFactory.createVerified().let { status -> + Model( + status = status, + runtimeExpected = userWalletIdWithSimple(status), + persistenceExpected = userWalletIdWithData(status), + ) + }, + MockNetworkStatusFactory.createNoAccount().let { status -> + Model( + status = status, + runtimeExpected = userWalletIdWithSimple(status), + persistenceExpected = userWalletIdWithData(status), + ) + }, + MockNetworkStatusFactory.createMissedDerivation().let { status -> + Model( + status = status, + runtimeExpected = userWalletIdWithSimple(status), + persistenceExpected = mapOf(), + ) + }, + MockNetworkStatusFactory.createUnreachable().let { status -> + Model( + status = status, + runtimeExpected = userWalletIdWithSimple(status), + persistenceExpected = mapOf(), + ) + }, + ) + } + + fun userWalletIdWithSimple(status: NetworkStatus) = mapOf( + userWalletId.stringValue to setOf(status.toSimple()), + ) + + fun userWalletIdWithData(status: NetworkStatus) = mapOf( + userWalletId.stringValue to setOf(status.toDataModel()!!), + ) + } +} \ No newline at end of file diff --git a/data/networks/src/test/java/com/tangem/data/networks/store/SetSourceAsCacheTest.kt b/data/networks/src/test/java/com/tangem/data/networks/store/SetSourceAsCacheTest.kt new file mode 100644 index 0000000000..3043939d1e --- /dev/null +++ b/data/networks/src/test/java/com/tangem/data/networks/store/SetSourceAsCacheTest.kt @@ -0,0 +1,154 @@ +package com.tangem.data.networks.store + +import com.google.common.truth.Truth +import com.tangem.common.test.datastore.MockStateDataStore +import com.tangem.common.test.domain.network.MockNetworkStatusFactory +import com.tangem.common.test.domain.token.MockCryptoCurrencyFactory +import com.tangem.data.networks.toSimple +import com.tangem.datasource.local.datastore.RuntimeSharedStore +import com.tangem.datasource.local.network.entity.NetworkStatusDM +import com.tangem.domain.models.StatusSource +import com.tangem.domain.tokens.model.NetworkStatus +import com.tangem.domain.wallets.models.UserWalletId +import com.tangem.utils.coroutines.TestingCoroutineDispatcherProvider +import kotlinx.coroutines.flow.firstOrNull +import kotlinx.coroutines.test.runTest +import org.junit.Test + +/** +[REDACTED_AUTHOR] + */ +internal class SetSourceAsCacheTest { + + private val runtimeStore = RuntimeSharedStore() + private val persistenceStore = MockStateDataStore(default = emptyMap()) + + private val store = DefaultNetworksStatusesStoreV2( + runtimeStore = runtimeStore, + persistenceDataStore = persistenceStore, + dispatchers = TestingCoroutineDispatcherProvider(), + ) + + @Test + fun `setSourceAsCache the single network if runtime store is empty`() = runTest { + store.setSourceAsCache(userWalletId = userWalletId, network = network) + + val runtimeExpected = mapOf(userWalletId.stringValue to emptySet()) + + Truth.assertThat(runtimeStore.getSyncOrNull()).isEqualTo(runtimeExpected) + Truth.assertThat(persistenceStore.data.firstOrNull()).isEqualTo(emptyMap>()) + } + + @Test + fun `setSourceAsCache the single network if runtime store contains empty map`() = runTest { + runtimeStore.store(value = emptyMap()) + + store.setSourceAsCache(userWalletId = userWalletId, network = network) + + val runtimeExpected = mapOf(userWalletId.stringValue to emptySet()) + + Truth.assertThat(runtimeStore.getSyncOrNull()).isEqualTo(runtimeExpected) + Truth.assertThat(persistenceStore.data.firstOrNull()).isEqualTo(emptyMap>()) + } + + @Test + fun `setSourceAsCache the single network if runtime store contains portfolio with empty statuses`() = runTest { + runtimeStore.store( + value = mapOf(userWalletId.stringValue to emptySet()), + ) + + store.setSourceAsCache(userWalletId = userWalletId, network = network) + + val runtimeExpected = mapOf(userWalletId.stringValue to emptySet()) + + Truth.assertThat(runtimeStore.getSyncOrNull()).isEqualTo(runtimeExpected) + Truth.assertThat(persistenceStore.data.firstOrNull()).isEqualTo(emptyMap>()) + } + + @Test + fun `setSourceAsCache the single network if runtime store contains status with this network`() = runTest { + val status = MockNetworkStatusFactory.createVerified(network).toSimple() + + runtimeStore.store( + value = mapOf(userWalletId.stringValue to setOf(status)), + ) + + store.setSourceAsCache(userWalletId = userWalletId, network = network) + + val runtimeExpected = mapOf( + userWalletId.stringValue to setOf( + status.copy(value = status.value.copySealed(source = StatusSource.CACHE)), + ), + ) + + Truth.assertThat(runtimeStore.getSyncOrNull()).isEqualTo(runtimeExpected) + Truth.assertThat(persistenceStore.data.firstOrNull()).isEqualTo(emptyMap>()) + } + + @Test + fun `setSourceAsCache the multi networks if runtime store is empty`() = runTest { + store.setSourceAsCache(userWalletId = userWalletId, networks = networks) + + val runtimeExpected = mapOf(userWalletId.stringValue to emptySet()) + + Truth.assertThat(runtimeStore.getSyncOrNull()).isEqualTo(runtimeExpected) + Truth.assertThat(persistenceStore.data.firstOrNull()).isEqualTo(emptyMap>()) + } + + @Test + fun `setSourceAsCache the multi networks if runtime store contains empty map`() = runTest { + runtimeStore.store(value = emptyMap()) + + store.setSourceAsCache(userWalletId = userWalletId, networks = networks) + + val runtimeExpected = mapOf(userWalletId.stringValue to emptySet()) + + Truth.assertThat(runtimeStore.getSyncOrNull()).isEqualTo(runtimeExpected) + Truth.assertThat(persistenceStore.data.firstOrNull()).isEqualTo(emptyMap>()) + } + + @Test + fun `setSourceAsCache the multi networks if runtime store contains portfolio with empty statuses`() = runTest { + runtimeStore.store( + value = mapOf(userWalletId.stringValue to emptySet()), + ) + + store.setSourceAsCache(userWalletId = userWalletId, networks = networks) + + val runtimeExpected = mapOf(userWalletId.stringValue to emptySet()) + + Truth.assertThat(runtimeStore.getSyncOrNull()).isEqualTo(runtimeExpected) + Truth.assertThat(persistenceStore.data.firstOrNull()).isEqualTo(emptyMap>()) + } + + @Test + fun `setSourceAsCache the multi networks if runtime store contains status with this network`() = runTest { + val firstStatus = MockNetworkStatusFactory.createVerified(network = networks.first()).toSimple() + val secondStatus = MockNetworkStatusFactory.createVerified(network = networks.last()).toSimple() + + runtimeStore.store( + value = mapOf(userWalletId.stringValue to setOf(firstStatus, secondStatus)), + ) + + store.setSourceAsCache(userWalletId = userWalletId, networks = networks) + + val runtimeExpected = mapOf( + userWalletId.stringValue to setOf( + firstStatus.copy(value = firstStatus.value.copySealed(source = StatusSource.CACHE)), + secondStatus.copy(value = secondStatus.value.copySealed(source = StatusSource.CACHE)), + ), + ) + + Truth.assertThat(runtimeStore.getSyncOrNull()).isEqualTo(runtimeExpected) + Truth.assertThat(persistenceStore.data.firstOrNull()).isEqualTo(emptyMap>()) + } + + private companion object { + + val userWalletId = UserWalletId(stringValue = "011") + + val network = MockCryptoCurrencyFactory().ethereum.network + + val networks = MockCryptoCurrencyFactory().ethereumAndStellar.mapTo(hashSetOf()) { it.network } + } +} \ No newline at end of file diff --git a/data/networks/src/test/java/com/tangem/data/networks/store/SetSourceAsOnlyCacheTest.kt b/data/networks/src/test/java/com/tangem/data/networks/store/SetSourceAsOnlyCacheTest.kt new file mode 100644 index 0000000000..6831b3d8be --- /dev/null +++ b/data/networks/src/test/java/com/tangem/data/networks/store/SetSourceAsOnlyCacheTest.kt @@ -0,0 +1,214 @@ +package com.tangem.data.networks.store + +import com.google.common.truth.Truth +import com.tangem.common.test.datastore.MockStateDataStore +import com.tangem.common.test.domain.network.MockNetworkStatusFactory +import com.tangem.common.test.domain.token.MockCryptoCurrencyFactory +import com.tangem.data.networks.toSimple +import com.tangem.datasource.local.datastore.RuntimeSharedStore +import com.tangem.datasource.local.network.entity.NetworkStatusDM +import com.tangem.domain.models.StatusSource +import com.tangem.domain.tokens.model.NetworkStatus +import com.tangem.domain.wallets.models.UserWalletId +import com.tangem.utils.coroutines.TestingCoroutineDispatcherProvider +import kotlinx.coroutines.flow.firstOrNull +import kotlinx.coroutines.test.runTest +import org.junit.Test + +/** +[REDACTED_AUTHOR] + */ +internal class SetSourceAsOnlyCacheTest { + + private val runtimeStore = RuntimeSharedStore() + private val persistenceStore = MockStateDataStore(default = emptyMap()) + + private val store = DefaultNetworksStatusesStoreV2( + runtimeStore = runtimeStore, + persistenceDataStore = persistenceStore, + dispatchers = TestingCoroutineDispatcherProvider(), + ) + + @Test + fun `setSourceAsOnlyCache if runtime store is empty`() = runTest { + store.setSourceAsOnlyCache(userWalletId = userWalletId, network = network) + + val status = MockNetworkStatusFactory.createUnreachable() + val runtimeExpected = mapOf(userWalletId.stringValue to setOf(status.toSimple())) + + Truth.assertThat(runtimeStore.getSyncOrNull()).isEqualTo(runtimeExpected) + Truth.assertThat(persistenceStore.data.firstOrNull()).isEqualTo(emptyMap>()) + } + + @Test + fun `setSourceAsOnlyCache with value if runtime store is empty`() = runTest { + val status = MockNetworkStatusFactory.createUnreachable() + + store.setSourceAsOnlyCache( + userWalletId = userWalletId, + network = network, + value = status.value as NetworkStatus.Unreachable, + ) + + val runtimeExpected = mapOf(userWalletId.stringValue to setOf(status.toSimple())) + + Truth.assertThat(runtimeStore.getSyncOrNull()).isEqualTo(runtimeExpected) + Truth.assertThat(persistenceStore.data.firstOrNull()).isEqualTo(emptyMap>()) + } + + @Test + fun `setSourceAsOnlyCache if runtime store contains empty map`() = runTest { + runtimeStore.store(value = emptyMap()) + + store.setSourceAsOnlyCache(userWalletId = userWalletId, network = network) + + val status = MockNetworkStatusFactory.createUnreachable() + val runtimeExpected = mapOf(userWalletId.stringValue to setOf(status.toSimple())) + + Truth.assertThat(runtimeStore.getSyncOrNull()).isEqualTo(runtimeExpected) + Truth.assertThat(persistenceStore.data.firstOrNull()).isEqualTo(emptyMap>()) + } + + @Test + fun `setSourceAsOnlyCache with value if runtime store contains empty map`() = runTest { + val status = MockNetworkStatusFactory.createUnreachable() + + runtimeStore.store(value = emptyMap()) + + store.setSourceAsOnlyCache( + userWalletId = userWalletId, + network = network, + value = status.value as NetworkStatus.Unreachable, + ) + + val runtimeExpected = mapOf(userWalletId.stringValue to setOf(status.toSimple())) + + Truth.assertThat(runtimeStore.getSyncOrNull()).isEqualTo(runtimeExpected) + Truth.assertThat(persistenceStore.data.firstOrNull()).isEqualTo(emptyMap>()) + } + + @Test + fun `setSourceAsOnlyCache if runtime store contains portfolio with empty statuses`() = runTest { + runtimeStore.store( + value = mapOf(userWalletId.stringValue to emptySet()), + ) + + store.setSourceAsOnlyCache(userWalletId = userWalletId, network = network) + + val status = MockNetworkStatusFactory.createUnreachable() + val runtimeExpected = mapOf(userWalletId.stringValue to setOf(status.toSimple())) + + Truth.assertThat(runtimeStore.getSyncOrNull()).isEqualTo(runtimeExpected) + Truth.assertThat(persistenceStore.data.firstOrNull()).isEqualTo(emptyMap>()) + } + + @Test + fun `setSourceAsOnlyCache with value if runtime store contains portfolio with empty statuses`() = runTest { + val status = MockNetworkStatusFactory.createUnreachable() + + runtimeStore.store( + value = mapOf(userWalletId.stringValue to emptySet()), + ) + + store.setSourceAsOnlyCache( + userWalletId = userWalletId, + network = network, + value = status.value as NetworkStatus.Unreachable, + ) + + val runtimeExpected = mapOf(userWalletId.stringValue to setOf(status.toSimple())) + + Truth.assertThat(runtimeStore.getSyncOrNull()).isEqualTo(runtimeExpected) + Truth.assertThat(persistenceStore.data.firstOrNull()).isEqualTo(emptyMap>()) + } + + @Test + fun `setSourceAsOnlyCache the single network if runtime store contains status with this network`() = runTest { + val status = MockNetworkStatusFactory.createVerified(network).toSimple() + + runtimeStore.store( + value = mapOf(userWalletId.stringValue to setOf(status)), + ) + + store.setSourceAsOnlyCache(userWalletId = userWalletId, network = network) + + val runtimeExpected = mapOf( + userWalletId.stringValue to setOf( + status.copy(value = status.value.copySealed(source = StatusSource.ONLY_CACHE)), + ), + ) + + Truth.assertThat(runtimeStore.getSyncOrNull()).isEqualTo(runtimeExpected) + Truth.assertThat(persistenceStore.data.firstOrNull()).isEqualTo(emptyMap>()) + } + + @Test + fun `setSourceAsOnlyCache the multi networks if runtime store is empty`() = runTest { + store.setSourceAsOnlyCache(userWalletId = userWalletId, networks = networks) + + val statuses = networks.map { MockNetworkStatusFactory.createUnreachable(it).toSimple() }.toSet() + val runtimeExpected = mapOf(userWalletId.stringValue to statuses) + + Truth.assertThat(runtimeStore.getSyncOrNull()).isEqualTo(runtimeExpected) + Truth.assertThat(persistenceStore.data.firstOrNull()).isEqualTo(emptyMap>()) + } + + @Test + fun `setSourceAsOnlyCache the multi networks if runtime store contains empty map`() = runTest { + runtimeStore.store(value = emptyMap()) + + store.setSourceAsOnlyCache(userWalletId = userWalletId, networks = networks) + + val statuses = networks.map { MockNetworkStatusFactory.createUnreachable(it).toSimple() }.toSet() + val runtimeExpected = mapOf(userWalletId.stringValue to statuses) + + Truth.assertThat(runtimeStore.getSyncOrNull()).isEqualTo(runtimeExpected) + Truth.assertThat(persistenceStore.data.firstOrNull()).isEqualTo(emptyMap>()) + } + + @Test + fun `setSourceAsOnlyCache the multi networks if runtime store contains portfolio with empty statuses`() = runTest { + runtimeStore.store( + value = mapOf(userWalletId.stringValue to emptySet()), + ) + + store.setSourceAsOnlyCache(userWalletId = userWalletId, networks = networks) + + val statuses = networks.map { MockNetworkStatusFactory.createUnreachable(it).toSimple() }.toSet() + val runtimeExpected = mapOf(userWalletId.stringValue to statuses) + + Truth.assertThat(runtimeStore.getSyncOrNull()).isEqualTo(runtimeExpected) + Truth.assertThat(persistenceStore.data.firstOrNull()).isEqualTo(emptyMap>()) + } + + @Test + fun `setSourceAsOnlyCache the multi networks if runtime store contains status with this network`() = runTest { + val firstStatus = MockNetworkStatusFactory.createVerified(network = networks.first()).toSimple() + val secondStatus = MockNetworkStatusFactory.createVerified(network = networks.last()).toSimple() + + runtimeStore.store( + value = mapOf(userWalletId.stringValue to setOf(firstStatus, secondStatus)), + ) + + store.setSourceAsOnlyCache(userWalletId = userWalletId, networks = networks) + + val runtimeExpected = mapOf( + userWalletId.stringValue to setOf( + firstStatus.copy(value = firstStatus.value.copySealed(source = StatusSource.ONLY_CACHE)), + secondStatus.copy(value = secondStatus.value.copySealed(source = StatusSource.ONLY_CACHE)), + ), + ) + + Truth.assertThat(runtimeStore.getSyncOrNull()).isEqualTo(runtimeExpected) + Truth.assertThat(persistenceStore.data.firstOrNull()).isEqualTo(emptyMap>()) + } + + private companion object { + + val userWalletId = UserWalletId(stringValue = "011") + + val network = MockCryptoCurrencyFactory().ethereum.network + + val networks = MockCryptoCurrencyFactory().ethereumAndStellar.mapTo(hashSetOf()) { it.network } + } +} \ No newline at end of file diff --git a/data/networks/src/test/java/com/tangem/data/networks/store/StoreActualNetworkStatusTest.kt b/data/networks/src/test/java/com/tangem/data/networks/store/StoreActualNetworkStatusTest.kt deleted file mode 100644 index 52e6f59b59..0000000000 --- a/data/networks/src/test/java/com/tangem/data/networks/store/StoreActualNetworkStatusTest.kt +++ /dev/null @@ -1,105 +0,0 @@ -package com.tangem.data.networks.store - -import com.google.common.truth.Truth -import com.tangem.common.test.datastore.MockStateDataStore -import com.tangem.common.test.domain.network.MockNetworkStatusFactory -import com.tangem.data.networks.models.SimpleNetworkStatus -import com.tangem.data.networks.toDataModel -import com.tangem.data.networks.toSimple -import com.tangem.datasource.local.datastore.RuntimeSharedStore -import com.tangem.domain.tokens.model.NetworkStatus -import com.tangem.domain.wallets.models.UserWalletId -import com.tangem.utils.coroutines.TestingCoroutineDispatcherProvider -import kotlinx.coroutines.flow.firstOrNull -import kotlinx.coroutines.test.runTest -import org.junit.Test -import org.junit.runner.RunWith -import org.junit.runners.Parameterized - -/** -[REDACTED_AUTHOR] - */ -@RunWith(Parameterized::class) -internal class StoreActualNetworkStatusTest(private val model: Model) { - - private val runtimeStore = RuntimeSharedStore() - private val persistenceStore = MockStateDataStore(default = emptyMap()) - - private val store = DefaultNetworksStatusesStoreV2( - runtimeStore = runtimeStore, - persistenceDataStore = persistenceStore, - dispatchers = TestingCoroutineDispatcherProvider(), - ) - - @Test - fun `store actual`() = runTest { - val actual = runCatching { store.storeSuccess(userWalletId = userWalletId, value = model.status) } - - Truth.assertThat(actual.isSuccess).isEqualTo(model.isSuccess) - Truth.assertThat(runtimeStore.getSyncOrNull()).isEqualTo(model.runtimeExpected) - Truth.assertThat(persistenceStore.data.firstOrNull()).isEqualTo(model.persistenceExpected) - } - - data class Model( - val status: NetworkStatus, - val isSuccess: Boolean, - val runtimeExpected: Map>, - val persistenceExpected: WalletIdWithStatusDM, - ) - - private companion object { - - val userWalletId = UserWalletId(stringValue = "011") - - @JvmStatic - @Parameterized.Parameters - fun data(): Collection { - return listOf( - Model( - status = MockNetworkStatusFactory.createVerified(), - isSuccess = true, - runtimeExpected = mapOf( - userWalletId.stringValue to setOf( - MockNetworkStatusFactory.createVerified().toSimple(), - ), - ), - persistenceExpected = mapOf( - userWalletId.stringValue to setOf( - MockNetworkStatusFactory.createVerified().toDataModel()!!, - ), - ), - ), - Model( - status = MockNetworkStatusFactory.createNoAccount(), - isSuccess = true, - runtimeExpected = mapOf( - userWalletId.stringValue to setOf( - MockNetworkStatusFactory.createNoAccount().toSimple(), - ), - ), - persistenceExpected = mapOf( - userWalletId.stringValue to setOf( - MockNetworkStatusFactory.createNoAccount().toDataModel()!!, - ), - ), - ), - Model( - status = MockNetworkStatusFactory.createMissedDerivation(), - isSuccess = true, - runtimeExpected = mapOf( - userWalletId.stringValue to setOf( - MockNetworkStatusFactory.createMissedDerivation().toSimple(), - ), - ), - persistenceExpected = mapOf(), - ), - Model( - status = MockNetworkStatusFactory.createUnreachable(), - isSuccess = false, - runtimeExpected = mapOf(), - persistenceExpected = mapOf(), - ), - ) - } - } -} \ No newline at end of file diff --git a/data/networks/src/test/java/com/tangem/data/networks/store/StoreStatusTest.kt b/data/networks/src/test/java/com/tangem/data/networks/store/StoreStatusTest.kt new file mode 100644 index 0000000000..5e19d72af9 --- /dev/null +++ b/data/networks/src/test/java/com/tangem/data/networks/store/StoreStatusTest.kt @@ -0,0 +1,247 @@ +package com.tangem.data.networks.store + +import com.google.common.truth.Truth +import com.tangem.common.test.datastore.MockStateDataStore +import com.tangem.common.test.domain.network.MockNetworkStatusFactory +import com.tangem.common.test.domain.token.MockCryptoCurrencyFactory +import com.tangem.data.networks.toDataModel +import com.tangem.data.networks.toSimple +import com.tangem.datasource.local.datastore.RuntimeSharedStore +import com.tangem.datasource.local.network.entity.NetworkStatusDM +import com.tangem.domain.models.StatusSource +import com.tangem.domain.wallets.models.UserWalletId +import com.tangem.utils.coroutines.TestingCoroutineDispatcherProvider +import kotlinx.coroutines.flow.firstOrNull +import kotlinx.coroutines.test.runTest +import org.junit.Test + +/** +[REDACTED_AUTHOR] + */ +internal class StoreStatusTest { + + private val runtimeStore = RuntimeSharedStore() + private val persistenceStore = MockStateDataStore(default = emptyMap()) + + private val store = DefaultNetworksStatusesStoreV2( + runtimeStore = runtimeStore, + persistenceDataStore = persistenceStore, + dispatchers = TestingCoroutineDispatcherProvider(), + ) + + @Test + fun `storeStatus if runtime and cache stores are empty`() = runTest { + val newStatus = MockNetworkStatusFactory.createVerified(network) + + store.storeStatus(userWalletId = userWalletId, status = newStatus) + + val runtimeExpected = mapOf( + userWalletId.stringValue to setOf(newStatus.toSimple()), + ) + + val persistenceExpected = mapOf( + userWalletId.stringValue to setOf(newStatus.toDataModel()!!), + ) + + Truth.assertThat(runtimeStore.getSyncOrNull()).isEqualTo(runtimeExpected) + Truth.assertThat(persistenceStore.data.firstOrNull()).isEqualTo(persistenceExpected) + } + + @Test + fun `storeStatus if runtime and cache stores contain empty map`() = runTest { + runtimeStore.store(value = emptyMap()) + persistenceStore.updateData { emptyMap() } + + val newStatus = MockNetworkStatusFactory.createVerified(network) + + store.storeStatus(userWalletId = userWalletId, status = newStatus) + + val runtimeExpected = mapOf( + userWalletId.stringValue to setOf(newStatus.toSimple()), + ) + + val persistenceExpected = mapOf( + userWalletId.stringValue to setOf(newStatus.toDataModel()!!), + ) + + Truth.assertThat(runtimeStore.getSyncOrNull()).isEqualTo(runtimeExpected) + Truth.assertThat(persistenceStore.data.firstOrNull()).isEqualTo(persistenceExpected) + } + + @Test + fun `storeStatus if runtime and cache stores contain portfolio with empty statuses`() = runTest { + runtimeStore.store( + value = mapOf(userWalletId.stringValue to setOf()), + ) + + persistenceStore.updateData { + mapOf(userWalletId.stringValue to setOf()) + } + + val newStatus = MockNetworkStatusFactory.createVerified(network) + + store.storeStatus(userWalletId = userWalletId, status = newStatus) + + val runtimeExpected = mapOf( + userWalletId.stringValue to setOf(newStatus.toSimple()), + ) + + val persistenceExpected = mapOf( + userWalletId.stringValue to setOf(newStatus.toDataModel()!!), + ) + + Truth.assertThat(runtimeStore.getSyncOrNull()).isEqualTo(runtimeExpected) + Truth.assertThat(persistenceStore.data.firstOrNull()).isEqualTo(persistenceExpected) + } + + @Test + fun `storeStatus if runtime and cache stores contain status with this network`() = runTest { + val prevStatus = MockNetworkStatusFactory.createVerified(network) + + runtimeStore.store( + value = mapOf(userWalletId.stringValue to setOf(prevStatus.toSimple())), + ) + + persistenceStore.updateData { + mapOf(userWalletId.stringValue to setOf(prevStatus.toDataModel()!!)) + } + + val newStatus = MockNetworkStatusFactory.createVerified(network).copy( + value = MockNetworkStatusFactory.createVerified(network).value.copySealed(source = StatusSource.ACTUAL), + ) + + store.storeStatus(userWalletId = userWalletId, status = newStatus) + + val runtimeExpected = mapOf( + userWalletId.stringValue to setOf(newStatus.toSimple()), + ) + + val persistenceExpected = mapOf( + userWalletId.stringValue to setOf(newStatus.toDataModel()!!), + ) + + Truth.assertThat(runtimeStore.getSyncOrNull()).isEqualTo(runtimeExpected) + Truth.assertThat(persistenceStore.data.firstOrNull()).isEqualTo(persistenceExpected) + } + + @Test + fun `store unreachable status if runtime and cache stores are empty`() = runTest { + val newStatus = MockNetworkStatusFactory.createUnreachable(network) + + store.storeStatus(userWalletId = userWalletId, status = newStatus) + + val runtimeExpected = mapOf( + userWalletId.stringValue to setOf(newStatus.toSimple()), + ) + + val persistenceExpected = emptyMap() + + Truth.assertThat(runtimeStore.getSyncOrNull()).isEqualTo(runtimeExpected) + Truth.assertThat(persistenceStore.data.firstOrNull()).isEqualTo(persistenceExpected) + } + + @Test + fun `store unreachable status if runtime and cache stores contain empty map`() = runTest { + runtimeStore.store(value = emptyMap()) + persistenceStore.updateData { emptyMap() } + + val newStatus = MockNetworkStatusFactory.createUnreachable(network) + + store.storeStatus(userWalletId = userWalletId, status = newStatus) + + val runtimeExpected = mapOf( + userWalletId.stringValue to setOf(newStatus.toSimple()), + ) + + val persistenceExpected = emptyMap() + + Truth.assertThat(runtimeStore.getSyncOrNull()).isEqualTo(runtimeExpected) + Truth.assertThat(persistenceStore.data.firstOrNull()).isEqualTo(persistenceExpected) + } + + @Test + fun `store unreachable status if runtime and cache stores contain portfolio with empty statuses`() = runTest { + runtimeStore.store( + value = mapOf(userWalletId.stringValue to setOf()), + ) + + persistenceStore.updateData { + mapOf(userWalletId.stringValue to setOf()) + } + + val newStatus = MockNetworkStatusFactory.createUnreachable(network) + + store.storeStatus(userWalletId = userWalletId, status = newStatus) + + val runtimeExpected = mapOf( + userWalletId.stringValue to setOf(newStatus.toSimple()), + ) + + val persistenceExpected = mapOf(userWalletId.stringValue to emptySet()) + + Truth.assertThat(runtimeStore.getSyncOrNull()).isEqualTo(runtimeExpected) + Truth.assertThat(persistenceStore.data.firstOrNull()).isEqualTo(persistenceExpected) + } + + @Test + fun `store unreachable status if runtime and cache stores contain verified status with this network`() = runTest { + val prevStatus = MockNetworkStatusFactory.createVerified(network) + + runtimeStore.store( + value = mapOf(userWalletId.stringValue to setOf(prevStatus.toSimple())), + ) + + persistenceStore.updateData { + mapOf(userWalletId.stringValue to setOf(prevStatus.toDataModel()!!)) + } + + val newStatus = MockNetworkStatusFactory.createUnreachable(network) + store.storeStatus(userWalletId = userWalletId, status = newStatus) + + val runtimeExpected = mapOf( + userWalletId.stringValue to setOf( + MockNetworkStatusFactory.createVerified(network = network, source = StatusSource.ONLY_CACHE).toSimple(), + ), + ) + + val persistenceExpected = mapOf( + userWalletId.stringValue to setOf(prevStatus.toDataModel()!!), + ) + + Truth.assertThat(runtimeStore.getSyncOrNull()).isEqualTo(runtimeExpected) + Truth.assertThat(persistenceStore.data.firstOrNull()).isEqualTo(persistenceExpected) + } + + @Test + fun `store unreachable status if stores contain missed derivation status with this network`() = runTest { + val prevStatus = MockNetworkStatusFactory.createMissedDerivation(network) + + runtimeStore.store( + value = mapOf(userWalletId.stringValue to setOf(prevStatus.toSimple())), + ) + + persistenceStore.updateData { + mapOf(userWalletId.stringValue to emptySet()) + } + + val newStatus = MockNetworkStatusFactory.createUnreachable(network) + + store.storeStatus(userWalletId = userWalletId, status = newStatus) + + val runtimeExpected = mapOf( + userWalletId.stringValue to setOf(newStatus.toSimple()), + ) + + val persistenceExpected = mapOf(userWalletId.stringValue to emptySet()) + + Truth.assertThat(runtimeStore.getSyncOrNull()).isEqualTo(runtimeExpected) + Truth.assertThat(persistenceStore.data.firstOrNull()).isEqualTo(persistenceExpected) + } + + private companion object { + + val userWalletId = UserWalletId(stringValue = "011") + + val network = MockCryptoCurrencyFactory().ethereum.network + } +} \ No newline at end of file diff --git a/data/networks/src/test/java/com/tangem/data/networks/store/StoreSuccessTest.kt b/data/networks/src/test/java/com/tangem/data/networks/store/StoreSuccessTest.kt new file mode 100644 index 0000000000..5e230e53ca --- /dev/null +++ b/data/networks/src/test/java/com/tangem/data/networks/store/StoreSuccessTest.kt @@ -0,0 +1,132 @@ +package com.tangem.data.networks.store + +import com.google.common.truth.Truth +import com.tangem.common.test.datastore.MockStateDataStore +import com.tangem.common.test.domain.network.MockNetworkStatusFactory +import com.tangem.common.test.domain.token.MockCryptoCurrencyFactory +import com.tangem.data.networks.toDataModel +import com.tangem.data.networks.toSimple +import com.tangem.datasource.local.datastore.RuntimeSharedStore +import com.tangem.domain.models.StatusSource +import com.tangem.domain.wallets.models.UserWalletId +import com.tangem.utils.coroutines.TestingCoroutineDispatcherProvider +import kotlinx.coroutines.flow.firstOrNull +import kotlinx.coroutines.test.runTest +import org.junit.Test + +/** +[REDACTED_AUTHOR] + */ +internal class StoreSuccessTest { + + private val runtimeStore = RuntimeSharedStore() + private val persistenceStore = MockStateDataStore(default = emptyMap()) + + private val store = DefaultNetworksStatusesStoreV2( + runtimeStore = runtimeStore, + persistenceDataStore = persistenceStore, + dispatchers = TestingCoroutineDispatcherProvider(), + ) + + @Test + fun `storeSuccess if runtime and cache stores are empty`() = runTest { + val newStatus = MockNetworkStatusFactory.createVerified(network) + + store.storeSuccess(userWalletId = userWalletId, status = newStatus) + + val runtimeExpected = mapOf( + userWalletId.stringValue to setOf(newStatus.toSimple()), + ) + + val persistenceExpected = mapOf( + userWalletId.stringValue to setOf(newStatus.toDataModel()!!), + ) + + Truth.assertThat(runtimeStore.getSyncOrNull()).isEqualTo(runtimeExpected) + Truth.assertThat(persistenceStore.data.firstOrNull()).isEqualTo(persistenceExpected) + } + + @Test + fun `storeSuccess if runtime and cache stores contain empty map`() = runTest { + runtimeStore.store(value = emptyMap()) + persistenceStore.updateData { emptyMap() } + + val newStatus = MockNetworkStatusFactory.createVerified(network) + + store.storeSuccess(userWalletId = userWalletId, status = newStatus) + + val runtimeExpected = mapOf( + userWalletId.stringValue to setOf(newStatus.toSimple()), + ) + + val persistenceExpected = mapOf( + userWalletId.stringValue to setOf(newStatus.toDataModel()!!), + ) + + Truth.assertThat(runtimeStore.getSyncOrNull()).isEqualTo(runtimeExpected) + Truth.assertThat(persistenceStore.data.firstOrNull()).isEqualTo(persistenceExpected) + } + + @Test + fun `storeSuccess if runtime and cache stores contain portfolio with empty statuses`() = runTest { + runtimeStore.store( + value = mapOf(userWalletId.stringValue to setOf()), + ) + + persistenceStore.updateData { + mapOf(userWalletId.stringValue to setOf()) + } + + val newStatus = MockNetworkStatusFactory.createVerified(network) + + store.storeSuccess(userWalletId = userWalletId, status = newStatus) + + val runtimeExpected = mapOf( + userWalletId.stringValue to setOf(newStatus.toSimple()), + ) + + val persistenceExpected = mapOf( + userWalletId.stringValue to setOf(newStatus.toDataModel()!!), + ) + + Truth.assertThat(runtimeStore.getSyncOrNull()).isEqualTo(runtimeExpected) + Truth.assertThat(persistenceStore.data.firstOrNull()).isEqualTo(persistenceExpected) + } + + @Test + fun `storeSuccess if runtime and cache stores contain status with this network`() = runTest { + val prevStatus = MockNetworkStatusFactory.createVerified(network) + + runtimeStore.store( + value = mapOf(userWalletId.stringValue to setOf(prevStatus.toSimple())), + ) + + persistenceStore.updateData { + mapOf(userWalletId.stringValue to setOf(prevStatus.toDataModel()!!)) + } + + val newStatus = MockNetworkStatusFactory.createVerified(network).copy( + value = MockNetworkStatusFactory.createVerified(network).value.copySealed(source = StatusSource.ACTUAL), + ) + + store.storeSuccess(userWalletId = userWalletId, status = newStatus) + + val runtimeExpected = mapOf( + userWalletId.stringValue to setOf(newStatus.toSimple()), + ) + + val persistenceExpected = mapOf( + userWalletId.stringValue to setOf(newStatus.toDataModel()!!), + ) + + Truth.assertThat(runtimeStore.getSyncOrNull()).isEqualTo(runtimeExpected) + Truth.assertThat(persistenceStore.data.firstOrNull()).isEqualTo(persistenceExpected) + } + + private companion object { + + val userWalletId = UserWalletId(stringValue = "011") + + val network = MockCryptoCurrencyFactory().ethereum.network + } +} \ No newline at end of file diff --git a/data/networks/src/test/java/com/tangem/data/networks/store/StoreTest.kt b/data/networks/src/test/java/com/tangem/data/networks/store/StoreTest.kt new file mode 100644 index 0000000000..21364e70b0 --- /dev/null +++ b/data/networks/src/test/java/com/tangem/data/networks/store/StoreTest.kt @@ -0,0 +1,132 @@ +package com.tangem.data.networks.store + +import com.google.common.truth.Truth +import com.tangem.common.test.datastore.MockStateDataStore +import com.tangem.common.test.domain.network.MockNetworkStatusFactory +import com.tangem.common.test.domain.token.MockCryptoCurrencyFactory +import com.tangem.data.networks.toDataModel +import com.tangem.data.networks.toSimple +import com.tangem.datasource.local.datastore.RuntimeSharedStore +import com.tangem.domain.models.StatusSource +import com.tangem.domain.wallets.models.UserWalletId +import com.tangem.utils.coroutines.TestingCoroutineDispatcherProvider +import kotlinx.coroutines.flow.firstOrNull +import kotlinx.coroutines.test.runTest +import org.junit.Test + +/** +[REDACTED_AUTHOR] + */ +internal class StoreTest { + + private val runtimeStore = RuntimeSharedStore() + private val persistenceStore = MockStateDataStore(default = emptyMap()) + + private val store = DefaultNetworksStatusesStoreV2( + runtimeStore = runtimeStore, + persistenceDataStore = persistenceStore, + dispatchers = TestingCoroutineDispatcherProvider(), + ) + + @Test + fun `store if runtime and cache stores are empty`() = runTest { + val newStatus = MockNetworkStatusFactory.createVerified(network) + + store.store(userWalletId = userWalletId, status = newStatus) + + val runtimeExpected = mapOf( + userWalletId.stringValue to setOf(newStatus.toSimple()), + ) + + val persistenceExpected = mapOf( + userWalletId.stringValue to setOf(newStatus.toDataModel()!!), + ) + + Truth.assertThat(runtimeStore.getSyncOrNull()).isEqualTo(runtimeExpected) + Truth.assertThat(persistenceStore.data.firstOrNull()).isEqualTo(persistenceExpected) + } + + @Test + fun `store if runtime and cache stores contain empty map`() = runTest { + runtimeStore.store(value = emptyMap()) + persistenceStore.updateData { emptyMap() } + + val newStatus = MockNetworkStatusFactory.createVerified(network) + + store.store(userWalletId = userWalletId, status = newStatus) + + val runtimeExpected = mapOf( + userWalletId.stringValue to setOf(newStatus.toSimple()), + ) + + val persistenceExpected = mapOf( + userWalletId.stringValue to setOf(newStatus.toDataModel()!!), + ) + + Truth.assertThat(runtimeStore.getSyncOrNull()).isEqualTo(runtimeExpected) + Truth.assertThat(persistenceStore.data.firstOrNull()).isEqualTo(persistenceExpected) + } + + @Test + fun `store if runtime and cache stores contain portfolio with empty statuses`() = runTest { + runtimeStore.store( + value = mapOf(userWalletId.stringValue to setOf()), + ) + + persistenceStore.updateData { + mapOf(userWalletId.stringValue to setOf()) + } + + val newStatus = MockNetworkStatusFactory.createVerified(network) + + store.store(userWalletId = userWalletId, status = newStatus) + + val runtimeExpected = mapOf( + userWalletId.stringValue to setOf(newStatus.toSimple()), + ) + + val persistenceExpected = mapOf( + userWalletId.stringValue to setOf(newStatus.toDataModel()!!), + ) + + Truth.assertThat(runtimeStore.getSyncOrNull()).isEqualTo(runtimeExpected) + Truth.assertThat(persistenceStore.data.firstOrNull()).isEqualTo(persistenceExpected) + } + + @Test + fun `store if runtime and cache stores contain status with this network`() = runTest { + val prevStatus = MockNetworkStatusFactory.createVerified(network) + + runtimeStore.store( + value = mapOf(userWalletId.stringValue to setOf(prevStatus.toSimple())), + ) + + persistenceStore.updateData { + mapOf(userWalletId.stringValue to setOf(prevStatus.toDataModel()!!)) + } + + val newStatus = MockNetworkStatusFactory.createVerified(network).copy( + value = MockNetworkStatusFactory.createVerified(network).value.copySealed(source = StatusSource.ACTUAL), + ) + + store.store(userWalletId = userWalletId, status = newStatus) + + val runtimeExpected = mapOf( + userWalletId.stringValue to setOf(newStatus.toSimple()), + ) + + val persistenceExpected = mapOf( + userWalletId.stringValue to setOf(newStatus.toDataModel()!!), + ) + + Truth.assertThat(runtimeStore.getSyncOrNull()).isEqualTo(runtimeExpected) + Truth.assertThat(persistenceStore.data.firstOrNull()).isEqualTo(persistenceExpected) + } + + private companion object { + + val userWalletId = UserWalletId(stringValue = "011") + + val network = MockCryptoCurrencyFactory().ethereum.network + } +} \ No newline at end of file diff --git a/data/networks/src/test/java/com/tangem/data/networks/store/UpdateStatusSourceTest.kt b/data/networks/src/test/java/com/tangem/data/networks/store/UpdateStatusSourceTest.kt new file mode 100644 index 0000000000..285b7c2ac8 --- /dev/null +++ b/data/networks/src/test/java/com/tangem/data/networks/store/UpdateStatusSourceTest.kt @@ -0,0 +1,477 @@ +package com.tangem.data.networks.store + +import com.google.common.truth.Truth +import com.tangem.common.test.datastore.MockStateDataStore +import com.tangem.common.test.domain.network.MockNetworkStatusFactory +import com.tangem.common.test.domain.token.MockCryptoCurrencyFactory +import com.tangem.data.networks.models.SimpleNetworkStatus +import com.tangem.data.networks.toDataModel +import com.tangem.data.networks.toSimple +import com.tangem.datasource.local.datastore.RuntimeSharedStore +import com.tangem.datasource.local.network.entity.NetworkStatusDM +import com.tangem.domain.models.StatusSource +import com.tangem.domain.tokens.model.NetworkAddress +import com.tangem.domain.tokens.model.NetworkStatus +import com.tangem.domain.wallets.models.UserWalletId +import com.tangem.utils.coroutines.TestingCoroutineDispatcherProvider +import kotlinx.coroutines.flow.firstOrNull +import kotlinx.coroutines.test.runTest +import org.junit.Test + +/** +[REDACTED_AUTHOR] + */ +internal class UpdateStatusSourceTest { + + private val runtimeStore = RuntimeSharedStore() + private val persistenceStore = MockStateDataStore(default = emptyMap()) + + private val store = DefaultNetworksStatusesStoreV2( + runtimeStore = runtimeStore, + persistenceDataStore = persistenceStore, + dispatchers = TestingCoroutineDispatcherProvider(), + ) + + @Test + fun `updateStatusSource if stores are empty`() = runTest { + store.updateStatusSource( + userWalletId = userWalletId, + network = network, + source = StatusSource.ACTUAL, + ) + + val runtimeExpected = mapOf(userWalletId.stringValue to emptySet()) + + val persistenceExpected = emptyMap>() + + Truth.assertThat(runtimeStore.getSyncOrNull()).isEqualTo(runtimeExpected) + Truth.assertThat(persistenceStore.data.firstOrNull()).isEqualTo(persistenceExpected) + } + + @Test + fun `updateStatusSource with ifNotFound if stores are empty`() = runTest { + val default = MockNetworkStatusFactory.createUnreachable(network).copy( + value = NetworkStatus.Unreachable( + address = NetworkAddress.Single( + defaultAddress = NetworkAddress.Address( + value = "", + type = NetworkAddress.Address.Type.Primary, + ), + ), + ), + ) + + store.updateStatusSource( + userWalletId = userWalletId, + network = network, + source = StatusSource.ACTUAL, + ifNotFound = { default.toSimple() }, + ) + + val runtimeExpected = mapOf(userWalletId.stringValue to setOf(default.toSimple())) + + val persistenceExpected = emptyMap>() + + Truth.assertThat(runtimeStore.getSyncOrNull()).isEqualTo(runtimeExpected) + Truth.assertThat(persistenceStore.data.firstOrNull()).isEqualTo(persistenceExpected) + } + + @Test + fun `updateStatusSource if stores contain empty map`() = runTest { + runtimeStore.store(value = emptyMap()) + persistenceStore.updateData { emptyMap() } + + store.updateStatusSource( + userWalletId = userWalletId, + network = network, + source = StatusSource.ACTUAL, + ) + + val runtimeExpected = mapOf(userWalletId.stringValue to emptySet()) + + val persistenceExpected = emptyMap>() + + Truth.assertThat(runtimeStore.getSyncOrNull()).isEqualTo(runtimeExpected) + Truth.assertThat(persistenceStore.data.firstOrNull()).isEqualTo(persistenceExpected) + } + + @Test + fun `updateStatusSource with ifNotFound if stores contain empty map`() = runTest { + runtimeStore.store(value = emptyMap()) + persistenceStore.updateData { emptyMap() } + + val default = MockNetworkStatusFactory.createUnreachable(network).copy( + value = NetworkStatus.Unreachable( + address = NetworkAddress.Single( + defaultAddress = NetworkAddress.Address( + value = "", + type = NetworkAddress.Address.Type.Primary, + ), + ), + ), + ) + + store.updateStatusSource( + userWalletId = userWalletId, + network = network, + source = StatusSource.ACTUAL, + ifNotFound = { default.toSimple() }, + ) + + val runtimeExpected = mapOf(userWalletId.stringValue to setOf(default.toSimple())) + + val persistenceExpected = emptyMap>() + + Truth.assertThat(runtimeStore.getSyncOrNull()).isEqualTo(runtimeExpected) + Truth.assertThat(persistenceStore.data.firstOrNull()).isEqualTo(persistenceExpected) + } + + @Test + fun `updateStatusSource if stores contain portfolio with empty statuses`() = runTest { + runtimeStore.store( + value = mapOf(userWalletId.stringValue to setOf()), + ) + + persistenceStore.updateData { + mapOf(userWalletId.stringValue to setOf()) + } + + store.updateStatusSource( + userWalletId = userWalletId, + network = network, + source = StatusSource.ACTUAL, + ) + + val runtimeExpected = mapOf(userWalletId.stringValue to emptySet()) + val persistenceExpected = mapOf(userWalletId.stringValue to emptySet()) + + Truth.assertThat(runtimeStore.getSyncOrNull()).isEqualTo(runtimeExpected) + Truth.assertThat(persistenceStore.data.firstOrNull()).isEqualTo(persistenceExpected) + } + + @Test + fun `updateStatusSource with ifNotFound if stores contain portfolio with empty statuses`() = runTest { + runtimeStore.store( + value = mapOf(userWalletId.stringValue to setOf()), + ) + + persistenceStore.updateData { + mapOf(userWalletId.stringValue to setOf()) + } + + val default = MockNetworkStatusFactory.createUnreachable(network).copy( + value = NetworkStatus.Unreachable( + address = NetworkAddress.Single( + defaultAddress = NetworkAddress.Address( + value = "", + type = NetworkAddress.Address.Type.Primary, + ), + ), + ), + ) + + store.updateStatusSource( + userWalletId = userWalletId, + network = network, + source = StatusSource.ACTUAL, + ifNotFound = { default.toSimple() }, + ) + + val runtimeExpected = mapOf(userWalletId.stringValue to setOf(default.toSimple())) + val persistenceExpected = mapOf(userWalletId.stringValue to emptySet()) + + Truth.assertThat(runtimeStore.getSyncOrNull()).isEqualTo(runtimeExpected) + Truth.assertThat(persistenceStore.data.firstOrNull()).isEqualTo(persistenceExpected) + } + + @Test + fun `updateStatusSource if stores contain status with this network`() = runTest { + val prevStatus = MockNetworkStatusFactory.createVerified(network) + + runtimeStore.store( + value = mapOf(userWalletId.stringValue to setOf(prevStatus.toSimple())), + ) + + persistenceStore.updateData { + mapOf(userWalletId.stringValue to setOf(prevStatus.toDataModel()!!)) + } + + store.updateStatusSource( + userWalletId = userWalletId, + network = network, + source = StatusSource.ACTUAL, + ) + + val newStatus = MockNetworkStatusFactory.createVerified(network) + + val runtimeExpected = mapOf( + userWalletId.stringValue to setOf(newStatus.toSimple()), + ) + + val persistenceExpected = mapOf( + userWalletId.stringValue to setOf(newStatus.toDataModel()!!), + ) + + Truth.assertThat(runtimeStore.getSyncOrNull()).isEqualTo(runtimeExpected) + Truth.assertThat(persistenceStore.data.firstOrNull()).isEqualTo(persistenceExpected) + } + + @Test + fun `updateStatusSource with ifNotFound if stores contain status with this network`() = runTest { + val prevStatus = MockNetworkStatusFactory.createVerified(network) + + runtimeStore.store( + value = mapOf(userWalletId.stringValue to setOf(prevStatus.toSimple())), + ) + + persistenceStore.updateData { + mapOf(userWalletId.stringValue to setOf(prevStatus.toDataModel()!!)) + } + + store.updateStatusSource( + userWalletId = userWalletId, + network = network, + source = StatusSource.ACTUAL, + ifNotFound = { prevStatus.toSimple() }, + ) + + val newStatus = MockNetworkStatusFactory.createVerified(network) + + val runtimeExpected = mapOf( + userWalletId.stringValue to setOf(newStatus.toSimple()), + ) + + val persistenceExpected = mapOf( + userWalletId.stringValue to setOf(newStatus.toDataModel()!!), + ) + + Truth.assertThat(runtimeStore.getSyncOrNull()).isEqualTo(runtimeExpected) + Truth.assertThat(persistenceStore.data.firstOrNull()).isEqualTo(persistenceExpected) + } + + @Test + fun `updateStatusSource of networks if stores are empty`() = runTest { + store.updateStatusSource( + userWalletId = userWalletId, + networks = networks, + source = StatusSource.ACTUAL, + ) + + val runtimeExpected = mapOf(userWalletId.stringValue to emptySet()) + + val persistenceExpected = emptyMap>() + + Truth.assertThat(runtimeStore.getSyncOrNull()).isEqualTo(runtimeExpected) + Truth.assertThat(persistenceStore.data.firstOrNull()).isEqualTo(persistenceExpected) + } + + @Test + fun `updateStatusSource of networks with ifNotFound if stores are empty`() = runTest { + val default = MockNetworkStatusFactory.createUnreachable(network).copy( + value = NetworkStatus.Unreachable( + address = NetworkAddress.Single( + defaultAddress = NetworkAddress.Address( + value = "", + type = NetworkAddress.Address.Type.Primary, + ), + ), + ), + ) + + store.updateStatusSource( + userWalletId = userWalletId, + networks = networks, + source = StatusSource.ACTUAL, + ifNotFound = { default.toSimple() }, + ) + + val runtimeExpected = mapOf(userWalletId.stringValue to setOf(default.toSimple())) + + val persistenceExpected = emptyMap>() + + Truth.assertThat(runtimeStore.getSyncOrNull()).isEqualTo(runtimeExpected) + Truth.assertThat(persistenceStore.data.firstOrNull()).isEqualTo(persistenceExpected) + } + + @Test + fun `updateStatusSource of networks if stores contain empty map`() = runTest { + runtimeStore.store(value = emptyMap()) + persistenceStore.updateData { emptyMap() } + + store.updateStatusSource( + userWalletId = userWalletId, + networks = networks, + source = StatusSource.ACTUAL, + ) + + val runtimeExpected = mapOf(userWalletId.stringValue to emptySet()) + + val persistenceExpected = emptyMap>() + + Truth.assertThat(runtimeStore.getSyncOrNull()).isEqualTo(runtimeExpected) + Truth.assertThat(persistenceStore.data.firstOrNull()).isEqualTo(persistenceExpected) + } + + @Test + fun `updateStatusSource of networks with ifNotFound if stores contain empty map`() = runTest { + runtimeStore.store(value = emptyMap()) + persistenceStore.updateData { emptyMap() } + + val default = MockNetworkStatusFactory.createUnreachable(network).copy( + value = NetworkStatus.Unreachable( + address = NetworkAddress.Single( + defaultAddress = NetworkAddress.Address( + value = "", + type = NetworkAddress.Address.Type.Primary, + ), + ), + ), + ) + + store.updateStatusSource( + userWalletId = userWalletId, + networks = networks, + source = StatusSource.ACTUAL, + ifNotFound = { default.toSimple() }, + ) + + val runtimeExpected = mapOf(userWalletId.stringValue to setOf(default.toSimple())) + + val persistenceExpected = emptyMap>() + + Truth.assertThat(runtimeStore.getSyncOrNull()).isEqualTo(runtimeExpected) + Truth.assertThat(persistenceStore.data.firstOrNull()).isEqualTo(persistenceExpected) + } + + @Test + fun `updateStatusSource of networks if stores contain portfolio with empty statuses`() = runTest { + runtimeStore.store( + value = mapOf(userWalletId.stringValue to setOf()), + ) + + persistenceStore.updateData { + mapOf(userWalletId.stringValue to setOf()) + } + + store.updateStatusSource( + userWalletId = userWalletId, + networks = networks, + source = StatusSource.ACTUAL, + ) + + val runtimeExpected = mapOf(userWalletId.stringValue to emptySet()) + val persistenceExpected = mapOf(userWalletId.stringValue to emptySet()) + + Truth.assertThat(runtimeStore.getSyncOrNull()).isEqualTo(runtimeExpected) + Truth.assertThat(persistenceStore.data.firstOrNull()).isEqualTo(persistenceExpected) + } + + @Test + fun `updateStatusSource of networks with ifNotFound if stores contain portfolio with empty statuses`() = runTest { + runtimeStore.store( + value = mapOf(userWalletId.stringValue to setOf()), + ) + + persistenceStore.updateData { + mapOf(userWalletId.stringValue to setOf()) + } + + val default = MockNetworkStatusFactory.createUnreachable(network).copy( + value = NetworkStatus.Unreachable( + address = NetworkAddress.Single( + defaultAddress = NetworkAddress.Address( + value = "", + type = NetworkAddress.Address.Type.Primary, + ), + ), + ), + ) + + store.updateStatusSource( + userWalletId = userWalletId, + networks = networks, + source = StatusSource.ACTUAL, + ifNotFound = { default.toSimple() }, + ) + + val runtimeExpected = mapOf(userWalletId.stringValue to setOf(default.toSimple())) + val persistenceExpected = mapOf(userWalletId.stringValue to emptySet()) + + Truth.assertThat(runtimeStore.getSyncOrNull()).isEqualTo(runtimeExpected) + Truth.assertThat(persistenceStore.data.firstOrNull()).isEqualTo(persistenceExpected) + } + + @Test + fun `updateStatusSource of networks if stores contain status with this network`() = runTest { + val prevStatus = MockNetworkStatusFactory.createVerified(network) + + runtimeStore.store( + value = mapOf(userWalletId.stringValue to setOf(prevStatus.toSimple())), + ) + + persistenceStore.updateData { + mapOf(userWalletId.stringValue to setOf(prevStatus.toDataModel()!!)) + } + + store.updateStatusSource( + userWalletId = userWalletId, + networks = networks, + source = StatusSource.ACTUAL, + ) + + val newStatus = MockNetworkStatusFactory.createVerified(network) + + val runtimeExpected = mapOf( + userWalletId.stringValue to setOf(newStatus.toSimple()), + ) + + val persistenceExpected = mapOf( + userWalletId.stringValue to setOf(newStatus.toDataModel()!!), + ) + + Truth.assertThat(runtimeStore.getSyncOrNull()).isEqualTo(runtimeExpected) + Truth.assertThat(persistenceStore.data.firstOrNull()).isEqualTo(persistenceExpected) + } + + @Test + fun `updateStatusSource of networks with ifNotFound if stores contain status with this network`() = runTest { + val prevStatus = MockNetworkStatusFactory.createVerified(network) + + runtimeStore.store( + value = mapOf(userWalletId.stringValue to setOf(prevStatus.toSimple())), + ) + + persistenceStore.updateData { + mapOf(userWalletId.stringValue to setOf(prevStatus.toDataModel()!!)) + } + + store.updateStatusSource( + userWalletId = userWalletId, + networks = networks, + source = StatusSource.ACTUAL, + ifNotFound = { prevStatus.toSimple() }, + ) + + val newStatus = MockNetworkStatusFactory.createVerified(network) + + val runtimeExpected = mapOf( + userWalletId.stringValue to setOf(newStatus.toSimple()), + ) + + val persistenceExpected = mapOf( + userWalletId.stringValue to setOf(newStatus.toDataModel()!!), + ) + + Truth.assertThat(runtimeStore.getSyncOrNull()).isEqualTo(runtimeExpected) + Truth.assertThat(persistenceStore.data.firstOrNull()).isEqualTo(persistenceExpected) + } + + private companion object { + + val userWalletId = UserWalletId(stringValue = "011") + + val network = MockCryptoCurrencyFactory().ethereum.network + + val networks = MockCryptoCurrencyFactory().ethereumAndStellar.mapTo(hashSetOf()) { it.network } + } +} \ No newline at end of file From 1b33e6b02ee59aed787e2388d58504d34102fa11 Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 14 May 2025 15:33:20 +0400 Subject: [PATCH 034/165] Updated on 2026-08-14 --- data/networks/build.gradle.kts | 7 +- .../repository/DefaultNetworksRepository.kt | 88 ++++++ .../DefaultNetworksRepositoryTest.kt | 268 ++++++++++++++++++ .../networks/repository/NetworksRepository.kt | 24 ++ gradle/dependencies.toml | 3 + 5 files changed, 389 insertions(+), 1 deletion(-) create mode 100644 data/networks/src/main/java/com/tangem/data/networks/repository/DefaultNetworksRepository.kt create mode 100644 data/networks/src/test/java/com/tangem/data/networks/repository/DefaultNetworksRepositoryTest.kt create mode 100644 domain/networks/src/main/java/com/tangem/domain/networks/repository/NetworksRepository.kt diff --git a/data/networks/build.gradle.kts b/data/networks/build.gradle.kts index 1dc873674c..510981e800 100644 --- a/data/networks/build.gradle.kts +++ b/data/networks/build.gradle.kts @@ -9,6 +9,10 @@ android { namespace = "com.tangem.data.networks" } +tasks.withType().configureEach { + useJUnitPlatform() +} + dependencies { // region Project - Core implementation(projects.core.datasource) @@ -52,7 +56,8 @@ dependencies { // region Tests testImplementation(deps.test.coroutine) - testImplementation(deps.test.junit) + testImplementation(deps.test.junit5) + testRuntimeOnly(deps.test.junit5.engine) testImplementation(deps.test.mockk) testImplementation(deps.test.truth) testImplementation(projects.common.test) diff --git a/data/networks/src/main/java/com/tangem/data/networks/repository/DefaultNetworksRepository.kt b/data/networks/src/main/java/com/tangem/data/networks/repository/DefaultNetworksRepository.kt new file mode 100644 index 0000000000..97bb6128ac --- /dev/null +++ b/data/networks/src/main/java/com/tangem/data/networks/repository/DefaultNetworksRepository.kt @@ -0,0 +1,88 @@ +package com.tangem.data.networks.repository + +import com.tangem.data.common.currency.CardCryptoCurrencyFactory +import com.tangem.data.networks.store.NetworksStatusesStoreV2 +import com.tangem.data.networks.store.storeStatus +import com.tangem.data.networks.utils.NetworkStatusFactory +import com.tangem.domain.networks.repository.NetworksRepository +import com.tangem.domain.tokens.model.CryptoCurrency +import com.tangem.domain.tokens.model.CryptoCurrencyAddress +import com.tangem.domain.tokens.model.Network +import com.tangem.domain.walletmanager.WalletManagersFacade +import com.tangem.domain.wallets.models.UserWalletId +import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import kotlinx.coroutines.withContext +import timber.log.Timber + +/** + * Default implementation of [NetworksRepository] + * + * @property cardCryptoCurrencyFactory card crypto currency factory + * @property walletManagersFacade wallet managers facade + * @property networksStatusesStore networks statuses store + * @property dispatchers dispatchers + * +[REDACTED_AUTHOR] + */ +internal class DefaultNetworksRepository( + private val cardCryptoCurrencyFactory: CardCryptoCurrencyFactory, + private val walletManagersFacade: WalletManagersFacade, + private val networksStatusesStore: NetworksStatusesStoreV2, + private val dispatchers: CoroutineDispatcherProvider, +) : NetworksRepository { + + override suspend fun fetchPendingTransactions(userWalletId: UserWalletId, network: Network) { + withContext(dispatchers.default) { + val currencies = runCatching { + cardCryptoCurrencyFactory.create(userWalletId = userWalletId, network = network) + } + .getOrElse { + Timber.e(it, "Unable to create wallet currencies") + return@withContext + } + + fetchPendingTransactions(userWalletId = userWalletId, network = network, currencies = currencies) + } + } + + override suspend fun getNetworkAddresses( + userWalletId: UserWalletId, + network: Network, + ): List { + return runCatching { cardCryptoCurrencyFactory.create(userWalletId = userWalletId, network = network) } + .getOrElse { + Timber.e(it, "Unable to create wallet currencies") + return emptyList() + } + .map { currency -> + CryptoCurrencyAddress( + cryptoCurrency = currency, + address = getDefaultAddress(userWalletId, network), + ) + } + } + + private suspend fun fetchPendingTransactions( + userWalletId: UserWalletId, + network: Network, + currencies: List, + ) { + val result = withContext(dispatchers.io) { + walletManagersFacade.updatePendingTransactions(userWalletId = userWalletId, network = network) + } + + val networkStatus = NetworkStatusFactory.create( + network = network, + updatingResult = result, + addedCurrencies = currencies.toSet(), + ) + + networksStatusesStore.storeStatus(userWalletId = userWalletId, status = networkStatus) + } + + private suspend fun getDefaultAddress(userWalletId: UserWalletId, network: Network): String { + return withContext(dispatchers.io) { + walletManagersFacade.getDefaultAddress(userWalletId = userWalletId, network = network).orEmpty() + } + } +} \ No newline at end of file diff --git a/data/networks/src/test/java/com/tangem/data/networks/repository/DefaultNetworksRepositoryTest.kt b/data/networks/src/test/java/com/tangem/data/networks/repository/DefaultNetworksRepositoryTest.kt new file mode 100644 index 0000000000..1facb5017b --- /dev/null +++ b/data/networks/src/test/java/com/tangem/data/networks/repository/DefaultNetworksRepositoryTest.kt @@ -0,0 +1,268 @@ +package com.tangem.data.networks.repository + +import com.google.common.truth.Truth +import com.tangem.common.test.domain.token.MockCryptoCurrencyFactory +import com.tangem.common.test.domain.walletmanager.MockUpdateWalletManagerResultFactory +import com.tangem.data.common.currency.CardCryptoCurrencyFactory +import com.tangem.data.networks.store.NetworksStatusesStoreV2 +import com.tangem.data.networks.store.storeStatus +import com.tangem.data.networks.utils.NetworkStatusFactory +import com.tangem.domain.tokens.model.CryptoCurrencyAddress +import com.tangem.domain.tokens.model.NetworkStatus +import com.tangem.domain.walletmanager.WalletManagersFacade +import com.tangem.domain.walletmanager.model.UpdateWalletManagerResult +import com.tangem.domain.wallets.models.UserWalletId +import com.tangem.utils.coroutines.TestingCoroutineDispatcherProvider +import io.mockk.* +import kotlinx.coroutines.test.runTest +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Nested +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.TestInstance + +/** +[REDACTED_AUTHOR] + */ +@TestInstance(TestInstance.Lifecycle.PER_CLASS) +internal class DefaultNetworksRepositoryTest { + + private val cardCryptoCurrencyFactory: CardCryptoCurrencyFactory = mockk() + private val walletManagersFacade: WalletManagersFacade = mockk() + private val networksStatusesStore: NetworksStatusesStoreV2 = mockk(relaxUnitFun = true) + + private val repository: DefaultNetworksRepository = DefaultNetworksRepository( + cardCryptoCurrencyFactory = cardCryptoCurrencyFactory, + walletManagersFacade = walletManagersFacade, + networksStatusesStore = networksStatusesStore, + dispatchers = TestingCoroutineDispatcherProvider(), + ) + + @BeforeEach + fun resetMocks() { + clearMocks(cardCryptoCurrencyFactory, walletManagersFacade, networksStatusesStore) + } + + @Nested + inner class FetchPendingTransactions { + + @Test + fun `walletManagersFacade returns Verified`() = runTest { + // Arrange + val result = updateWalletManagerResultFactory.createVerified() + val status = result.toNetworkStatus() + + coEvery { + cardCryptoCurrencyFactory.create(userWalletId = userWalletId, network = network) + } returns listOf(cryptoCurrency) + + coEvery { + walletManagersFacade.updatePendingTransactions(userWalletId = userWalletId, network = network) + } returns result + + coEvery { networksStatusesStore.storeStatus(userWalletId = userWalletId, status = status) } returns Unit + + // Act + repository.fetchPendingTransactions(userWalletId = userWalletId, network = network) + + // Assert + coVerifyOrder { + cardCryptoCurrencyFactory.create(userWalletId = userWalletId, network = network) + walletManagersFacade.updatePendingTransactions(userWalletId = userWalletId, network = network) + networksStatusesStore.storeStatus(userWalletId = userWalletId, status = status) + } + } + + @Test + fun `walletManagersFacade returns NoAccount`() = runTest { + // Arrange + val result = updateWalletManagerResultFactory.createNoAccount() + val status = result.toNetworkStatus() + + coEvery { + cardCryptoCurrencyFactory.create(userWalletId = userWalletId, network = network) + } returns listOf(cryptoCurrency) + + coEvery { + walletManagersFacade.updatePendingTransactions(userWalletId = userWalletId, network = network) + } returns result + + coEvery { networksStatusesStore.storeStatus(userWalletId = userWalletId, status = status) } returns Unit + + // Act + repository.fetchPendingTransactions(userWalletId = userWalletId, network = network) + + // Assert + coVerifyOrder { + cardCryptoCurrencyFactory.create(userWalletId = userWalletId, network = network) + walletManagersFacade.updatePendingTransactions(userWalletId = userWalletId, network = network) + networksStatusesStore.storeStatus(userWalletId = userWalletId, status = status) + } + } + + @Test + fun `walletManagersFacade returns MissedDerivation`() = runTest { + // Arrange + val result = UpdateWalletManagerResult.MissedDerivation + val status = result.toNetworkStatus() + + coEvery { + cardCryptoCurrencyFactory.create(userWalletId = userWalletId, network = network) + } returns listOf(cryptoCurrency) + + coEvery { + walletManagersFacade.updatePendingTransactions(userWalletId = userWalletId, network = network) + } returns result + + coEvery { networksStatusesStore.storeStatus(userWalletId = userWalletId, status = status) } returns Unit + + // Act + repository.fetchPendingTransactions(userWalletId = userWalletId, network = network) + + // Assert + coVerifyOrder { + cardCryptoCurrencyFactory.create(userWalletId = userWalletId, network = network) + walletManagersFacade.updatePendingTransactions(userWalletId = userWalletId, network = network) + networksStatusesStore.storeStatus(userWalletId = userWalletId, status = status) + } + } + + @Test + fun `walletManagersFacade returns Unreachable`() = runTest { + // Arrange + val result = updateWalletManagerResultFactory.createUnreachableWithAddress() + val status = result.toNetworkStatus() + + coEvery { + cardCryptoCurrencyFactory.create(userWalletId = userWalletId, network = network) + } returns listOf(cryptoCurrency) + + coEvery { + walletManagersFacade.updatePendingTransactions(userWalletId = userWalletId, network = network) + } returns result + + coEvery { networksStatusesStore.storeStatus(userWalletId = userWalletId, status = status) } returns Unit + + // Act + repository.fetchPendingTransactions(userWalletId = userWalletId, network = network) + + // Assert + coVerifyOrder { + cardCryptoCurrencyFactory.create(userWalletId = userWalletId, network = network) + walletManagersFacade.updatePendingTransactions(userWalletId = userWalletId, network = network) + networksStatusesStore.storeStatus(userWalletId = userWalletId, status = status) + } + } + + @Test + fun `cardCryptoCurrencyFactory throws exception`() = runTest { + // Arrange + coEvery { + cardCryptoCurrencyFactory.create(userWalletId = userWalletId, network = network) + } throws IllegalStateException() + + // Act + repository.fetchPendingTransactions(userWalletId = userWalletId, network = network) + + // Assert + coVerify { + cardCryptoCurrencyFactory.create(userWalletId = userWalletId, network = network) + } + + coVerify(inverse = true) { + walletManagersFacade.updatePendingTransactions(userWalletId = any(), network = any()) + } + } + } + + @Nested + inner class GetNetworkAddresses { + + @Test + fun `cardCryptoCurrencyFactory create throws exception`() = runTest { + // Arrange + coEvery { + cardCryptoCurrencyFactory.create(userWalletId = userWalletId, network = network) + } throws IllegalStateException() + + // Act + val actual = repository.getNetworkAddresses(userWalletId = userWalletId, network = network) + + // Assert + val expected = emptyList() + + Truth.assertThat(actual).isEqualTo(expected) + + coVerify { cardCryptoCurrencyFactory.create(userWalletId = userWalletId, network = network) } + coVerify(inverse = true) { + walletManagersFacade.getDefaultAddress(userWalletId = userWalletId, network = network) + } + } + + @Test + fun `cardCryptoCurrencyFactory create returns empty list`() = runTest { + // Arrange + coEvery { + cardCryptoCurrencyFactory.create(userWalletId = userWalletId, network = network) + } returns emptyList() + + // Act + val actual = repository.getNetworkAddresses(userWalletId = userWalletId, network = network) + + // Assert + val expected = emptyList() + + Truth.assertThat(actual).isEqualTo(expected) + + coVerify { cardCryptoCurrencyFactory.create(userWalletId = userWalletId, network = network) } + coVerify(inverse = true) { + walletManagersFacade.getDefaultAddress(userWalletId = userWalletId, network = network) + } + } + + @Test + fun `cardCryptoCurrencyFactory create returns not empty list`() = runTest { + // Arrange + val currencies = listOf(cryptoCurrency) + + coEvery { + cardCryptoCurrencyFactory.create(userWalletId = userWalletId, network = network) + } returns currencies + + coEvery { + walletManagersFacade.getDefaultAddress(userWalletId = userWalletId, network = cryptoCurrency.network) + } returns "address" + + // Act + val actual = repository.getNetworkAddresses(userWalletId = userWalletId, network = network) + + // Assert + val expected = listOf( + CryptoCurrencyAddress(cryptoCurrency = cryptoCurrency, address = "address"), + ) + + Truth.assertThat(actual).isEqualTo(expected) + + coVerify { + cardCryptoCurrencyFactory.create(userWalletId = userWalletId, network = cryptoCurrency.network) + walletManagersFacade.getDefaultAddress(userWalletId = userWalletId, network = cryptoCurrency.network) + } + } + } + + private companion object { + + val userWalletId = UserWalletId("011") + val cryptoCurrency = MockCryptoCurrencyFactory().ethereum + val network = MockCryptoCurrencyFactory().ethereum.network + + val updateWalletManagerResultFactory = MockUpdateWalletManagerResultFactory() + + fun UpdateWalletManagerResult.toNetworkStatus(): NetworkStatus { + return NetworkStatusFactory.create( + network = network, + updatingResult = this, + addedCurrencies = setOf(cryptoCurrency), + ) + } + } +} \ No newline at end of file diff --git a/domain/networks/src/main/java/com/tangem/domain/networks/repository/NetworksRepository.kt b/domain/networks/src/main/java/com/tangem/domain/networks/repository/NetworksRepository.kt new file mode 100644 index 0000000000..c828d3f38a --- /dev/null +++ b/domain/networks/src/main/java/com/tangem/domain/networks/repository/NetworksRepository.kt @@ -0,0 +1,24 @@ +package com.tangem.domain.networks.repository + +import com.tangem.domain.tokens.model.CryptoCurrencyAddress +import com.tangem.domain.tokens.model.Network +import com.tangem.domain.wallets.models.UserWalletId + +/** + * Repository for working with pending transactions + * +[REDACTED_AUTHOR] + */ +interface NetworksRepository { + + /** Fetches pending transactions for given [network] in selected [userWalletId] */ + suspend fun fetchPendingTransactions(userWalletId: UserWalletId, network: Network) + + /** + * Returns addresses and crypto currency + * + * @param userWalletId the unique identifier of the user wallet + * @param network network + */ + suspend fun getNetworkAddresses(userWalletId: UserWalletId, network: Network): List +} \ No newline at end of file diff --git a/gradle/dependencies.toml b/gradle/dependencies.toml index 2353d61b82..ef9fae7586 100644 --- a/gradle/dependencies.toml +++ b/gradle/dependencies.toml @@ -100,6 +100,7 @@ detekt = "1.22.0" espresso = "3.5.1" espresso-intents = "3.5.1" junit = "4.13.2" +junit5 = "5.8.2" junitAndroidExt = "1.1.5" mockk = "1.13.4" turbine = "1.2.0" @@ -197,6 +198,8 @@ test-coroutine = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-test", ver test-espresso = { module = "androidx.test.espresso:espresso-core", version.ref = "espresso" } test-espresso-intents = { module = "androidx.test.espresso:espresso-intents", version.ref = "espresso-intents" } test-junit = { module = "junit:junit", version.ref = "junit" } +test-junit5 = { module = "org.junit.jupiter:junit-jupiter", version.ref = "junit5" } +test-junit5-engine = { module = "org.junit.jupiter:junit-jupiter-engine", version.ref = "junit5" } test-junit-android = { module = "androidx.test.ext:junit", version.ref = "junitAndroidExt" } test-truth = { module = "com.google.truth:truth", version.ref = "truth" } test-mockk = { module = "io.mockk:mockk", version.ref = "mockk" } From 66efd406cc58508bb80a77f311d0b477cb1cb418 Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 15 May 2025 13:23:49 +0300 Subject: [PATCH 035/165] Updated on 2026-08-14 --- .../common/analytics/events/AnalyticsParam.kt | 1 - .../amplitude/AmplitudeAnalyticsHandler.kt | 11 +++++++- .../handlers/amplitude/AmplitudeClient.kt | 11 +++++++- .../handlers/amplitude/AmplitudeLogClient.kt | 10 ++++++++ .../firebase/FirebaseAnalyticsHandler.kt | 11 +++++++- .../handlers/firebase/FirebaseClient.kt | 11 +++++++- .../handlers/firebase/FirebaseLogClient.kt | 9 +++++++ .../CardContextInterceptor.kt | 3 --- .../tangem/tap/common/extensions/Analytics.kt | 12 +++++++++ core/analytics/build.gradle.kts | 3 +++ .../com/tangem/core/analytics/Analytics.kt | 25 ++++++++++++++++++- .../core/analytics/api/EventHandlerApi.kt | 5 ++++ .../tangem/core/analytics/api/UserIdHolder.kt | 8 ++++++ 13 files changed, 111 insertions(+), 9 deletions(-) create mode 100644 core/analytics/src/main/java/com/tangem/core/analytics/api/UserIdHolder.kt diff --git a/app/src/main/java/com/tangem/tap/common/analytics/events/AnalyticsParam.kt b/app/src/main/java/com/tangem/tap/common/analytics/events/AnalyticsParam.kt index fe2add9b71..a90e193a92 100644 --- a/app/src/main/java/com/tangem/tap/common/analytics/events/AnalyticsParam.kt +++ b/app/src/main/java/com/tangem/tap/common/analytics/events/AnalyticsParam.kt @@ -104,7 +104,6 @@ sealed class AnalyticsParam { const val PERMISSION_TYPE = "Permission Type" const val PRODUCT_TYPE = "Product Type" const val FIRMWARE = "Firmware" - const val USER_WALLET_ID = "User Wallet ID" const val CURRENCY = "Currency" const val ERROR_DESCRIPTION = "Error Description" const val ERROR_CODE = "Error Code" diff --git a/app/src/main/java/com/tangem/tap/common/analytics/handlers/amplitude/AmplitudeAnalyticsHandler.kt b/app/src/main/java/com/tangem/tap/common/analytics/handlers/amplitude/AmplitudeAnalyticsHandler.kt index 2aa36c5413..d28504b612 100644 --- a/app/src/main/java/com/tangem/tap/common/analytics/handlers/amplitude/AmplitudeAnalyticsHandler.kt +++ b/app/src/main/java/com/tangem/tap/common/analytics/handlers/amplitude/AmplitudeAnalyticsHandler.kt @@ -1,14 +1,23 @@ package com.tangem.tap.common.analytics.handlers.amplitude import com.tangem.core.analytics.api.AnalyticsHandler +import com.tangem.core.analytics.api.AnalyticsUserIdHandler import com.tangem.tap.common.analytics.api.AnalyticsHandlerBuilder class AmplitudeAnalyticsHandler( private val client: AmplitudeAnalyticsClient, -) : AnalyticsHandler { +) : AnalyticsHandler, AnalyticsUserIdHandler { override fun id(): String = ID + override fun setUserId(userId: String) { + client.setUserId(userId) + } + + override fun clearUserId() { + client.clearUserId() + } + override fun send(eventId: String, params: Map) { client.logEvent(eventId, params) } diff --git a/app/src/main/java/com/tangem/tap/common/analytics/handlers/amplitude/AmplitudeClient.kt b/app/src/main/java/com/tangem/tap/common/analytics/handlers/amplitude/AmplitudeClient.kt index 74cec2eaf2..a5ffec919c 100644 --- a/app/src/main/java/com/tangem/tap/common/analytics/handlers/amplitude/AmplitudeClient.kt +++ b/app/src/main/java/com/tangem/tap/common/analytics/handlers/amplitude/AmplitudeClient.kt @@ -4,13 +4,14 @@ import android.app.Application import com.amplitude.api.Amplitude import com.amplitude.api.AmplitudeClient import com.tangem.core.analytics.api.EventLogger +import com.tangem.core.analytics.api.UserIdHolder import com.tangem.utils.converter.Converter import org.json.JSONObject /** [REDACTED_AUTHOR] */ -interface AmplitudeAnalyticsClient : EventLogger +interface AmplitudeAnalyticsClient : EventLogger, UserIdHolder internal class AmplitudeClient( application: Application, @@ -24,6 +25,14 @@ internal class AmplitudeClient( client.enableForegroundTracking(application) } + override fun setUserId(userId: String) { + client.setUserId(userId) + } + + override fun clearUserId() { + client.setUserId(null) + } + override fun logEvent(event: String, params: Map) { client.logEvent(event, ParamsToJSONObjectConverter().convert(params)) } diff --git a/app/src/main/java/com/tangem/tap/common/analytics/handlers/amplitude/AmplitudeLogClient.kt b/app/src/main/java/com/tangem/tap/common/analytics/handlers/amplitude/AmplitudeLogClient.kt index f60d2edfcf..f24a5925a3 100644 --- a/app/src/main/java/com/tangem/tap/common/analytics/handlers/amplitude/AmplitudeLogClient.kt +++ b/app/src/main/java/com/tangem/tap/common/analytics/handlers/amplitude/AmplitudeLogClient.kt @@ -12,6 +12,16 @@ internal class AmplitudeLogClient( private val logger: AnalyticsEventsLogger = AnalyticsEventsLogger(AmplitudeAnalyticsHandler.ID, jsonConverter) + private var userId: String? = null + + override fun setUserId(userId: String) { + this.userId = userId + } + + override fun clearUserId() { + this.userId = null + } + override fun logEvent(event: String, params: Map) { logger.logEvent(event, params) } diff --git a/app/src/main/java/com/tangem/tap/common/analytics/handlers/firebase/FirebaseAnalyticsHandler.kt b/app/src/main/java/com/tangem/tap/common/analytics/handlers/firebase/FirebaseAnalyticsHandler.kt index 298874dcef..fbea91a9f4 100644 --- a/app/src/main/java/com/tangem/tap/common/analytics/handlers/firebase/FirebaseAnalyticsHandler.kt +++ b/app/src/main/java/com/tangem/tap/common/analytics/handlers/firebase/FirebaseAnalyticsHandler.kt @@ -3,6 +3,7 @@ package com.tangem.tap.common.analytics.handlers.firebase import com.tangem.core.analytics.api.AnalyticsErrorHandler import com.tangem.core.analytics.api.AnalyticsHandler import com.tangem.core.analytics.api.AnalyticsExceptionHandler +import com.tangem.core.analytics.api.AnalyticsUserIdHandler import com.tangem.core.analytics.models.AnalyticsEvent import com.tangem.core.analytics.models.ExceptionAnalyticsEvent import com.tangem.tap.common.analytics.api.AnalyticsHandlerBuilder @@ -10,12 +11,20 @@ import com.tangem.tap.common.analytics.converters.AnalyticsErrorConverter class FirebaseAnalyticsHandler( private val client: FirebaseAnalyticsClient, -) : AnalyticsHandler, AnalyticsErrorHandler, AnalyticsExceptionHandler { +) : AnalyticsHandler, AnalyticsErrorHandler, AnalyticsExceptionHandler, AnalyticsUserIdHandler { private val errorConverter = AnalyticsErrorConverter() override fun id(): String = ID + override fun setUserId(userId: String) { + client.setUserId(userId) + } + + override fun clearUserId() { + client.clearUserId() + } + override fun send(eventId: String, params: Map) { client.logEvent(eventId, params) } diff --git a/app/src/main/java/com/tangem/tap/common/analytics/handlers/firebase/FirebaseClient.kt b/app/src/main/java/com/tangem/tap/common/analytics/handlers/firebase/FirebaseClient.kt index 06ccf50709..6268ebf230 100644 --- a/app/src/main/java/com/tangem/tap/common/analytics/handlers/firebase/FirebaseClient.kt +++ b/app/src/main/java/com/tangem/tap/common/analytics/handlers/firebase/FirebaseClient.kt @@ -8,11 +8,12 @@ import com.google.firebase.crashlytics.recordException import com.google.firebase.ktx.Firebase import com.tangem.core.analytics.api.ExceptionLogger import com.tangem.core.analytics.api.EventLogger +import com.tangem.core.analytics.api.UserIdHolder /** [REDACTED_AUTHOR] */ -interface FirebaseAnalyticsClient : EventLogger, ExceptionLogger +interface FirebaseAnalyticsClient : EventLogger, ExceptionLogger, UserIdHolder internal class FirebaseClient : FirebaseAnalyticsClient { @@ -21,6 +22,14 @@ internal class FirebaseClient : FirebaseAnalyticsClient { private val eventConverter = FirebaseAnalyticsEventConverter() + override fun setUserId(userId: String) { + Firebase.analytics.setUserId(userId) + } + + override fun clearUserId() { + Firebase.analytics.setUserId(null) + } + override fun logEvent(event: String, params: Map) { fbAnalytics.logEvent( eventConverter.convertEventName(event), diff --git a/app/src/main/java/com/tangem/tap/common/analytics/handlers/firebase/FirebaseLogClient.kt b/app/src/main/java/com/tangem/tap/common/analytics/handlers/firebase/FirebaseLogClient.kt index 4b818bdf51..391b7e7470 100644 --- a/app/src/main/java/com/tangem/tap/common/analytics/handlers/firebase/FirebaseLogClient.kt +++ b/app/src/main/java/com/tangem/tap/common/analytics/handlers/firebase/FirebaseLogClient.kt @@ -11,6 +11,15 @@ internal class FirebaseLogClient( ) : FirebaseAnalyticsClient { private val logger: AnalyticsEventsLogger = AnalyticsEventsLogger(FirebaseAnalyticsHandler.ID, jsonConverter) + private var userId: String? = null + + override fun setUserId(userId: String) { + this.userId = userId + } + + override fun clearUserId() { + this.userId = null + } override fun logEvent(event: String, params: Map) { logger.logEvent(event, params) diff --git a/app/src/main/java/com/tangem/tap/common/analytics/paramsInterceptor/CardContextInterceptor.kt b/app/src/main/java/com/tangem/tap/common/analytics/paramsInterceptor/CardContextInterceptor.kt index 42a7f46c75..f0f3326917 100644 --- a/app/src/main/java/com/tangem/tap/common/analytics/paramsInterceptor/CardContextInterceptor.kt +++ b/app/src/main/java/com/tangem/tap/common/analytics/paramsInterceptor/CardContextInterceptor.kt @@ -40,9 +40,6 @@ class CardContextInterceptor( params[AnalyticsParam.BATCH] = card.batchId params[AnalyticsParam.PRODUCT_TYPE] = getProductType() params[AnalyticsParam.FIRMWARE] = card.firmwareVersion.stringValue - if (userWalletId != null) { - params[AnalyticsParam.USER_WALLET_ID] = userWalletId.stringValue - } ParamCardCurrencyConverter().convert(scanResponse.cardTypesResolver)?.let { params[AnalyticsParam.CURRENCY] = it.value diff --git a/app/src/main/java/com/tangem/tap/common/extensions/Analytics.kt b/app/src/main/java/com/tangem/tap/common/extensions/Analytics.kt index 8e79d07381..d8dc74443d 100644 --- a/app/src/main/java/com/tangem/tap/common/extensions/Analytics.kt +++ b/app/src/main/java/com/tangem/tap/common/extensions/Analytics.kt @@ -2,6 +2,7 @@ package com.tangem.tap.common.extensions import com.tangem.core.analytics.Analytics import com.tangem.domain.models.scan.ScanResponse +import com.tangem.domain.wallets.builder.UserWalletIdBuilder import com.tangem.tap.common.analytics.paramsInterceptor.LinkedCardContextInterceptor /** @@ -12,6 +13,11 @@ import com.tangem.tap.common.analytics.paramsInterceptor.LinkedCardContextInterc * Sets the new context */ fun Analytics.setContext(scanResponse: ScanResponse) { + val userWalletId = UserWalletIdBuilder.scanResponse(scanResponse).build() + if (userWalletId != null) { + setUserId(userWalletId.stringValue) + } + addParamsInterceptor(LinkedCardContextInterceptor(scanResponse)) } @@ -19,6 +25,7 @@ fun Analytics.setContext(scanResponse: ScanResponse) { * Erases the context */ fun Analytics.eraseContext() { + clearUserId() removeParamsInterceptor(LinkedCardContextInterceptor.id()) } @@ -26,6 +33,11 @@ fun Analytics.eraseContext() { * Adds a new context and keeps a previous context as the parent of the new one */ fun Analytics.addContext(scanResponse: ScanResponse) { + val userWalletId = UserWalletIdBuilder.scanResponse(scanResponse).build() + if (userWalletId != null) { + setUserId(userWalletId.stringValue) + } + val currentContext = removeParamsInterceptor(LinkedCardContextInterceptor.id()) as? LinkedCardContextInterceptor val newContext = LinkedCardContextInterceptor(scanResponse, parent = currentContext) diff --git a/core/analytics/build.gradle.kts b/core/analytics/build.gradle.kts index 99c29cd55d..819db765b9 100644 --- a/core/analytics/build.gradle.kts +++ b/core/analytics/build.gradle.kts @@ -22,4 +22,7 @@ dependencies { /** Core shouldn't depend on core, but in case with utils and logging its necessary */ implementation(projects.core.utils) + + /** For calculating user id hash */ + implementation(tangemDeps.card.core) } \ No newline at end of file diff --git a/core/analytics/src/main/java/com/tangem/core/analytics/Analytics.kt b/core/analytics/src/main/java/com/tangem/core/analytics/Analytics.kt index 081a583c06..57633c428a 100644 --- a/core/analytics/src/main/java/com/tangem/core/analytics/Analytics.kt +++ b/core/analytics/src/main/java/com/tangem/core/analytics/Analytics.kt @@ -1,5 +1,7 @@ package com.tangem.core.analytics +import com.tangem.common.extensions.calculateSha256 +import com.tangem.common.extensions.toHexString import com.tangem.core.analytics.api.* import com.tangem.core.analytics.models.AnalyticsEvent import com.tangem.core.analytics.models.ExceptionAnalyticsEvent @@ -19,7 +21,8 @@ interface GlobalAnalyticsEventHandler : AnalyticsFilterHolder, ParamsInterceptorHolder, AnalyticsErrorHandler, - AnalyticsExceptionHandler + AnalyticsExceptionHandler, + AnalyticsUserIdHandler object Analytics : GlobalAnalyticsEventHandler { @@ -57,6 +60,26 @@ object Analytics : GlobalAnalyticsEventHandler { return paramsInterceptors.remove(interceptorId) } + override fun setUserId(userId: String) { + analyticsScope.launch { + val userIdHash = userId.calculateSha256().toHexString() + + analyticsMutex.withLock { + analyticsHandlers.filterIsInstance() + .forEach { handler -> handler.setUserId(userIdHash) } + } + } + } + + override fun clearUserId() { + analyticsScope.launch { + analyticsMutex.withLock { + analyticsHandlers.filterIsInstance() + .forEach { handler -> handler.clearUserId() } + } + } + } + override fun send(event: AnalyticsEvent) { analyticsScope.launch { event.params = applyParamsInterceptors(event) diff --git a/core/analytics/src/main/java/com/tangem/core/analytics/api/EventHandlerApi.kt b/core/analytics/src/main/java/com/tangem/core/analytics/api/EventHandlerApi.kt index 6c98379d6d..08e59781a0 100644 --- a/core/analytics/src/main/java/com/tangem/core/analytics/api/EventHandlerApi.kt +++ b/core/analytics/src/main/java/com/tangem/core/analytics/api/EventHandlerApi.kt @@ -18,6 +18,11 @@ interface AnalyticsExceptionHandler { fun sendException(event: ExceptionAnalyticsEvent) } +interface AnalyticsUserIdHandler { + fun setUserId(userId: String) + fun clearUserId() +} + interface AnalyticsHandler : AnalyticsEventHandler { fun id(): String diff --git a/core/analytics/src/main/java/com/tangem/core/analytics/api/UserIdHolder.kt b/core/analytics/src/main/java/com/tangem/core/analytics/api/UserIdHolder.kt new file mode 100644 index 0000000000..50a3af361e --- /dev/null +++ b/core/analytics/src/main/java/com/tangem/core/analytics/api/UserIdHolder.kt @@ -0,0 +1,8 @@ +package com.tangem.core.analytics.api + +interface UserIdHolder { + + fun setUserId(userId: String) + + fun clearUserId() +} \ No newline at end of file From 90fea21f1741b5efe471a9033f91c4201489e4e6 Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 14 May 2025 19:54:34 +0400 Subject: [PATCH 036/165] Updated on 2026-08-14 --- .../tangem/tap/di/domain/NFTDomainModule.kt | 11 +- .../tap/di/domain/TokensDomainModule.kt | 6 +- .../data/networks/di/NetworkDataModule.kt | 51 ++++ .../di/NetworkStatusSupplierModule.kt | 19 -- .../tangem/data/tokens/di/TokensDataModule.kt | 35 +-- .../repository/DefaultNetworksRepository.kt | 260 ------------------ domain/nft/build.gradle.kts | 15 +- .../domain/nft/GetNFTNetworkStatusUseCase.kt | 15 +- .../tokens/FetchPendingTransactionsUseCase.kt | 10 +- .../tokens/GetNetworkAddressesUseCase.kt | 4 +- .../tokens/repository/NetworksRepository.kt | 66 ----- .../send/v2/common/SendBalanceUpdater.kt | 2 +- .../send/impl/presentation/model/SendModel.kt | 5 +- .../state/helpers/StakingBalanceUpdater.kt | 2 +- 14 files changed, 103 insertions(+), 398 deletions(-) create mode 100644 data/networks/src/main/java/com/tangem/data/networks/di/NetworkDataModule.kt delete mode 100644 data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultNetworksRepository.kt delete mode 100644 domain/tokens/src/main/kotlin/com/tangem/domain/tokens/repository/NetworksRepository.kt diff --git a/app/src/main/java/com/tangem/tap/di/domain/NFTDomainModule.kt b/app/src/main/java/com/tangem/tap/di/domain/NFTDomainModule.kt index 1a79b6209b..def43853a4 100644 --- a/app/src/main/java/com/tangem/tap/di/domain/NFTDomainModule.kt +++ b/app/src/main/java/com/tangem/tap/di/domain/NFTDomainModule.kt @@ -1,11 +1,11 @@ package com.tangem.tap.di.domain +import com.tangem.domain.networks.single.SingleNetworkStatusSupplier import com.tangem.domain.nft.* import com.tangem.domain.nft.repository.NFTRepository import com.tangem.domain.quotes.single.SingleQuoteFetcher import com.tangem.domain.quotes.single.SingleQuoteSupplier import com.tangem.domain.tokens.repository.CurrenciesRepository -import com.tangem.domain.tokens.repository.NetworksRepository import com.tangem.utils.coroutines.CoroutineDispatcherProvider import dagger.Module import dagger.Provides @@ -74,10 +74,11 @@ internal object NFTDomainModule { @Provides @Singleton - fun providesGetNFTNetworkStatusUseCase(networksRepository: NetworksRepository): GetNFTNetworkStatusUseCase = - GetNFTNetworkStatusUseCase( - networksRepository = networksRepository, - ) + fun providesGetNFTNetworkStatusUseCase( + singleNetworkStatusSupplier: SingleNetworkStatusSupplier, + ): GetNFTNetworkStatusUseCase { + return GetNFTNetworkStatusUseCase(singleNetworkStatusSupplier = singleNetworkStatusSupplier) + } @Provides @Singleton diff --git a/app/src/main/java/com/tangem/tap/di/domain/TokensDomainModule.kt b/app/src/main/java/com/tangem/tap/di/domain/TokensDomainModule.kt index f1b9bdeabc..c678251ad9 100644 --- a/app/src/main/java/com/tangem/tap/di/domain/TokensDomainModule.kt +++ b/app/src/main/java/com/tangem/tap/di/domain/TokensDomainModule.kt @@ -4,6 +4,7 @@ import com.tangem.core.configtoggle.feature.FeatureTogglesManager import com.tangem.domain.exchange.RampStateManager import com.tangem.domain.networks.multi.MultiNetworkStatusFetcher import com.tangem.domain.networks.multi.MultiNetworkStatusSupplier +import com.tangem.domain.networks.repository.NetworksRepository import com.tangem.domain.networks.single.SingleNetworkStatusFetcher import com.tangem.domain.networks.single.SingleNetworkStatusSupplier import com.tangem.domain.promo.PromoRepository @@ -18,7 +19,10 @@ import com.tangem.domain.tokens.* import com.tangem.domain.tokens.operations.BaseCurrenciesStatusesOperations import com.tangem.domain.tokens.operations.BaseCurrencyStatusOperations import com.tangem.domain.tokens.operations.CachedCurrenciesStatusesOperations -import com.tangem.domain.tokens.repository.* +import com.tangem.domain.tokens.repository.CurrenciesRepository +import com.tangem.domain.tokens.repository.CurrencyChecksRepository +import com.tangem.domain.tokens.repository.PolkadotAccountHealthCheckRepository +import com.tangem.domain.tokens.repository.QuotesRepository import com.tangem.domain.walletmanager.WalletManagersFacade import com.tangem.tap.domain.tokens.DefaultTokensFeatureToggles import com.tangem.utils.coroutines.CoroutineDispatcherProvider diff --git a/data/networks/src/main/java/com/tangem/data/networks/di/NetworkDataModule.kt b/data/networks/src/main/java/com/tangem/data/networks/di/NetworkDataModule.kt new file mode 100644 index 0000000000..368dad5d0e --- /dev/null +++ b/data/networks/src/main/java/com/tangem/data/networks/di/NetworkDataModule.kt @@ -0,0 +1,51 @@ +package com.tangem.data.networks.di + +import androidx.datastore.core.DataStore +import com.tangem.data.common.currency.CardCryptoCurrencyFactory +import com.tangem.data.networks.repository.DefaultNetworksRepository +import com.tangem.data.networks.store.DefaultNetworksStatusesStoreV2 +import com.tangem.data.networks.store.NetworksStatusesStoreV2 +import com.tangem.datasource.local.datastore.RuntimeSharedStore +import com.tangem.datasource.local.network.entity.NetworkStatusDM +import com.tangem.domain.networks.repository.NetworksRepository +import com.tangem.domain.walletmanager.WalletManagersFacade +import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent +import javax.inject.Singleton + +@Module +@InstallIn(SingletonComponent::class) +internal object NetworkDataModule { + + @Provides + @Singleton + fun provideNetworksStatusesStoreV2( + persistenceNetworksStatusesStore: DataStore>>, + dispatchers: CoroutineDispatcherProvider, + ): NetworksStatusesStoreV2 { + return DefaultNetworksStatusesStoreV2( + runtimeStore = RuntimeSharedStore(), + persistenceDataStore = persistenceNetworksStatusesStore, + dispatchers = dispatchers, + ) + } + + @Provides + @Singleton + fun provideNetworkRepository( + cardCryptoCurrencyFactory: CardCryptoCurrencyFactory, + walletManagersFacade: WalletManagersFacade, + networksStatusesStoreV2: NetworksStatusesStoreV2, + dispatchers: CoroutineDispatcherProvider, + ): NetworksRepository { + return DefaultNetworksRepository( + cardCryptoCurrencyFactory = cardCryptoCurrencyFactory, + walletManagersFacade = walletManagersFacade, + networksStatusesStore = networksStatusesStoreV2, + dispatchers = dispatchers, + ) + } +} \ No newline at end of file diff --git a/data/networks/src/main/java/com/tangem/data/networks/di/NetworkStatusSupplierModule.kt b/data/networks/src/main/java/com/tangem/data/networks/di/NetworkStatusSupplierModule.kt index aea068fa04..7532f3b080 100644 --- a/data/networks/src/main/java/com/tangem/data/networks/di/NetworkStatusSupplierModule.kt +++ b/data/networks/src/main/java/com/tangem/data/networks/di/NetworkStatusSupplierModule.kt @@ -1,15 +1,9 @@ package com.tangem.data.networks.di -import androidx.datastore.core.DataStore -import com.tangem.data.networks.store.DefaultNetworksStatusesStoreV2 -import com.tangem.data.networks.store.NetworksStatusesStoreV2 -import com.tangem.datasource.local.datastore.RuntimeSharedStore -import com.tangem.datasource.local.network.entity.NetworkStatusDM import com.tangem.domain.networks.multi.MultiNetworkStatusProducer import com.tangem.domain.networks.multi.MultiNetworkStatusSupplier import com.tangem.domain.networks.single.SingleNetworkStatusProducer import com.tangem.domain.networks.single.SingleNetworkStatusSupplier -import com.tangem.utils.coroutines.CoroutineDispatcherProvider import dagger.Module import dagger.Provides import dagger.hilt.InstallIn @@ -20,19 +14,6 @@ import javax.inject.Singleton @InstallIn(SingletonComponent::class) internal object NetworkStatusSupplierModule { - @Provides - @Singleton - fun provideNetworksStatusesStoreV2( - persistenceNetworksStatusesStore: DataStore>>, - dispatchers: CoroutineDispatcherProvider, - ): NetworksStatusesStoreV2 { - return DefaultNetworksStatusesStoreV2( - runtimeStore = RuntimeSharedStore(), - persistenceDataStore = persistenceNetworksStatusesStore, - dispatchers = dispatchers, - ) - } - @Provides @Singleton fun provideSingleNetworkStatusSupplier(factory: SingleNetworkStatusProducer.Factory): SingleNetworkStatusSupplier { diff --git a/data/tokens/src/main/kotlin/com/tangem/data/tokens/di/TokensDataModule.kt b/data/tokens/src/main/kotlin/com/tangem/data/tokens/di/TokensDataModule.kt index af3cf30f5f..3f3df8bc0f 100644 --- a/data/tokens/src/main/kotlin/com/tangem/data/tokens/di/TokensDataModule.kt +++ b/data/tokens/src/main/kotlin/com/tangem/data/tokens/di/TokensDataModule.kt @@ -3,14 +3,19 @@ package com.tangem.data.tokens.di import com.tangem.blockchainsdk.utils.ExcludedBlockchains import com.tangem.data.common.cache.CacheRegistry import com.tangem.data.common.currency.CardCryptoCurrencyFactory -import com.tangem.data.tokens.repository.* +import com.tangem.data.tokens.repository.DefaultCurrenciesRepository +import com.tangem.data.tokens.repository.DefaultCurrencyChecksRepository +import com.tangem.data.tokens.repository.DefaultPolkadotAccountHealthCheckRepository +import com.tangem.data.tokens.repository.DefaultQuotesRepository import com.tangem.datasource.api.tangemTech.TangemTechApi import com.tangem.datasource.exchangeservice.swap.ExpressServiceLoader -import com.tangem.datasource.local.network.NetworksStatusesStore import com.tangem.datasource.local.preferences.AppPreferencesStore import com.tangem.datasource.local.quote.QuotesStore import com.tangem.datasource.local.userwallet.UserWalletsStore -import com.tangem.domain.tokens.repository.* +import com.tangem.domain.tokens.repository.CurrenciesRepository +import com.tangem.domain.tokens.repository.CurrencyChecksRepository +import com.tangem.domain.tokens.repository.PolkadotAccountHealthCheckRepository +import com.tangem.domain.tokens.repository.QuotesRepository import com.tangem.domain.walletmanager.WalletManagersFacade import com.tangem.utils.coroutines.CoroutineDispatcherProvider import dagger.Module @@ -67,30 +72,6 @@ internal object TokensDataModule { ) } - @Provides - @Singleton - fun provideNetworksRepository( - networksStatusesStore: NetworksStatusesStore, - walletManagersFacade: WalletManagersFacade, - userWalletsStore: UserWalletsStore, - appPreferencesStore: AppPreferencesStore, - cacheRegistry: CacheRegistry, - dispatchers: CoroutineDispatcherProvider, - excludedBlockchains: ExcludedBlockchains, - cardCryptoCurrencyFactory: CardCryptoCurrencyFactory, - ): NetworksRepository { - return DefaultNetworksRepository( - networksStatusesStore = networksStatusesStore, - walletManagersFacade = walletManagersFacade, - userWalletsStore = userWalletsStore, - appPreferencesStore = appPreferencesStore, - cacheRegistry = cacheRegistry, - dispatchers = dispatchers, - excludedBlockchains = excludedBlockchains, - cardCryptoCurrencyFactory = cardCryptoCurrencyFactory, - ) - } - @Provides @Singleton fun provideCurrencyChecksRepository( diff --git a/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultNetworksRepository.kt b/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultNetworksRepository.kt deleted file mode 100644 index 46ee114672..0000000000 --- a/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultNetworksRepository.kt +++ /dev/null @@ -1,260 +0,0 @@ -package com.tangem.data.tokens.repository - -import com.tangem.blockchain.common.address.AddressType -import com.tangem.blockchainsdk.utils.ExcludedBlockchains -import com.tangem.data.common.cache.CacheRegistry -import com.tangem.data.common.currency.CardCryptoCurrencyFactory -import com.tangem.data.common.currency.ResponseCryptoCurrenciesFactory -import com.tangem.data.networks.utils.NetworkStatusFactory -import com.tangem.datasource.api.tangemTech.models.UserTokensResponse -import com.tangem.datasource.local.network.NetworksStatusesStore -import com.tangem.datasource.local.preferences.AppPreferencesStore -import com.tangem.datasource.local.preferences.PreferencesKeys -import com.tangem.datasource.local.preferences.utils.getObjectSyncOrNull -import com.tangem.datasource.local.userwallet.UserWalletsStore -import com.tangem.domain.common.util.cardTypesResolver -import com.tangem.domain.tokens.model.CryptoCurrency -import com.tangem.domain.tokens.model.CryptoCurrencyAddress -import com.tangem.domain.tokens.model.Network -import com.tangem.domain.tokens.model.NetworkStatus -import com.tangem.domain.tokens.repository.NetworksRepository -import com.tangem.domain.walletmanager.WalletManagersFacade -import com.tangem.domain.walletmanager.model.UpdateWalletManagerResult -import com.tangem.domain.wallets.models.UserWalletId -import com.tangem.utils.coroutines.CoroutineDispatcherProvider -import kotlinx.coroutines.* -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.distinctUntilChanged -import kotlinx.coroutines.flow.flowOn -import timber.log.Timber - -@Suppress("LongParameterList") -internal class DefaultNetworksRepository( - private val networksStatusesStore: NetworksStatusesStore, - private val walletManagersFacade: WalletManagersFacade, - private val userWalletsStore: UserWalletsStore, - private val appPreferencesStore: AppPreferencesStore, - private val cacheRegistry: CacheRegistry, - private val cardCryptoCurrencyFactory: CardCryptoCurrencyFactory, - private val dispatchers: CoroutineDispatcherProvider, - excludedBlockchains: ExcludedBlockchains, -) : NetworksRepository { - - private val responseCurrenciesFactory = ResponseCryptoCurrenciesFactory(excludedBlockchains) - - override fun getNetworkStatusesUpdates( - userWalletId: UserWalletId, - networks: Set, - ): Flow> { - return networksStatusesStore.get(userWalletId, networks) - .distinctUntilChanged() - .flowOn(dispatchers.io) - } - - override suspend fun fetchNetworkStatuses(userWalletId: UserWalletId, networks: Set, refresh: Boolean) { - withContext(dispatchers.io) { - fetchNetworksStatusesIfCacheExpired(userWalletId, networks, refresh) - } - } - - override suspend fun fetchNetworkPendingTransactions(userWalletId: UserWalletId, networks: Set) { - val currencies = getCurrencies(userWalletId, networks) - withContext(dispatchers.io) { - fetchNetworksPendingTransactions(userWalletId, networks, currencies) - } - } - - override suspend fun getNetworkStatusesSync( - userWalletId: UserWalletId, - networks: Set, - refresh: Boolean, - ): Set = withContext(dispatchers.io) { - fetchNetworksStatusesIfCacheExpired(userWalletId, networks, refresh) - networksStatusesStore.getSyncOrNull(userWalletId).orEmpty() - } - - override suspend fun getNetworkAddresses( - userWalletId: UserWalletId, - network: Network, - ): List = withContext(dispatchers.io) { - // Get list of currencies matching [network] - val currencies = getCurrencies(userWalletId) - .filter { currency -> network.id == currency.network.id } - - // There is no currencies matching given [networks] in [userWalletId] - if (currencies.toList().isEmpty()) return@withContext emptyList() - - currencies.toList().map { currency -> - CryptoCurrencyAddress( - cryptoCurrency = currency, - address = walletManagersFacade.getAddresses(userWalletId, currency.network) - .firstOrNull { it.type == AddressType.Default } - ?.value.orEmpty(), - ) - } - } - - private suspend fun fetchNetworksStatusesIfCacheExpired( - userWalletId: UserWalletId, - networks: Set, - refresh: Boolean, - ) = coroutineScope { - if (refresh) { - networksStatusesStore.refresh(key = userWalletId, networks = networks) - } - - val currencies = getCurrencies(userWalletId, networks) - val networksDeferred = networks.mapNotNull { network -> - coroutineScope { - val key = getNetworksStatusesCacheKey(userWalletId, network) - - if (refresh || cacheRegistry.isExpired(key)) { - async { - cacheRegistry.invokeOnExpire( - key = key, - skipCache = refresh, - block = { fetchNetworkStatus(userWalletId, network, currencies) }, - ) - } - } else { - null - } - } - } - - networksDeferred.awaitAll() - } - - private suspend fun fetchNetworksPendingTransactions( - userWalletId: UserWalletId, - networks: Set, - currencies: Sequence, - ) { - coroutineScope { - networks - .map { network -> - async { - fetchNetworkPendingTransactions(userWalletId, network, currencies) - } - } - .awaitAll() - } - } - - private suspend fun fetchNetworkStatus( - userWalletId: UserWalletId, - network: Network, - currencies: Sequence, - ) { - val networkCurrencies = currencies.filter { it.network == network } - - val result = walletManagersFacade.update( - userWalletId = userWalletId, - network = network, - extraTokens = networkCurrencies - .filterIsInstance() - .toSet(), - ) - - withContext(NonCancellable) { - invalidateCacheKeyIfNeeded(userWalletId, network, result) - } - - val networkStatus = NetworkStatusFactory.create( - network = network, - updatingResult = result, - addedCurrencies = networkCurrencies.toSet(), - ) - - networksStatusesStore.store(userWalletId, networkStatus) - } - - private suspend fun fetchNetworkPendingTransactions( - userWalletId: UserWalletId, - network: Network, - currencies: Sequence, - ) { - val result = walletManagersFacade.updatePendingTransactions( - userWalletId = userWalletId, - network = network, - ) - - withContext(NonCancellable) { - invalidateCacheKeyIfNeeded(userWalletId, network, result) - } - - val networkStatus = NetworkStatusFactory.create( - network = network, - updatingResult = result, - addedCurrencies = currencies.toSet(), - ) - - networksStatusesStore.store(userWalletId, networkStatus) - } - - private suspend fun getCurrencies(userWalletId: UserWalletId, networks: Set): Sequence { - val currencies = getCurrencies(userWalletId) - return currencies.filter { it.network in networks } - } - - private suspend fun getCurrencies(userWalletId: UserWalletId): Sequence { - val userWallet = requireNotNull(userWalletsStore.getSyncOrNull(userWalletId)) { - "Unable to find user wallet with provided ID: $userWalletId" - } - - return if (userWallet.isMultiCurrency) { - val response = requireNotNull( - value = appPreferencesStore.getObjectSyncOrNull( - key = PreferencesKeys.getUserTokensKey(userWalletId.stringValue), - ), - lazyMessage = { - "Unable to find tokens response for user wallet with provided ID: $userWalletId" - }, - ) - - responseCurrenciesFactory.createCurrencies(response, userWallet.scanResponse).asSequence() - } else { - if (userWallet.scanResponse.cardTypesResolver.isSingleWalletWithToken()) { - cardCryptoCurrencyFactory.createCurrenciesForSingleCurrencyCardWithToken( - scanResponse = userWallet.scanResponse, - ) - .asSequence() - } else { - val currency = cardCryptoCurrencyFactory.createPrimaryCurrencyForSingleCurrencyCard( - scanResponse = userWallet.scanResponse, - ) - - sequenceOf(currency) - } - } - } - - private suspend fun invalidateCacheKeyIfNeeded( - userWalletId: UserWalletId, - network: Network, - result: UpdateWalletManagerResult, - ) { - when (result) { - is UpdateWalletManagerResult.Verified, - is UpdateWalletManagerResult.NoAccount, - -> Unit - is UpdateWalletManagerResult.Unreachable, - is UpdateWalletManagerResult.MissedDerivation, - -> { - Timber.w( - """ - Invalidate network cache key - |- User wallet ID: $userWalletId - |- Network: ${network.id} - """.trimIndent(), - ) - - cacheRegistry.invalidate(getNetworksStatusesCacheKey(userWalletId, network)) - } - } - } - - private fun getNetworksStatusesCacheKey(userWalletId: UserWalletId, network: Network): String { - return "network_status_${userWalletId}_${network.id.value}_${network.derivationPath.value}" - } -} \ No newline at end of file diff --git a/domain/nft/build.gradle.kts b/domain/nft/build.gradle.kts index 28e807c797..6151f94e1b 100644 --- a/domain/nft/build.gradle.kts +++ b/domain/nft/build.gradle.kts @@ -10,17 +10,24 @@ android { } dependencies { - implementation(deps.arrow.core) - implementation(deps.kotlin.coroutines) - + // region Project – Core implementation(projects.core.analytics.models) implementation(projects.core.utils) + // endregion + // region Project – Domain implementation(projects.domain.core) implementation(projects.domain.models) + implementation(projects.domain.networks) implementation(projects.domain.nft.models) + implementation(projects.domain.quotes) implementation(projects.domain.tokens) implementation(projects.domain.tokens.models) implementation(projects.domain.wallets.models) - implementation(projects.domain.quotes) + // endregion + + // region Others + implementation(deps.arrow.core) + implementation(deps.kotlin.coroutines) + // endregion } \ No newline at end of file diff --git a/domain/nft/src/main/kotlin/com/tangem/domain/nft/GetNFTNetworkStatusUseCase.kt b/domain/nft/src/main/kotlin/com/tangem/domain/nft/GetNFTNetworkStatusUseCase.kt index 4d0771cde1..2d4a03d111 100644 --- a/domain/nft/src/main/kotlin/com/tangem/domain/nft/GetNFTNetworkStatusUseCase.kt +++ b/domain/nft/src/main/kotlin/com/tangem/domain/nft/GetNFTNetworkStatusUseCase.kt @@ -1,15 +1,20 @@ package com.tangem.domain.nft +import com.tangem.domain.networks.single.SingleNetworkStatusProducer +import com.tangem.domain.networks.single.SingleNetworkStatusSupplier import com.tangem.domain.tokens.model.Network import com.tangem.domain.tokens.model.NetworkStatus -import com.tangem.domain.tokens.repository.NetworksRepository import com.tangem.domain.wallets.models.UserWalletId +import kotlinx.coroutines.flow.firstOrNull class GetNFTNetworkStatusUseCase( - private val networksRepository: NetworksRepository, + private val singleNetworkStatusSupplier: SingleNetworkStatusSupplier, ) { - suspend operator fun invoke(userWalletId: UserWalletId, network: Network): NetworkStatus? = networksRepository - .getNetworkStatusesSync(userWalletId, setOf(network), false) - .firstOrNull { it.network == network } + suspend operator fun invoke(userWalletId: UserWalletId, network: Network): NetworkStatus? { + return singleNetworkStatusSupplier( + params = SingleNetworkStatusProducer.Params(userWalletId = userWalletId, network = network), + ) + .firstOrNull() + } } \ No newline at end of file diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/FetchPendingTransactionsUseCase.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/FetchPendingTransactionsUseCase.kt index aeda66d1f9..55609038af 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/FetchPendingTransactionsUseCase.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/FetchPendingTransactionsUseCase.kt @@ -1,9 +1,9 @@ package com.tangem.domain.tokens +import arrow.core.Either +import com.tangem.domain.networks.repository.NetworksRepository import com.tangem.domain.tokens.model.Network -import com.tangem.domain.tokens.repository.NetworksRepository import com.tangem.domain.wallets.models.UserWalletId -import kotlinx.coroutines.coroutineScope /** * Use case responsible for fetching current pending transactions @@ -14,9 +14,7 @@ class FetchPendingTransactionsUseCase( private val networksRepository: NetworksRepository, ) { - suspend operator fun invoke(userWalletId: UserWalletId, networks: Set) { - coroutineScope { - networksRepository.fetchNetworkPendingTransactions(userWalletId, networks) - } + suspend operator fun invoke(userWalletId: UserWalletId, network: Network) = Either.catch { + networksRepository.fetchPendingTransactions(userWalletId = userWalletId, network = network) } } \ No newline at end of file diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetNetworkAddressesUseCase.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetNetworkAddressesUseCase.kt index 654c263b3e..773d5f44e3 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetNetworkAddressesUseCase.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetNetworkAddressesUseCase.kt @@ -1,12 +1,12 @@ package com.tangem.domain.tokens +import com.tangem.domain.networks.repository.NetworksRepository import com.tangem.domain.tokens.model.CryptoCurrencyAddress import com.tangem.domain.tokens.model.Network -import com.tangem.domain.tokens.repository.NetworksRepository import com.tangem.domain.wallets.models.UserWalletId class GetNetworkAddressesUseCase( - internal val networksRepository: NetworksRepository, + private val networksRepository: NetworksRepository, ) { suspend fun invokeSync(userWalletId: UserWalletId, network: Network): List { diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/repository/NetworksRepository.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/repository/NetworksRepository.kt deleted file mode 100644 index 47f82bff87..0000000000 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/repository/NetworksRepository.kt +++ /dev/null @@ -1,66 +0,0 @@ -package com.tangem.domain.tokens.repository - -import com.tangem.domain.tokens.model.CryptoCurrencyAddress -import com.tangem.domain.tokens.model.Network -import com.tangem.domain.tokens.model.NetworkStatus -import com.tangem.domain.wallets.models.UserWalletId -import kotlinx.coroutines.flow.Flow - -/** - * Repository for everything related to the blockchain networks - * */ -interface NetworksRepository { - - /** - * Retrieves updates of network statuses of specified blockchain networks for a specific user wallet. - * - * To fetch and populate the cache with network statuses, use [fetchNetworkStatuses]. - * - * @param userWalletId The unique identifier of the user wallet. - * @param networks A set of network which statuses are to be retrieved. - * - * @return A [Flow] emitting a set of [NetworkStatus] objects corresponding to the specified networks. - * */ - fun getNetworkStatusesUpdates(userWalletId: UserWalletId, networks: Set): Flow> - - /** - * Fetches network statuses of specified blockchain networks for a specific user wallet. - * - * Fetched network statuses are stored in the local cache and can be retrieved by calling - * [getNetworkStatusesUpdates]. - * - * @param userWalletId The unique identifier of the user wallet. - * @param networks A set of network which statuses are to be retrieved. - * @param refresh A boolean flag indicating whether the data should be refreshed. Default is `false`. - * */ - suspend fun fetchNetworkStatuses(userWalletId: UserWalletId, networks: Set, refresh: Boolean = false) - - /** - * Fetches pending transactions for given network - * - * @param userWalletId The unique identifier of the user wallet. - * @param networks A set of network which statuses are to be retrieved. - */ - suspend fun fetchNetworkPendingTransactions(userWalletId: UserWalletId, networks: Set) - - /** - * Retrieves network statuses of specified blockchain networks for a specific user wallet. - * - * Loads remote network statuses if they have expired or if [refresh] is `true`. - * - * @param userWalletId The unique identifier of the user wallet. - * @param networks A set of network which statuses are to be retrieved. - * @param refresh A boolean flag indicating whether the data should be refreshed. - * @return A [Flow] emitting a set of [NetworkStatus] objects corresponding to the specified networks. - */ - suspend fun getNetworkStatusesSync( - userWalletId: UserWalletId, - networks: Set, - refresh: Boolean = false, - ): Set - - /** - * Returns list of addresses and crypto currency info of added currencies of [network] in selected wallet [userWalletId] - */ - suspend fun getNetworkAddresses(userWalletId: UserWalletId, network: Network): List -} \ No newline at end of file diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/common/SendBalanceUpdater.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/common/SendBalanceUpdater.kt index bb10f2b5d3..73a87992bd 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/common/SendBalanceUpdater.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/common/SendBalanceUpdater.kt @@ -33,7 +33,7 @@ internal class SendBalanceUpdater @AssistedInject constructor( async { fetchPendingTransactionsUseCase( userWalletId = userWallet.walletId, - networks = setOf(cryptoCurrency.network), + network = cryptoCurrency.network, ) }, // we should update tx history and network for new balances diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/model/SendModel.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/model/SendModel.kt index 02f0478ef5..7fe805ebb4 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/model/SendModel.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/model/SendModel.kt @@ -1037,7 +1037,10 @@ internal class SendModel @Inject constructor( listOf( // we should update network to find pending tx after 1 sec async { - fetchPendingTransactionsUseCase(userWallet.walletId, setOf(cryptoCurrency.network)) + fetchPendingTransactionsUseCase( + userWalletId = userWallet.walletId, + network = cryptoCurrency.network, + ) }, // we should update tx history and network for new balance async { diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/helpers/StakingBalanceUpdater.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/helpers/StakingBalanceUpdater.kt index 88d2974d5c..e3593afffa 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/helpers/StakingBalanceUpdater.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/helpers/StakingBalanceUpdater.kt @@ -42,7 +42,7 @@ internal class StakingBalanceUpdater @AssistedInject constructor( async { fetchPendingTransactionsUseCase( userWalletId = userWallet.walletId, - networks = setOf(cryptoCurrencyStatus.currency.network), + network = cryptoCurrencyStatus.currency.network, ) }, // we should update tx history and network for new balances From d5da2a7f0851cce07264f3394e1f4414b0f45a4e Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 15 May 2025 18:03:01 +0500 Subject: [PATCH 037/165] Updated on 2026-08-14 --- .../com/tangem/features/send/v2/common/ui/state/ConfirmUM.kt | 1 + .../features/send/v2/send/confirm/model/SendConfirmModel.kt | 2 ++ .../model/transformers/SendConfirmInitialStateTransformer.kt | 2 ++ .../model/transformers/NFTSendConfirmInitialStateTransformer.kt | 2 ++ .../com/tangem/features/send/v2/sendnft/ui/state/NFTSendUM.kt | 2 +- 5 files changed, 8 insertions(+), 1 deletion(-) diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/common/ui/state/ConfirmUM.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/common/ui/state/ConfirmUM.kt index aef792a091..78cc00c705 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/common/ui/state/ConfirmUM.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/common/ui/state/ConfirmUM.kt @@ -12,6 +12,7 @@ internal sealed class ConfirmUM { data class Content( override val isPrimaryButtonEnabled: Boolean = false, + val walletName: TextReference, val isSending: Boolean, val showTapHelp: Boolean, val sendingFooter: TextReference, diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/confirm/model/SendConfirmModel.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/confirm/model/SendConfirmModel.kt index e810d725d9..0627ddcd3a 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/confirm/model/SendConfirmModel.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/confirm/model/SendConfirmModel.kt @@ -15,6 +15,7 @@ import com.tangem.core.decompose.navigation.Router import com.tangem.core.navigation.share.ShareManager import com.tangem.core.navigation.url.UrlOpener import com.tangem.core.ui.extensions.resourceReference +import com.tangem.core.ui.extensions.stringReference import com.tangem.core.ui.extensions.wrappedList import com.tangem.domain.feedback.GetCardInfoUseCase import com.tangem.domain.feedback.SaveBlockchainErrorUseCase @@ -286,6 +287,7 @@ internal class SendConfirmModel @Inject constructor( it.copy( confirmUM = SendConfirmInitialStateTransformer( isShowTapHelp = isShowTapHelp, + walletName = stringReference(userWallet.name), ).transform(uiState.value.confirmUM), ) } diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/confirm/model/transformers/SendConfirmInitialStateTransformer.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/confirm/model/transformers/SendConfirmInitialStateTransformer.kt index c980c558a7..1231a4954e 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/confirm/model/transformers/SendConfirmInitialStateTransformer.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/confirm/model/transformers/SendConfirmInitialStateTransformer.kt @@ -7,9 +7,11 @@ import kotlinx.collections.immutable.persistentListOf internal class SendConfirmInitialStateTransformer( private val isShowTapHelp: Boolean, + private val walletName: TextReference, ) : Transformer { override fun transform(prevState: ConfirmUM): ConfirmUM { return ConfirmUM.Content( + walletName = walletName, isSending = false, showTapHelp = isShowTapHelp, sendingFooter = TextReference.EMPTY, diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/sendnft/confirm/model/transformers/NFTSendConfirmInitialStateTransformer.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/sendnft/confirm/model/transformers/NFTSendConfirmInitialStateTransformer.kt index 004117bd1d..e7f383dbe3 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/sendnft/confirm/model/transformers/NFTSendConfirmInitialStateTransformer.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/sendnft/confirm/model/transformers/NFTSendConfirmInitialStateTransformer.kt @@ -7,12 +7,14 @@ import kotlinx.collections.immutable.persistentListOf internal class NFTSendConfirmInitialStateTransformer( private val isShowTapHelp: Boolean, + private val walletName: TextReference, ) : Transformer { override fun transform(prevState: ConfirmUM): ConfirmUM { return ConfirmUM.Content( isSending = false, showTapHelp = isShowTapHelp, sendingFooter = TextReference.EMPTY, + walletName = walletName, notifications = persistentListOf(), ) } diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/sendnft/ui/state/NFTSendUM.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/sendnft/ui/state/NFTSendUM.kt index 526bfdd621..6d119d46cf 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/sendnft/ui/state/NFTSendUM.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/sendnft/ui/state/NFTSendUM.kt @@ -1,7 +1,7 @@ package com.tangem.features.send.v2.sendnft.ui.state -import com.tangem.features.send.v2.common.ui.state.NavigationUM import com.tangem.features.send.v2.common.ui.state.ConfirmUM +import com.tangem.features.send.v2.common.ui.state.NavigationUM import com.tangem.features.send.v2.subcomponents.destination.ui.state.DestinationUM import com.tangem.features.send.v2.subcomponents.fee.ui.state.FeeUM From eee3b57fe5eadbc36063d09b08877f1298babba5 Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 15 May 2025 18:03:07 +0500 Subject: [PATCH 038/165] Updated on 2026-08-14 --- .../java/com/tangem/utils/StringsSigns.kt | 1 + .../features/send/v2/common/ui/SendingText.kt | 51 ++++++++++++++ .../v2/common/utils/ConfirmFooterUtils.kt | 66 ++++++++++++++++++ ...endConfirmationNotificationsTransformer.kt | 69 +++---------------- .../v2/send/confirm/ui/SendConfirmContent.kt | 43 +----------- .../confirm/model/NFTSendConfirmModel.kt | 14 ++-- ...endConfirmationNotificationsTransformer.kt | 42 +++++++++++ .../confirm/ui/NFTSendConfirmContent.kt | 5 ++ 8 files changed, 183 insertions(+), 108 deletions(-) create mode 100644 features/send-v2/impl/src/main/java/com/tangem/features/send/v2/common/ui/SendingText.kt create mode 100644 features/send-v2/impl/src/main/java/com/tangem/features/send/v2/common/utils/ConfirmFooterUtils.kt diff --git a/core/utils/src/main/java/com/tangem/utils/StringsSigns.kt b/core/utils/src/main/java/com/tangem/utils/StringsSigns.kt index 17a02be930..7d53361c16 100644 --- a/core/utils/src/main/java/com/tangem/utils/StringsSigns.kt +++ b/core/utils/src/main/java/com/tangem/utils/StringsSigns.kt @@ -8,6 +8,7 @@ object StringsSigns { const val DASH_SIGN = "—" const val LOWER_SIGN = "<" const val TILDE_SIGN = "~" + const val COMA_SIGN = "," const val INFINITY_SIGN = "∞" const val NON_BREAKING_SPACE = '\u00A0' const val PERCENT = "%" diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/common/ui/SendingText.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/common/ui/SendingText.kt new file mode 100644 index 0000000000..da368be38b --- /dev/null +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/common/ui/SendingText.kt @@ -0,0 +1,51 @@ +package com.tangem.features.send.v2.common.ui + +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.animation.core.tween +import androidx.compose.animation.fadeIn +import androidx.compose.animation.fadeOut +import androidx.compose.animation.slideInVertically +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.Text +import androidx.compose.runtime.* +import androidx.compose.ui.Modifier +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.dp +import com.tangem.core.ui.components.Keyboard +import com.tangem.core.ui.components.keyboardAsState +import com.tangem.core.ui.extensions.TextReference +import com.tangem.core.ui.extensions.resolveAnnotatedReference +import com.tangem.core.ui.res.TangemTheme + +@Composable +internal fun SendingText(footerText: TextReference, modifier: Modifier = Modifier) { + var isVisibleProxy by remember { mutableStateOf(footerText != TextReference.EMPTY) } + val keyboard by keyboardAsState() + + // the text should appear when the keyboard is closed + LaunchedEffect(footerText != TextReference.EMPTY, keyboard) { + if (footerText != TextReference.EMPTY && keyboard is Keyboard.Opened) { + return@LaunchedEffect + } + isVisibleProxy = footerText != TextReference.EMPTY + } + + AnimatedVisibility( + visible = isVisibleProxy, + modifier = modifier, + enter = slideInVertically(initialOffsetY = { it / 2 }) + fadeIn(), + exit = fadeOut(tween(durationMillis = 300)), + label = "Animate show sending state text", + ) { + Text( + text = footerText.resolveAnnotatedReference(), + textAlign = TextAlign.Center, + style = TangemTheme.typography.caption2, + color = TangemTheme.colors.text.primary1, + modifier = Modifier + .fillMaxWidth() + .padding(12.dp), + ) + } +} \ No newline at end of file diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/common/utils/ConfirmFooterUtils.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/common/utils/ConfirmFooterUtils.kt new file mode 100644 index 0000000000..3c13220638 --- /dev/null +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/common/utils/ConfirmFooterUtils.kt @@ -0,0 +1,66 @@ +package com.tangem.features.send.v2.common.utils + +import com.tangem.blockchain.common.Amount +import com.tangem.blockchain.common.transaction.Fee +import com.tangem.core.ui.extensions.* +import com.tangem.core.ui.format.bigdecimal.crypto +import com.tangem.core.ui.format.bigdecimal.fee +import com.tangem.core.ui.format.bigdecimal.fiat +import com.tangem.core.ui.format.bigdecimal.format +import com.tangem.domain.appcurrency.model.AppCurrency +import com.tangem.features.send.v2.impl.R +import com.tangem.utils.StringsSigns.COMA_SIGN + +internal fun getTronTokenFeeSendingText(fee: Fee.Tron, fiatFee: String, fiatSending: TextReference): TextReference { + val suffix = when { + fee.remainingEnergy == 0L -> { + resourceReference( + R.string.send_summary_transaction_description_suffix_including, + wrappedList(fiatFee), + ) + } + fee.feeEnergy <= fee.remainingEnergy -> { + resourceReference( + R.string.send_summary_transaction_description_suffix_fee_covered, + wrappedList(fee.feeEnergy), + ) + } + else -> { + resourceReference( + R.string.send_summary_transaction_description_suffix_fee_reduced, + wrappedList(fee.remainingEnergy), + ) + } + } + val prefix = resourceReference( + R.string.send_summary_transaction_description_prefix, + wrappedList(fiatSending), + ) + + return combinedReference(prefix, stringReference("$COMA_SIGN "), suffix) +} + +internal fun formatFooterFiatFee( + amount: Amount?, + isFeeConvertibleToFiat: Boolean, + isFeeApproximate: Boolean, + appCurrency: AppCurrency, +): String { + return if (isFeeConvertibleToFiat) { + amount?.value.format { + fiat( + fiatCurrencyCode = appCurrency.code, + fiatCurrencySymbol = appCurrency.symbol, + ) + } + } else { + amount?.value.format { + crypto( + decimals = amount?.decimals ?: 0, + symbol = amount?.currencySymbol.orEmpty(), + ).fee( + canBeLower = isFeeApproximate, + ) + } + } +} \ No newline at end of file diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/confirm/model/transformers/SendConfirmationNotificationsTransformer.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/confirm/model/transformers/SendConfirmationNotificationsTransformer.kt index dc73d36994..5167eb5122 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/confirm/model/transformers/SendConfirmationNotificationsTransformer.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/confirm/model/transformers/SendConfirmationNotificationsTransformer.kt @@ -1,13 +1,14 @@ package com.tangem.features.send.v2.send.confirm.model.transformers -import com.tangem.blockchain.common.Amount import com.tangem.blockchain.common.transaction.Fee import com.tangem.blockchain.common.transaction.TransactionFee import com.tangem.common.ui.amountScreen.models.AmountState import com.tangem.common.ui.notifications.NotificationUM import com.tangem.core.analytics.api.AnalyticsEventHandler -import com.tangem.core.ui.extensions.* -import com.tangem.core.ui.format.bigdecimal.crypto +import com.tangem.core.ui.extensions.TextReference +import com.tangem.core.ui.extensions.resourceReference +import com.tangem.core.ui.extensions.stringReference +import com.tangem.core.ui.extensions.wrappedList import com.tangem.core.ui.format.bigdecimal.fee import com.tangem.core.ui.format.bigdecimal.fiat import com.tangem.core.ui.format.bigdecimal.format @@ -16,6 +17,8 @@ import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.features.send.v2.common.analytics.CommonSendAnalyticEvents import com.tangem.features.send.v2.common.ui.state.ConfirmUM +import com.tangem.features.send.v2.common.utils.formatFooterFiatFee +import com.tangem.features.send.v2.common.utils.getTronTokenFeeSendingText import com.tangem.features.send.v2.impl.R import com.tangem.features.send.v2.subcomponents.fee.model.checkIfFeeTooHigh import com.tangem.features.send.v2.subcomponents.fee.ui.state.FeeSelectorUM @@ -92,17 +95,18 @@ internal class SendConfirmationNotificationsTransformer( fiatCurrencySymbol = appCurrency.symbol, ) } - val fiatFee = formatFiatFee( + val fiatFee = formatFooterFiatFee( amount = fee.amount, isFeeConvertibleToFiat = feeUM.isFeeConvertibleToFiat, isFeeApproximate = feeUM.isFeeApproximate, + appCurrency = appCurrency, ) return if (feeUM.isTronToken && fee is Fee.Tron) { - getTokenFeeSendingText( + getTronTokenFeeSendingText( fee = fee, fiatFee = fiatFee, - fiatSending = fiatSending, + fiatSending = stringReference(fiatSending), ) } else { resourceReference( @@ -115,57 +119,4 @@ internal class SendConfirmationNotificationsTransformer( ) } } - - private fun formatFiatFee(amount: Amount?, isFeeConvertibleToFiat: Boolean, isFeeApproximate: Boolean): String { - return if (isFeeConvertibleToFiat) { - amount?.value.format { - fiat( - fiatCurrencyCode = appCurrency.code, - fiatCurrencySymbol = appCurrency.symbol, - ) - } - } else { - amount?.value.format { - crypto( - decimals = amount?.decimals ?: 0, - symbol = amount?.currencySymbol.orEmpty(), - ).fee( - canBeLower = isFeeApproximate, - ) - } - } - } - - private fun getTokenFeeSendingText(fee: Fee.Tron, fiatFee: String, fiatSending: String): TextReference { - val suffix = when { - fee.remainingEnergy == 0L -> { - resourceReference( - R.string.send_summary_transaction_description_suffix_including, - wrappedList(fiatFee), - ) - } - fee.feeEnergy <= fee.remainingEnergy -> { - resourceReference( - R.string.send_summary_transaction_description_suffix_fee_covered, - wrappedList(fee.feeEnergy), - ) - } - else -> { - resourceReference( - R.string.send_summary_transaction_description_suffix_fee_reduced, - wrappedList(fee.remainingEnergy), - ) - } - } - val prefix = resourceReference( - R.string.send_summary_transaction_description_prefix, - wrappedList(fiatSending), - ) - - return combinedReference(prefix, COMMA_SEPARATOR, suffix) - } - - companion object { - private val COMMA_SEPARATOR = stringReference(", ") - } } \ No newline at end of file diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/confirm/ui/SendConfirmContent.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/confirm/ui/SendConfirmContent.kt index bcf7d3283a..4f13d834a7 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/confirm/ui/SendConfirmContent.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/confirm/ui/SendConfirmContent.kt @@ -1,33 +1,24 @@ package com.tangem.features.send.v2.send.confirm.ui import androidx.compose.animation.AnimatedVisibility -import androidx.compose.animation.core.tween -import androidx.compose.animation.fadeIn -import androidx.compose.animation.fadeOut -import androidx.compose.animation.slideInVertically import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyListScope -import androidx.compose.material3.Text import androidx.compose.runtime.* import androidx.compose.ui.Modifier -import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp import com.tangem.common.ui.notifications.NotificationUM -import com.tangem.core.ui.components.Keyboard import com.tangem.core.ui.components.SpacerHMax -import com.tangem.core.ui.components.keyboardAsState import com.tangem.core.ui.components.transactions.TransactionDoneTitle import com.tangem.core.ui.extensions.TextReference -import com.tangem.core.ui.extensions.resolveAnnotatedReference import com.tangem.core.ui.extensions.resourceReference import com.tangem.core.ui.extensions.wrappedList import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.utils.DateTimeFormatters import com.tangem.core.ui.utils.toTimeFormat +import com.tangem.features.send.v2.common.ui.SendingText import com.tangem.features.send.v2.common.ui.state.ConfirmUM import com.tangem.features.send.v2.common.ui.tapHelp import com.tangem.features.send.v2.impl.R @@ -82,38 +73,6 @@ internal fun SendConfirmContent( } } -@Composable -private fun SendingText(footerText: TextReference, modifier: Modifier = Modifier) { - var isVisibleProxy by remember { mutableStateOf(footerText != TextReference.EMPTY) } - val keyboard by keyboardAsState() - - // the text should appear when the keyboard is closed - LaunchedEffect(footerText != TextReference.EMPTY, keyboard) { - if (footerText != TextReference.EMPTY && keyboard is Keyboard.Opened) { - return@LaunchedEffect - } - isVisibleProxy = footerText != TextReference.EMPTY - } - - AnimatedVisibility( - visible = isVisibleProxy, - modifier = modifier, - enter = slideInVertically() + fadeIn(), - exit = fadeOut(tween(durationMillis = 300)), - label = "Animate show sending state text", - ) { - Text( - text = footerText.resolveAnnotatedReference(), - textAlign = TextAlign.Center, - style = TangemTheme.typography.caption2, - color = TangemTheme.colors.text.primary1, - modifier = Modifier - .fillMaxWidth() - .padding(12.dp), - ) - } -} - private fun LazyListScope.blocks( uiState: SendUM, destinationBlockComponent: SendDestinationBlockComponent, diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/sendnft/confirm/model/NFTSendConfirmModel.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/sendnft/confirm/model/NFTSendConfirmModel.kt index aafd0448ea..1cc52b7256 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/sendnft/confirm/model/NFTSendConfirmModel.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/sendnft/confirm/model/NFTSendConfirmModel.kt @@ -12,7 +12,7 @@ import com.tangem.core.decompose.navigation.Router import com.tangem.core.navigation.share.ShareManager import com.tangem.core.navigation.url.UrlOpener import com.tangem.core.ui.extensions.resourceReference -import com.tangem.core.ui.extensions.wrappedList +import com.tangem.core.ui.extensions.stringReference import com.tangem.datasource.local.nft.converter.NFTSdkAssetConverter import com.tangem.domain.feedback.GetCardInfoUseCase import com.tangem.domain.feedback.SaveBlockchainErrorUseCase @@ -228,6 +228,7 @@ internal class NFTSendConfirmModel @Inject constructor( it.copy( confirmUM = NFTSendConfirmInitialStateTransformer( isShowTapHelp = isShowTapHelp, + walletName = stringReference(userWallet.name), ).transform(uiState.value.confirmUM), ) } @@ -357,6 +358,7 @@ internal class NFTSendConfirmModel @Inject constructor( analyticsEventHandler = analyticsEventHandler, cryptoCurrency = cryptoCurrencyStatus.currency, analyticsCategoryName = analyticsCategoryName, + appCurrency = params.appCurrency, ).transform(uiState.value.confirmUM), ) } @@ -369,15 +371,13 @@ internal class NFTSendConfirmModel @Inject constructor( transform = { state, route -> state to route }, ).onEach { (state, _) -> val confirmUM = state.confirmUM - val isReadyToSend = confirmUM is ConfirmUM.Content && !confirmUM.isSending + val confirmUMContent = confirmUM as? ConfirmUM.Content + val isReadyToSend = confirmUMContent != null && !confirmUM.isSending params.callback.onResult( state.copy( navigationUM = NavigationUM.Content( - title = resourceReference( - id = R.string.send_summary_title, - formatArgs = wrappedList(params.cryptoCurrencyStatus.currency.name), - ), - subtitle = null, + title = resourceReference(R.string.nft_send), + subtitle = confirmUMContent?.walletName, backIconRes = R.drawable.ic_close_24, backIconClick = { analyticsEventHandler.send( diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/sendnft/confirm/model/transformers/NFTSendConfirmationNotificationsTransformer.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/sendnft/confirm/model/transformers/NFTSendConfirmationNotificationsTransformer.kt index f5b2ba2744..742dcea1cf 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/sendnft/confirm/model/transformers/NFTSendConfirmationNotificationsTransformer.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/sendnft/confirm/model/transformers/NFTSendConfirmationNotificationsTransformer.kt @@ -1,12 +1,20 @@ package com.tangem.features.send.v2.sendnft.confirm.model.transformers +import com.tangem.blockchain.common.transaction.Fee import com.tangem.blockchain.common.transaction.TransactionFee import com.tangem.common.ui.notifications.NotificationUM import com.tangem.core.analytics.api.AnalyticsEventHandler +import com.tangem.core.ui.extensions.TextReference +import com.tangem.core.ui.extensions.resourceReference +import com.tangem.core.ui.extensions.wrappedList import com.tangem.core.ui.utils.parseToBigDecimal +import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.features.send.v2.common.analytics.CommonSendAnalyticEvents import com.tangem.features.send.v2.common.ui.state.ConfirmUM +import com.tangem.features.send.v2.common.utils.formatFooterFiatFee +import com.tangem.features.send.v2.common.utils.getTronTokenFeeSendingText +import com.tangem.features.send.v2.impl.R import com.tangem.features.send.v2.subcomponents.fee.model.checkIfFeeTooHigh import com.tangem.features.send.v2.subcomponents.fee.ui.state.FeeSelectorUM import com.tangem.features.send.v2.subcomponents.fee.ui.state.FeeType @@ -19,11 +27,13 @@ internal class NFTSendConfirmationNotificationsTransformer( private val analyticsEventHandler: AnalyticsEventHandler, private val cryptoCurrency: CryptoCurrency, private val analyticsCategoryName: String, + private val appCurrency: AppCurrency, ) : Transformer { override fun transform(prevState: ConfirmUM): ConfirmUM { val state = prevState as? ConfirmUM.Content ?: return prevState val feeUM = feeUM as? FeeUM.Content ?: return prevState return state.copy( + sendingFooter = getSendingFooterText(), notifications = buildList { addTooHighNotification(feeUM.feeSelectorUM) addTooLowNotification(feeUM) @@ -48,6 +58,38 @@ internal class NFTSendConfirmationNotificationsTransformer( } } + private fun getSendingFooterText(): TextReference { + val feeUM = feeUM as? FeeUM.Content + val fee = (feeUM?.feeSelectorUM as? FeeSelectorUM.Content)?.selectedFee ?: return TextReference.EMPTY + + val fiatFee = formatFooterFiatFee( + amount = fee.amount, + isFeeConvertibleToFiat = feeUM.isFeeConvertibleToFiat, + isFeeApproximate = feeUM.isFeeApproximate, + appCurrency = appCurrency, + ) + + return if (feeUM.isTronToken && fee is Fee.Tron) { + getTronTokenFeeSendingText( + fee = fee, + fiatFee = fiatFee, + fiatSending = resourceReference(R.string.common_nft), + ) + } else { + resourceReference( + id = if (feeUM.isFeeConvertibleToFiat) { + R.string.send_summary_transaction_description + } else { + R.string.send_summary_transaction_description_no_fiat_fee + }, + formatArgs = wrappedList( + resourceReference(R.string.common_nft), + fiatFee, + ), + ) + } + } + private fun MutableList.addTooHighNotification(feeSelectorUM: FeeSelectorUM) { if (feeSelectorUM !is FeeSelectorUM.Content) return diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/sendnft/confirm/ui/NFTSendConfirmContent.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/sendnft/confirm/ui/NFTSendConfirmContent.kt index 735f3ecaf0..2d41666e61 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/sendnft/confirm/ui/NFTSendConfirmContent.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/sendnft/confirm/ui/NFTSendConfirmContent.kt @@ -11,13 +11,16 @@ import androidx.compose.runtime.remember import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp import com.tangem.common.ui.notifications.NotificationUM +import com.tangem.core.ui.components.SpacerHMax import com.tangem.core.ui.components.transactions.TransactionDoneTitle +import com.tangem.core.ui.extensions.TextReference import com.tangem.core.ui.extensions.resourceReference import com.tangem.core.ui.extensions.wrappedList import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.utils.DateTimeFormatters import com.tangem.core.ui.utils.toTimeFormat import com.tangem.features.nft.component.NFTDetailsBlockComponent +import com.tangem.features.send.v2.common.ui.SendingText import com.tangem.features.send.v2.common.ui.state.ConfirmUM import com.tangem.features.send.v2.common.ui.tapHelp import com.tangem.features.send.v2.impl.R @@ -66,6 +69,8 @@ internal fun NFTSendConfirmContent( ) } } + SpacerHMax() + SendingText(footerText = confirmUM?.sendingFooter ?: TextReference.EMPTY) } } From 1841d77c4cacd62d8e362a3aed4df4ce4277dff8 Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 15 May 2025 08:39:07 +0400 Subject: [PATCH 039/165] Updated on 2026-08-14 --- .../di/NetworksStatusesStoreModule.kt | 57 ----- .../network/DefaultNetworksStatusesStore.kt | 201 ------------------ .../local/network/NetworksStatusesStore.kt | 21 -- .../converter/NetworkStatusConverter.kt | 47 ---- .../java/com/tangem/utils/extensions/Map.kt | 11 + .../converters}/NetworkAddressConverter.kt | 4 +- .../converters}/NetworkAmountsConverter.kt | 6 +- .../NetworkDerivationPathConverter.kt | 17 +- .../NetworkStatusDataModelConverter.kt | 4 +- .../SimpleNetworkStatusConverter.kt | 3 - .../data/networks/di/NetworkDataModule.kt | 29 +++ .../store/DefaultNetworksStatusesStoreV2.kt | 2 +- .../tangem/data/networks/NetworkStatusExt.kt | 2 +- 13 files changed, 57 insertions(+), 347 deletions(-) delete mode 100644 core/datasource/src/main/java/com/tangem/datasource/di/NetworksStatusesStoreModule.kt delete mode 100644 core/datasource/src/main/java/com/tangem/datasource/local/network/DefaultNetworksStatusesStore.kt delete mode 100644 core/datasource/src/main/java/com/tangem/datasource/local/network/NetworksStatusesStore.kt delete mode 100644 core/datasource/src/main/java/com/tangem/datasource/local/network/converter/NetworkStatusConverter.kt create mode 100644 core/utils/src/main/java/com/tangem/utils/extensions/Map.kt rename {core/datasource/src/main/java/com/tangem/datasource/local/network/converter => data/networks/src/main/java/com/tangem/data/networks/converters}/NetworkAddressConverter.kt (96%) rename {core/datasource/src/main/java/com/tangem/datasource/local/network/converter => data/networks/src/main/java/com/tangem/data/networks/converters}/NetworkAmountsConverter.kt (85%) rename {core/datasource/src/main/java/com/tangem/datasource/local/network/converter => data/networks/src/main/java/com/tangem/data/networks/converters}/NetworkDerivationPathConverter.kt (53%) rename {core/datasource/src/main/java/com/tangem/datasource/local/network/converter => data/networks/src/main/java/com/tangem/data/networks/converters}/NetworkStatusDataModelConverter.kt (92%) diff --git a/core/datasource/src/main/java/com/tangem/datasource/di/NetworksStatusesStoreModule.kt b/core/datasource/src/main/java/com/tangem/datasource/di/NetworksStatusesStoreModule.kt deleted file mode 100644 index f0cabaf025..0000000000 --- a/core/datasource/src/main/java/com/tangem/datasource/di/NetworksStatusesStoreModule.kt +++ /dev/null @@ -1,57 +0,0 @@ -package com.tangem.datasource.di - -import android.content.Context -import androidx.datastore.core.DataStore -import androidx.datastore.core.DataStoreFactory -import androidx.datastore.dataStoreFile -import com.squareup.moshi.Moshi -import com.tangem.datasource.local.datastore.RuntimeDataStore -import com.tangem.datasource.local.network.DefaultNetworksStatusesStore -import com.tangem.datasource.local.network.NetworksStatusesStore -import com.tangem.datasource.local.network.entity.NetworkStatusDM -import com.tangem.datasource.utils.MoshiDataStoreSerializer -import com.tangem.datasource.utils.mapWithStringKeyTypes -import com.tangem.datasource.utils.setTypes -import com.tangem.utils.coroutines.CoroutineDispatcherProvider -import dagger.Module -import dagger.Provides -import dagger.hilt.InstallIn -import dagger.hilt.android.qualifiers.ApplicationContext -import dagger.hilt.components.SingletonComponent -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.SupervisorJob -import javax.inject.Singleton - -@Module -@InstallIn(SingletonComponent::class) -internal object NetworksStatusesStoreModule { - - @Singleton - @Provides - fun providePersistenceNetworksStatusesStore( - @NetworkMoshi moshi: Moshi, - @ApplicationContext context: Context, - dispatchers: CoroutineDispatcherProvider, - ): DataStore>> { - return DataStoreFactory.create( - serializer = MoshiDataStoreSerializer( - moshi = moshi, - types = mapWithStringKeyTypes(valueTypes = setTypes()), - defaultValue = emptyMap(), - ), - produceFile = { context.dataStoreFile(fileName = "networks_statuses") }, - scope = CoroutineScope(context = dispatchers.io + SupervisorJob()), - ) - } - - @Singleton - @Provides - fun provideNetworksStatusesStore( - persistenceNetworksStatusesStore: DataStore>>, - ): NetworksStatusesStore { - return DefaultNetworksStatusesStore( - runtimeDataStore = RuntimeDataStore(), - persistenceDataStore = persistenceNetworksStatusesStore, - ) - } -} \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/network/DefaultNetworksStatusesStore.kt b/core/datasource/src/main/java/com/tangem/datasource/local/network/DefaultNetworksStatusesStore.kt deleted file mode 100644 index b31620f4a4..0000000000 --- a/core/datasource/src/main/java/com/tangem/datasource/local/network/DefaultNetworksStatusesStore.kt +++ /dev/null @@ -1,201 +0,0 @@ -package com.tangem.datasource.local.network - -import androidx.datastore.core.DataStore -import com.tangem.datasource.local.datastore.RuntimeDataStore -import com.tangem.datasource.local.network.converter.NetworkDerivationPathConverter -import com.tangem.datasource.local.network.converter.NetworkStatusConverter -import com.tangem.datasource.local.network.converter.NetworkStatusDataModelConverter -import com.tangem.datasource.local.network.entity.NetworkStatusDM -import com.tangem.domain.models.StatusSource -import com.tangem.domain.tokens.model.Network -import com.tangem.domain.tokens.model.NetworkStatus -import com.tangem.domain.wallets.models.UserWalletId -import com.tangem.utils.extensions.addOrReplace -import kotlinx.coroutines.coroutineScope -import kotlinx.coroutines.flow.* -import kotlinx.coroutines.launch -import kotlinx.coroutines.sync.Mutex -import kotlinx.coroutines.sync.withLock - -private typealias NetworkStatusesByWalletId = Map> - -internal class DefaultNetworksStatusesStore( - private val runtimeDataStore: RuntimeDataStore>, - private val persistenceDataStore: DataStore, -) : NetworksStatusesStore { - - private val mutex = Mutex() - - override fun get(key: UserWalletId): Flow> { - return runtimeDataStore.get(provideStringKey(key)) - } - - override fun get(key: UserWalletId, networks: Set): Flow> = channelFlow { - val cachedStatuses = persistenceDataStore.data.firstOrNull() - ?.get(key.stringValue) - ?.mapNotNullTo(mutableSetOf()) { cached -> - val network = networks.firstOrNull { - it.id == cached.networkId && - it.derivationPath == NetworkDerivationPathConverter.convert(cached.derivationPath) - } - ?: return@mapNotNullTo null - - NetworkStatusConverter(network = network, isCached = true).convert(value = cached) - } - .orEmpty() - - if (cachedStatuses.isNotEmpty()) { - send(cachedStatuses) - } - - runtimeDataStore.get(provideStringKey(key)) - .onEach { runtimeStatuses -> - val mergedStatuses = mergeStatuses( - networks = networks, - cachedStatuses = cachedStatuses, - runtimeStatuses = runtimeStatuses, - ) - - send(mergedStatuses) - } - .launchIn(scope = this) - } - - override suspend fun getSyncOrNull(key: UserWalletId): Set? { - val runtimeStatuses = runtimeDataStore.getSyncOrNull(key = provideStringKey(key)) ?: return null - - val networks = runtimeStatuses.map(NetworkStatus::network).toSet() - - val cachedStatuses = persistenceDataStore.data.firstOrNull() - ?.get(key.stringValue) - ?.mapNotNullTo(mutableSetOf()) { cached -> - val network = networks.firstOrNull { - it.id == cached.networkId && - it.derivationPath == NetworkDerivationPathConverter.convert(cached.derivationPath) - } - ?: return@mapNotNullTo null - - NetworkStatusConverter(network = network, isCached = true).convert(value = cached) - } - .orEmpty() - - return mergeStatuses( - networks = networks, - cachedStatuses = cachedStatuses, - runtimeStatuses = runtimeStatuses, - ) - } - - override suspend fun store(key: UserWalletId, value: NetworkStatus) { - storeAll(key = key, values = setOf(value)) - } - - override suspend fun storeAll(key: UserWalletId, values: Set) { - mutex.withLock { - coroutineScope { - launch { storeInRuntimeStore(key = key, statuses = values) } - launch { storeInPersistenceStore(userWalletId = key, statuses = values) } - } - } - } - - override suspend fun refresh(key: UserWalletId, networks: Set) { - mutex.withLock { - val currentStatuses = getSyncOrNull(key).orEmpty() - - storeInRuntimeStore( - key = key, - statuses = networks.mapNotNullTo(hashSetOf()) { network -> - val status = currentStatuses.firstOrNull { - it.network.id == network.id && it.network.derivationPath == network.derivationPath - } ?: return@mapNotNullTo null - - status.copy( - value = status.value.copySealed(source = StatusSource.CACHE), - ) - }, - ) - } - } - - /** - * Merge [cachedStatuses] with [runtimeStatuses] - * The resulting set contains statuses from both sets. - * If a status with the same network is in both sets, the status from [runtimeStatuses] is used. - */ - private fun mergeStatuses( - networks: Set, - cachedStatuses: Set, - runtimeStatuses: Set, - ): Set { - return networks.mapNotNullTo(hashSetOf()) { network -> - val runtimeStatus = runtimeStatuses.firstOrNull { it.network == network } - - if (runtimeStatus == null) { - getCachedStatusIfPossible( - cachedStatuses = cachedStatuses, - network = network, - source = StatusSource.CACHE, - ) - } else if (runtimeStatus.value is NetworkStatus.Unreachable) { - getCachedStatusIfPossible( - cachedStatuses = cachedStatuses, - network = network, - source = StatusSource.ONLY_CACHE, - ) - ?: runtimeStatus - } else { - runtimeStatus - } - } - } - - private fun getCachedStatusIfPossible( - cachedStatuses: Set, - network: Network, - source: StatusSource, - ): NetworkStatus? { - val cached = cachedStatuses.firstOrNull { it.network == network } ?: return null - - val updatedCachedStatus = when (val status = cached.value) { - is NetworkStatus.NoAccount -> status.copy(source = source) - is NetworkStatus.Verified -> status.copy(source = source) - is NetworkStatus.Unreachable, - is NetworkStatus.MissedDerivation, - -> null - } - - return if (updatedCachedStatus != null) { - cached.copy(value = updatedCachedStatus) - } else { - null - } - } - - private suspend fun storeInRuntimeStore(key: UserWalletId, statuses: Set) { - val updatedValues = getSyncOrNull(key).orEmpty() - .addOrReplace(items = statuses) { prev, new -> prev.network == new.network } - - runtimeDataStore.store(key = provideStringKey(key), value = updatedValues) - } - - private suspend fun storeInPersistenceStore(userWalletId: UserWalletId, statuses: Set) { - // Converter will return null if the network status is not supported - val newStatuses = NetworkStatusDataModelConverter.convertSet(input = statuses).filterNotNull().toSet() - - persistenceDataStore.updateData { storedStatuses -> - storedStatuses.toMutableMap().apply { - val updatedValues = this[userWalletId.stringValue].orEmpty() - .addOrReplace(newStatuses) { prev, new -> - prev.networkId == new.networkId && prev.derivationPath == new.derivationPath - } - - this[userWalletId.stringValue] = updatedValues - } - } - } - - private fun provideStringKey(key: UserWalletId): String { - return "network_statuses_${key.stringValue}" - } -} \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/network/NetworksStatusesStore.kt b/core/datasource/src/main/java/com/tangem/datasource/local/network/NetworksStatusesStore.kt deleted file mode 100644 index a1d92c60cc..0000000000 --- a/core/datasource/src/main/java/com/tangem/datasource/local/network/NetworksStatusesStore.kt +++ /dev/null @@ -1,21 +0,0 @@ -package com.tangem.datasource.local.network - -import com.tangem.domain.tokens.model.Network -import com.tangem.domain.tokens.model.NetworkStatus -import com.tangem.domain.wallets.models.UserWalletId -import kotlinx.coroutines.flow.Flow - -interface NetworksStatusesStore { - - fun get(key: UserWalletId): Flow> - - fun get(key: UserWalletId, networks: Set): Flow> - - suspend fun getSyncOrNull(key: UserWalletId): Set? - - suspend fun store(key: UserWalletId, value: NetworkStatus) - - suspend fun storeAll(key: UserWalletId, values: Set) - - suspend fun refresh(key: UserWalletId, networks: Set) -} \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/network/converter/NetworkStatusConverter.kt b/core/datasource/src/main/java/com/tangem/datasource/local/network/converter/NetworkStatusConverter.kt deleted file mode 100644 index f0a01555bf..0000000000 --- a/core/datasource/src/main/java/com/tangem/datasource/local/network/converter/NetworkStatusConverter.kt +++ /dev/null @@ -1,47 +0,0 @@ -package com.tangem.datasource.local.network.converter - -import com.tangem.datasource.local.network.entity.NetworkStatusDM -import com.tangem.domain.models.StatusSource -import com.tangem.domain.tokens.model.Network -import com.tangem.domain.tokens.model.NetworkStatus -import com.tangem.utils.converter.Converter - -/** - * Converter from [NetworkStatusDM] to [NetworkStatus] - * - * @property network network - * @property isCached flag that determines whether the status is a cache - * -[REDACTED_AUTHOR] - */ -internal class NetworkStatusConverter( - private val network: Network, - private val isCached: Boolean, -) : Converter { - - override fun convert(value: NetworkStatusDM): NetworkStatus { - val address = NetworkAddressConverter(selectedAddress = value.selectedAddress) - .convert(value = value.availableAddresses) - - val status = when (value) { - is NetworkStatusDM.Verified -> { - NetworkStatus.Verified( - address = address, - amounts = NetworkAmountsConverter.convert(value = value.amounts), - pendingTransactions = mapOf(), - source = if (isCached) StatusSource.CACHE else StatusSource.ACTUAL, - ) - } - is NetworkStatusDM.NoAccount -> { - NetworkStatus.NoAccount( - address = address, - amountToCreateAccount = value.amountToCreateAccount, - errorMessage = value.errorMessage, - source = if (isCached) StatusSource.CACHE else StatusSource.ACTUAL, - ) - } - } - - return NetworkStatus(network = network, value = status) - } -} \ No newline at end of file diff --git a/core/utils/src/main/java/com/tangem/utils/extensions/Map.kt b/core/utils/src/main/java/com/tangem/utils/extensions/Map.kt new file mode 100644 index 0000000000..99cf375010 --- /dev/null +++ b/core/utils/src/main/java/com/tangem/utils/extensions/Map.kt @@ -0,0 +1,11 @@ +package com.tangem.utils.extensions + +fun Map.mapNotNullValues(transform: (Map.Entry) -> R?): Map { + return this + .mapNotNull { entry -> + val newValue = transform(entry) ?: return@mapNotNull null + + entry.key to newValue + } + .toMap() +} \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/network/converter/NetworkAddressConverter.kt b/data/networks/src/main/java/com/tangem/data/networks/converters/NetworkAddressConverter.kt similarity index 96% rename from core/datasource/src/main/java/com/tangem/datasource/local/network/converter/NetworkAddressConverter.kt rename to data/networks/src/main/java/com/tangem/data/networks/converters/NetworkAddressConverter.kt index 5d3ba03610..f3e2e962e2 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/local/network/converter/NetworkAddressConverter.kt +++ b/data/networks/src/main/java/com/tangem/data/networks/converters/NetworkAddressConverter.kt @@ -1,4 +1,4 @@ -package com.tangem.datasource.local.network.converter +package com.tangem.data.networks.converters import com.tangem.datasource.local.network.entity.NetworkStatusDM import com.tangem.domain.tokens.model.NetworkAddress @@ -10,7 +10,7 @@ import timber.log.Timber * [REDACTED_AUTHOR] */ -class NetworkAddressConverter( +internal class NetworkAddressConverter( private val selectedAddress: String, ) : TwoWayConverter, NetworkAddress> { diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/network/converter/NetworkAmountsConverter.kt b/data/networks/src/main/java/com/tangem/data/networks/converters/NetworkAmountsConverter.kt similarity index 85% rename from core/datasource/src/main/java/com/tangem/datasource/local/network/converter/NetworkAmountsConverter.kt rename to data/networks/src/main/java/com/tangem/data/networks/converters/NetworkAmountsConverter.kt index e3bac6232a..f55ec596d5 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/local/network/converter/NetworkAmountsConverter.kt +++ b/data/networks/src/main/java/com/tangem/data/networks/converters/NetworkAmountsConverter.kt @@ -1,9 +1,9 @@ -package com.tangem.datasource.local.network.converter +package com.tangem.data.networks.converters -import com.tangem.common.extensions.mapNotNullValues import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.domain.tokens.model.CryptoCurrencyAmountStatus import com.tangem.utils.converter.TwoWayConverter +import com.tangem.utils.extensions.mapNotNullValues import java.math.BigDecimal private typealias AmountsDataModel = Map @@ -14,7 +14,7 @@ private typealias AmountsDomainModel = Map { +internal object NetworkAmountsConverter : TwoWayConverter { override fun convert(value: AmountsDataModel): AmountsDomainModel { return value diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/network/converter/NetworkDerivationPathConverter.kt b/data/networks/src/main/java/com/tangem/data/networks/converters/NetworkDerivationPathConverter.kt similarity index 53% rename from core/datasource/src/main/java/com/tangem/datasource/local/network/converter/NetworkDerivationPathConverter.kt rename to data/networks/src/main/java/com/tangem/data/networks/converters/NetworkDerivationPathConverter.kt index 1cffbc73f9..c03450ee25 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/local/network/converter/NetworkDerivationPathConverter.kt +++ b/data/networks/src/main/java/com/tangem/data/networks/converters/NetworkDerivationPathConverter.kt @@ -1,7 +1,6 @@ -package com.tangem.datasource.local.network.converter +package com.tangem.data.networks.converters import com.tangem.datasource.local.network.entity.NetworkStatusDM -import com.tangem.datasource.local.network.entity.NetworkStatusDM.DerivationPath.Type import com.tangem.domain.tokens.model.Network import com.tangem.utils.converter.TwoWayConverter @@ -10,14 +9,14 @@ import com.tangem.utils.converter.TwoWayConverter * [REDACTED_AUTHOR] */ -object NetworkDerivationPathConverter : +internal object NetworkDerivationPathConverter : TwoWayConverter { override fun convert(value: NetworkStatusDM.DerivationPath): Network.DerivationPath { return when (value.type) { - Type.CARD -> Network.DerivationPath.Card(value.value) - Type.CUSTOM -> Network.DerivationPath.Custom(value.value) - Type.NONE -> Network.DerivationPath.None + NetworkStatusDM.DerivationPath.Type.CARD -> Network.DerivationPath.Card(value.value) + NetworkStatusDM.DerivationPath.Type.CUSTOM -> Network.DerivationPath.Custom(value.value) + NetworkStatusDM.DerivationPath.Type.NONE -> Network.DerivationPath.None } } @@ -25,9 +24,9 @@ object NetworkDerivationPathConverter : return NetworkStatusDM.DerivationPath( value = value.value.orEmpty(), type = when (value) { - is Network.DerivationPath.Card -> Type.CARD - is Network.DerivationPath.Custom -> Type.CUSTOM - Network.DerivationPath.None -> Type.NONE + is Network.DerivationPath.Card -> NetworkStatusDM.DerivationPath.Type.CARD + is Network.DerivationPath.Custom -> NetworkStatusDM.DerivationPath.Type.CUSTOM + Network.DerivationPath.None -> NetworkStatusDM.DerivationPath.Type.NONE }, ) } diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/network/converter/NetworkStatusDataModelConverter.kt b/data/networks/src/main/java/com/tangem/data/networks/converters/NetworkStatusDataModelConverter.kt similarity index 92% rename from core/datasource/src/main/java/com/tangem/datasource/local/network/converter/NetworkStatusDataModelConverter.kt rename to data/networks/src/main/java/com/tangem/data/networks/converters/NetworkStatusDataModelConverter.kt index 55ab2be060..f0c3cfb572 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/local/network/converter/NetworkStatusDataModelConverter.kt +++ b/data/networks/src/main/java/com/tangem/data/networks/converters/NetworkStatusDataModelConverter.kt @@ -1,4 +1,4 @@ -package com.tangem.datasource.local.network.converter +package com.tangem.data.networks.converters import com.tangem.datasource.local.network.entity.NetworkStatusDM import com.tangem.domain.tokens.model.NetworkStatus @@ -9,7 +9,7 @@ import com.tangem.utils.converter.Converter * [REDACTED_AUTHOR] */ -object NetworkStatusDataModelConverter : Converter { +internal object NetworkStatusDataModelConverter : Converter { override fun convert(value: NetworkStatus): NetworkStatusDM? { return when (val status = value.value) { diff --git a/data/networks/src/main/java/com/tangem/data/networks/converters/SimpleNetworkStatusConverter.kt b/data/networks/src/main/java/com/tangem/data/networks/converters/SimpleNetworkStatusConverter.kt index 5bfe00747d..314a02853a 100644 --- a/data/networks/src/main/java/com/tangem/data/networks/converters/SimpleNetworkStatusConverter.kt +++ b/data/networks/src/main/java/com/tangem/data/networks/converters/SimpleNetworkStatusConverter.kt @@ -1,9 +1,6 @@ package com.tangem.data.networks.converters import com.tangem.data.networks.models.SimpleNetworkStatus -import com.tangem.datasource.local.network.converter.NetworkAddressConverter -import com.tangem.datasource.local.network.converter.NetworkAmountsConverter -import com.tangem.datasource.local.network.converter.NetworkDerivationPathConverter import com.tangem.datasource.local.network.entity.NetworkStatusDM import com.tangem.domain.models.StatusSource import com.tangem.domain.tokens.model.NetworkStatus diff --git a/data/networks/src/main/java/com/tangem/data/networks/di/NetworkDataModule.kt b/data/networks/src/main/java/com/tangem/data/networks/di/NetworkDataModule.kt index 368dad5d0e..b0f097e74c 100644 --- a/data/networks/src/main/java/com/tangem/data/networks/di/NetworkDataModule.kt +++ b/data/networks/src/main/java/com/tangem/data/networks/di/NetworkDataModule.kt @@ -1,25 +1,54 @@ package com.tangem.data.networks.di +import android.content.Context import androidx.datastore.core.DataStore +import androidx.datastore.core.DataStoreFactory +import androidx.datastore.dataStoreFile +import com.squareup.moshi.Moshi import com.tangem.data.common.currency.CardCryptoCurrencyFactory import com.tangem.data.networks.repository.DefaultNetworksRepository import com.tangem.data.networks.store.DefaultNetworksStatusesStoreV2 import com.tangem.data.networks.store.NetworksStatusesStoreV2 +import com.tangem.datasource.di.NetworkMoshi import com.tangem.datasource.local.datastore.RuntimeSharedStore import com.tangem.datasource.local.network.entity.NetworkStatusDM +import com.tangem.datasource.utils.MoshiDataStoreSerializer +import com.tangem.datasource.utils.mapWithStringKeyTypes +import com.tangem.datasource.utils.setTypes import com.tangem.domain.networks.repository.NetworksRepository import com.tangem.domain.walletmanager.WalletManagersFacade import com.tangem.utils.coroutines.CoroutineDispatcherProvider import dagger.Module import dagger.Provides import dagger.hilt.InstallIn +import dagger.hilt.android.qualifiers.ApplicationContext import dagger.hilt.components.SingletonComponent +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.SupervisorJob import javax.inject.Singleton @Module @InstallIn(SingletonComponent::class) internal object NetworkDataModule { + @Singleton + @Provides + fun providePersistenceNetworksStatusesStore( + @NetworkMoshi moshi: Moshi, + @ApplicationContext context: Context, + dispatchers: CoroutineDispatcherProvider, + ): DataStore>> { + return DataStoreFactory.create( + serializer = MoshiDataStoreSerializer( + moshi = moshi, + types = mapWithStringKeyTypes(valueTypes = setTypes()), + defaultValue = emptyMap(), + ), + produceFile = { context.dataStoreFile(fileName = "networks_statuses") }, + scope = CoroutineScope(context = dispatchers.io + SupervisorJob()), + ) + } + @Provides @Singleton fun provideNetworksStatusesStoreV2( diff --git a/data/networks/src/main/java/com/tangem/data/networks/store/DefaultNetworksStatusesStoreV2.kt b/data/networks/src/main/java/com/tangem/data/networks/store/DefaultNetworksStatusesStoreV2.kt index 7bc6845b13..8f438408cc 100644 --- a/data/networks/src/main/java/com/tangem/data/networks/store/DefaultNetworksStatusesStoreV2.kt +++ b/data/networks/src/main/java/com/tangem/data/networks/store/DefaultNetworksStatusesStoreV2.kt @@ -1,10 +1,10 @@ package com.tangem.data.networks.store import androidx.datastore.core.DataStore +import com.tangem.data.networks.converters.NetworkStatusDataModelConverter import com.tangem.data.networks.converters.SimpleNetworkStatusConverter import com.tangem.data.networks.models.SimpleNetworkStatus import com.tangem.datasource.local.datastore.RuntimeSharedStore -import com.tangem.datasource.local.network.converter.NetworkStatusDataModelConverter import com.tangem.datasource.local.network.entity.NetworkStatusDM import com.tangem.domain.models.StatusSource import com.tangem.domain.tokens.model.Network diff --git a/data/networks/src/test/java/com/tangem/data/networks/NetworkStatusExt.kt b/data/networks/src/test/java/com/tangem/data/networks/NetworkStatusExt.kt index 1b19c1bc0f..f82fcb835c 100644 --- a/data/networks/src/test/java/com/tangem/data/networks/NetworkStatusExt.kt +++ b/data/networks/src/test/java/com/tangem/data/networks/NetworkStatusExt.kt @@ -1,7 +1,7 @@ package com.tangem.data.networks +import com.tangem.data.networks.converters.NetworkStatusDataModelConverter import com.tangem.data.networks.models.SimpleNetworkStatus -import com.tangem.datasource.local.network.converter.NetworkStatusDataModelConverter import com.tangem.datasource.local.network.entity.NetworkStatusDM import com.tangem.domain.tokens.model.NetworkStatus From 177bdfad5a0649a7bcff049e394e5315051c0ebe Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 15 May 2025 19:09:04 +0500 Subject: [PATCH 040/165] Updated on 2026-08-14 --- app/src/main/AndroidManifest.xml | 4 +-- .../main/java/com/tangem/tap/MainActivity.kt | 16 +++++++++-- .../tangem/tap/di/routing/AppRouterModule.kt | 8 ++++++ .../tangem/tap/features/main/MainViewModel.kt | 6 +++- common/routing/build.gradle.kts | 1 + .../common/routing/RoutingFeatureToggle.kt | 11 ++++++++ .../configs/feature_toggles_config.json | 4 +++ .../DefaultTokenDetailsComponent.kt | 28 +++++++++++-------- .../wallet/child/wallet/model/WalletModel.kt | 13 ++++++--- .../implementors/MultiWalletContentLoader.kt | 3 ++ .../MultiWalletContentLoaderFactory.kt | 3 ++ .../SingleWalletWithTokenContentLoader.kt | 3 ++ ...ngleWalletWithTokenContentLoaderFactory.kt | 3 ++ .../subscribers/BasicTokenListSubscriber.kt | 10 +++++-- .../MultiWalletTokenListSubscriber.kt | 3 ++ .../SingleWalletWithTokenListSubscriber.kt | 3 ++ 16 files changed, 95 insertions(+), 24 deletions(-) create mode 100644 common/routing/src/main/kotlin/com/tangem/common/routing/RoutingFeatureToggle.kt diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index cac33d689c..826b723d16 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -128,7 +128,7 @@ @@ -150,7 +150,7 @@ diff --git a/app/src/main/java/com/tangem/tap/MainActivity.kt b/app/src/main/java/com/tangem/tap/MainActivity.kt index 66df0f2d73..b3ad882b44 100644 --- a/app/src/main/java/com/tangem/tap/MainActivity.kt +++ b/app/src/main/java/com/tangem/tap/MainActivity.kt @@ -25,6 +25,7 @@ import androidx.lifecycle.flowWithLifecycle import androidx.lifecycle.lifecycleScope import arrow.core.getOrElse import com.tangem.common.routing.AppRoute +import com.tangem.common.routing.RoutingFeatureToggle import com.tangem.common.routing.entity.SerializableIntent import com.tangem.core.analytics.api.AnalyticsEventHandler import com.tangem.core.decompose.context.AppComponentContext @@ -171,6 +172,9 @@ class MainActivity : AppCompatActivity(), ActivityResultCallbackHolder { @Inject internal lateinit var defaultDeviceFlipDetector: DefaultDeviceFlipDetector + @Inject + internal lateinit var routingFeatureToggle: RoutingFeatureToggle + internal val viewModel: MainViewModel by viewModels() private lateinit var appThemeModeFlow: SharedFlow @@ -225,7 +229,11 @@ class MainActivity : AppCompatActivity(), ActivityResultCallbackHolder { if (intent != null && savedInstanceState == null) { // handle intent only on start, not on recreate - deepLinksRegistry.launch(intent) + if (routingFeatureToggle.isDeepLinkNavigationEnabled) { + // todo [REDACTED_TASK_KEY] + } else { + deepLinksRegistry.launch(intent) + } } lifecycle.addObserver(WindowObscurationObserver) @@ -374,7 +382,11 @@ class MainActivity : AppCompatActivity(), ActivityResultCallbackHolder { } if (intent != null) { - deepLinksRegistry.launch(intent) + if (routingFeatureToggle.isDeepLinkNavigationEnabled) { + // todo [REDACTED_TASK_KEY] + } else { + deepLinksRegistry.launch(intent) + } } } diff --git a/app/src/main/java/com/tangem/tap/di/routing/AppRouterModule.kt b/app/src/main/java/com/tangem/tap/di/routing/AppRouterModule.kt index 3bfc0b3a44..a95acf5cd6 100644 --- a/app/src/main/java/com/tangem/tap/di/routing/AppRouterModule.kt +++ b/app/src/main/java/com/tangem/tap/di/routing/AppRouterModule.kt @@ -1,7 +1,9 @@ package com.tangem.tap.di.routing import com.tangem.common.routing.AppRouter +import com.tangem.common.routing.RoutingFeatureToggle import com.tangem.core.analytics.api.AnalyticsExceptionHandler +import com.tangem.core.configtoggle.feature.FeatureTogglesManager import com.tangem.tap.routing.ProxyAppRouter import com.tangem.tap.routing.configurator.AppRouterConfig import com.tangem.tap.routing.configurator.MutableAppRouterConfig @@ -31,4 +33,10 @@ internal object AppRouterModule { @Provides @Singleton fun provideAppRouterConfigurator(): AppRouterConfig = MutableAppRouterConfig() + + @Provides + @Singleton + fun provideRoutingFeatureToggle(featureTogglesManager: FeatureTogglesManager): RoutingFeatureToggle { + return RoutingFeatureToggle(featureTogglesManager) + } } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/main/MainViewModel.kt b/app/src/main/java/com/tangem/tap/features/main/MainViewModel.kt index c00ae2518f..8911eebb8c 100644 --- a/app/src/main/java/com/tangem/tap/features/main/MainViewModel.kt +++ b/app/src/main/java/com/tangem/tap/features/main/MainViewModel.kt @@ -4,6 +4,7 @@ import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.tangem.blockchainsdk.BlockchainSDKFactory import com.tangem.common.keyboard.KeyboardValidator +import com.tangem.common.routing.RoutingFeatureToggle import com.tangem.core.analytics.Analytics import com.tangem.core.analytics.api.AnalyticsEventHandler import com.tangem.core.analytics.models.event.TechAnalyticsEvent @@ -67,6 +68,7 @@ internal class MainViewModel @Inject constructor( private val onboardingRepository: OnboardingRepository, private val deepLinksRegistry: DeepLinksRegistry, private val onrampDeepLinkFactory: OnrampDeepLink.Factory, + routingFeatureToggle: RoutingFeatureToggle, getBalanceHidingSettingsUseCase: GetBalanceHidingSettingsUseCase, ) : ViewModel() { @@ -102,7 +104,9 @@ internal class MainViewModel @Inject constructor( preloadImages() - initializeDeepLinks() + if (!routingFeatureToggle.isDeepLinkNavigationEnabled) { + initializeDeepLinks() + } } fun checkForUnfinishedBackup() { diff --git a/common/routing/build.gradle.kts b/common/routing/build.gradle.kts index 285041a42a..75ff433c7e 100644 --- a/common/routing/build.gradle.kts +++ b/common/routing/build.gradle.kts @@ -12,6 +12,7 @@ android { dependencies { /* Core */ implementation(projects.core.decompose) + implementation(projects.core.configToggles) /* Domain */ implementation(projects.domain.qrScanning.models) diff --git a/common/routing/src/main/kotlin/com/tangem/common/routing/RoutingFeatureToggle.kt b/common/routing/src/main/kotlin/com/tangem/common/routing/RoutingFeatureToggle.kt new file mode 100644 index 0000000000..9574ce9bf3 --- /dev/null +++ b/common/routing/src/main/kotlin/com/tangem/common/routing/RoutingFeatureToggle.kt @@ -0,0 +1,11 @@ +package com.tangem.common.routing + +import com.tangem.core.configtoggle.feature.FeatureTogglesManager + +class RoutingFeatureToggle( + private val featureTogglesManager: FeatureTogglesManager, +) { + + val isDeepLinkNavigationEnabled: Boolean + get() = featureTogglesManager.isFeatureEnabled(name = "DEEPLINK_NAVIGATION_ENABLED") +} \ No newline at end of file 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 4d74917c2a..79160bda24 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 @@ -59,6 +59,10 @@ "name": "STAKING_LOADING_REFACTORING_ENABLED", "version": "5.25.0" }, + { + "name": "DEEPLINK_NAVIGATION_ENABLED", + "version": "5.25.0" + }, { "name": "PUSH_NOTIFICATIONS_ENABLED", "version": "undefined" diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/DefaultTokenDetailsComponent.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/DefaultTokenDetailsComponent.kt index baa21915e4..d2352860a5 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/DefaultTokenDetailsComponent.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/DefaultTokenDetailsComponent.kt @@ -5,6 +5,7 @@ import androidx.compose.runtime.getValue import androidx.compose.ui.Modifier import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.arkivanov.essenty.lifecycle.subscribe +import com.tangem.common.routing.RoutingFeatureToggle import com.tangem.core.decompose.context.AppComponentContext import com.tangem.core.decompose.context.child import com.tangem.core.decompose.model.getOrCreateModel @@ -32,6 +33,7 @@ internal class DefaultTokenDetailsComponent @AssistedInject constructor( txHistoryComponentFactory: TxHistoryComponent.Factory, txHistoryFeatureToggles: TxHistoryFeatureToggles, onrampFeatureToggles: OnrampFeatureToggles, + routingFeatureToggle: RoutingFeatureToggle, deepLinksRegistry: DeepLinksRegistry, ) : TokenDetailsComponent, AppComponentContext by appComponentContext { @@ -51,20 +53,22 @@ internal class DefaultTokenDetailsComponent @AssistedInject constructor( onResume = model::onResume, ) - val deeplinks = buildList { - if (!onrampFeatureToggles.isFeatureEnabled) { - add( - BuyCurrencyDeepLink( - onReceive = model::onBuyCurrencyDeepLink, - ), - ) + if (!routingFeatureToggle.isDeepLinkNavigationEnabled) { + val deeplinks = buildList { + if (!onrampFeatureToggles.isFeatureEnabled) { + add( + BuyCurrencyDeepLink( + onReceive = model::onBuyCurrencyDeepLink, + ), + ) + } } - } - registerDeepLinks( - registry = deepLinksRegistry, - deeplinks, - ) + registerDeepLinks( + registry = deepLinksRegistry, + deepLinks = deeplinks, + ) + } } private val tokenMarketBlockComponent = params.currency.toTokenMarketParam()?.let { tokenMarketParams -> 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 4922cf66c1..1fe0628b59 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 @@ -6,6 +6,7 @@ import com.arkivanov.decompose.router.slot.activate import com.arkivanov.decompose.router.slot.dismiss import com.tangem.common.routing.AppRoute import com.tangem.common.routing.AppRouter +import com.tangem.common.routing.RoutingFeatureToggle import com.tangem.core.analytics.api.AnalyticsEventHandler import com.tangem.core.analytics.models.AnalyticsParam import com.tangem.core.analytics.models.event.MainScreenAnalyticsEvent @@ -82,6 +83,7 @@ internal class WalletModel @Inject constructor( private val deepLinksRegistry: DeepLinksRegistry, private val fetchCurrencyStatusUseCase: FetchCurrencyStatusUseCase, private val appRouter: AppRouter, + private val routingFeatureToggle: RoutingFeatureToggle, val screenLifecycleProvider: ScreenLifecycleProvider, val innerWalletRouter: InnerWalletRouter, ) : Model() { @@ -213,10 +215,13 @@ internal class WalletModel @Inject constructor( if (selectedWallet.isMultiCurrency) { selectedWalletAnalyticsSender.send(selectedWallet) } - // Registering here, because `WalletDeepLinksHandler` unregisters deeplink when scope is cancelled - // This is temporary solution, will be removed with complete deeplink navigation overhaul - addReferralDeepLink(selectedWallet) - walletDeepLinksHandler.registerForWallet(scope = modelScope, userWallet = selectedWallet) + + if (!routingFeatureToggle.isDeepLinkNavigationEnabled) { + // Registering here, because `WalletDeepLinksHandler` unregisters deeplink when scope is cancelled + // This is temporary solution, will be removed with complete deeplink navigation overhaul + addReferralDeepLink(selectedWallet) + walletDeepLinksHandler.registerForWallet(scope = modelScope, userWallet = selectedWallet) + } subscribeOnExpressTransactionsUpdates(selectedWallet) subscribeToScreenBackgroundState(selectedWallet) } diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/loaders/implementors/MultiWalletContentLoader.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/loaders/implementors/MultiWalletContentLoader.kt index 238eecd063..62325f8bfe 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/loaders/implementors/MultiWalletContentLoader.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/loaders/implementors/MultiWalletContentLoader.kt @@ -1,5 +1,6 @@ package com.tangem.feature.wallet.presentation.wallet.loaders.implementors +import com.tangem.common.routing.RoutingFeatureToggle import com.tangem.core.decompose.di.ModelScoped import com.tangem.core.deeplink.DeepLinksRegistry import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase @@ -46,6 +47,7 @@ internal class MultiWalletContentLoader( private val deepLinksRegistry: DeepLinksRegistry, private val nftFeatureToggles: NFTFeatureToggles, private val walletsRepository: WalletsRepository, + private val routingFeatureToggle: RoutingFeatureToggle, ) : WalletContentLoader(id = userWallet.walletId) { override fun create(): List { @@ -61,6 +63,7 @@ internal class MultiWalletContentLoader( applyTokenListSortingUseCase = applyTokenListSortingUseCase, runPolkadotAccountHealthCheckUseCase = runPolkadotAccountHealthCheckUseCase, deepLinksRegistry = deepLinksRegistry, + routingFeatureToggle = routingFeatureToggle, ).let(::add) if (nftFeatureToggles.isNFTEnabled) { WalletNFTListSubscriber( diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/loaders/implementors/MultiWalletContentLoaderFactory.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/loaders/implementors/MultiWalletContentLoaderFactory.kt index 0f4e9d37b4..44aa146b20 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/loaders/implementors/MultiWalletContentLoaderFactory.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/loaders/implementors/MultiWalletContentLoaderFactory.kt @@ -1,5 +1,6 @@ package com.tangem.feature.wallet.presentation.wallet.loaders.implementors +import com.tangem.common.routing.RoutingFeatureToggle import com.tangem.core.decompose.di.ModelScoped import com.tangem.core.deeplink.DeepLinksRegistry import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase @@ -40,6 +41,7 @@ internal class MultiWalletContentLoaderFactory @Inject constructor( private val nftFeatureToggles: NFTFeatureToggles, private val walletsRepository: WalletsRepository, private val getNFTCollectionsUseCase: GetNFTCollectionsUseCase, + private val routingFeatureToggle: RoutingFeatureToggle, ) { fun create(userWallet: UserWallet, clickIntents: WalletClickIntents): WalletContentLoader { @@ -62,6 +64,7 @@ internal class MultiWalletContentLoaderFactory @Inject constructor( nftFeatureToggles = nftFeatureToggles, walletsRepository = walletsRepository, getNFTCollectionsUseCase = getNFTCollectionsUseCase, + routingFeatureToggle = routingFeatureToggle, ) } } \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/loaders/implementors/SingleWalletWithTokenContentLoader.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/loaders/implementors/SingleWalletWithTokenContentLoader.kt index 109e4deec2..811b27a4f4 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/loaders/implementors/SingleWalletWithTokenContentLoader.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/loaders/implementors/SingleWalletWithTokenContentLoader.kt @@ -1,5 +1,6 @@ package com.tangem.feature.wallet.presentation.wallet.loaders.implementors +import com.tangem.common.routing.RoutingFeatureToggle import com.tangem.core.deeplink.DeepLinksRegistry import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase import com.tangem.domain.promo.GetStoryContentUseCase @@ -32,6 +33,7 @@ internal class SingleWalletWithTokenContentLoader( private val shouldSaveUserWalletsUseCase: ShouldSaveUserWalletsUseCase, private val getStoryContentUseCase: GetStoryContentUseCase, private val deepLinksRegistry: DeepLinksRegistry, + private val routingFeatureToggle: RoutingFeatureToggle, ) : WalletContentLoader(id = userWallet.walletId) { override fun create(): List { @@ -46,6 +48,7 @@ internal class SingleWalletWithTokenContentLoader( getSelectedAppCurrencyUseCase = getSelectedAppCurrencyUseCase, runPolkadotAccountHealthCheckUseCase = runPolkadotAccountHealthCheckUseCase, deepLinksRegistry = deepLinksRegistry, + routingFeatureToggle = routingFeatureToggle, ).let(::add) MultiWalletWarningsSubscriber( userWallet = userWallet, diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/loaders/implementors/SingleWalletWithTokenContentLoaderFactory.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/loaders/implementors/SingleWalletWithTokenContentLoaderFactory.kt index 829bb4b3d6..8f7e5cb7cf 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/loaders/implementors/SingleWalletWithTokenContentLoaderFactory.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/loaders/implementors/SingleWalletWithTokenContentLoaderFactory.kt @@ -1,5 +1,6 @@ package com.tangem.feature.wallet.presentation.wallet.loaders.implementors +import com.tangem.common.routing.RoutingFeatureToggle import com.tangem.core.decompose.di.ModelScoped import com.tangem.core.deeplink.DeepLinksRegistry import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase @@ -33,6 +34,7 @@ internal class SingleWalletWithTokenContentLoaderFactory @Inject constructor( private val shouldSaveUserWalletsUseCase: ShouldSaveUserWalletsUseCase, private val getStoryContentUseCase: GetStoryContentUseCase, private val deepLinksRegistry: DeepLinksRegistry, + private val routingFeatureToggle: RoutingFeatureToggle, ) { fun create(userWallet: UserWallet, clickIntents: WalletClickIntents): SingleWalletWithTokenContentLoader { @@ -51,6 +53,7 @@ internal class SingleWalletWithTokenContentLoaderFactory @Inject constructor( getStoryContentUseCase = getStoryContentUseCase, shouldSaveUserWalletsUseCase = shouldSaveUserWalletsUseCase, deepLinksRegistry = deepLinksRegistry, + routingFeatureToggle = routingFeatureToggle, ) } } \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/BasicTokenListSubscriber.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/BasicTokenListSubscriber.kt index e336654e43..5370e45d02 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/BasicTokenListSubscriber.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/BasicTokenListSubscriber.kt @@ -1,6 +1,7 @@ package com.tangem.feature.wallet.presentation.wallet.subscribers import arrow.core.getOrElse +import com.tangem.common.routing.RoutingFeatureToggle import com.tangem.core.deeplink.DeepLinksRegistry import com.tangem.core.deeplink.global.ReferralDeepLink import com.tangem.core.deeplink.global.SellCurrencyDeepLink @@ -39,6 +40,7 @@ internal abstract class BasicTokenListSubscriber( private val getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase, private val runPolkadotAccountHealthCheckUseCase: RunPolkadotAccountHealthCheckUseCase, private val deepLinksRegistry: DeepLinksRegistry, + private val routingFeatureToggle: RoutingFeatureToggle, ) : WalletSubscriber() { private val sendAnalyticsJobHolder = JobHolder() @@ -56,9 +58,11 @@ internal abstract class BasicTokenListSubscriber( } .distinctUntilChanged() .onEach { maybeTokenList -> - coroutineScope.launch { - onTokenListReceived(maybeTokenList) - }.saveIn(onTokenListReceivedJobHolder) + if (!routingFeatureToggle.isDeepLinkNavigationEnabled) { + coroutineScope.launch { + onTokenListReceived(maybeTokenList) + }.saveIn(onTokenListReceivedJobHolder) + } coroutineScope.launch { startCheck(maybeTokenList) } }, diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/MultiWalletTokenListSubscriber.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/MultiWalletTokenListSubscriber.kt index 260517ff46..a01b01448a 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/MultiWalletTokenListSubscriber.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/MultiWalletTokenListSubscriber.kt @@ -1,5 +1,6 @@ package com.tangem.feature.wallet.presentation.wallet.subscribers +import com.tangem.common.routing.RoutingFeatureToggle import com.tangem.core.deeplink.DeepLinksRegistry import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase import com.tangem.domain.core.lce.Lce @@ -30,6 +31,7 @@ internal class MultiWalletTokenListSubscriber( getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase, runPolkadotAccountHealthCheckUseCase: RunPolkadotAccountHealthCheckUseCase, deepLinksRegistry: DeepLinksRegistry, + routingFeatureToggle: RoutingFeatureToggle, ) : BasicTokenListSubscriber( userWallet = userWallet, stateHolder = stateHolder, @@ -39,6 +41,7 @@ internal class MultiWalletTokenListSubscriber( getSelectedAppCurrencyUseCase = getSelectedAppCurrencyUseCase, runPolkadotAccountHealthCheckUseCase = runPolkadotAccountHealthCheckUseCase, deepLinksRegistry = deepLinksRegistry, + routingFeatureToggle = routingFeatureToggle, ) { override fun tokenListFlow(coroutineScope: CoroutineScope): LceFlow { diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/SingleWalletWithTokenListSubscriber.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/SingleWalletWithTokenListSubscriber.kt index e52c903a67..467d739893 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/SingleWalletWithTokenListSubscriber.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/SingleWalletWithTokenListSubscriber.kt @@ -1,5 +1,6 @@ package com.tangem.feature.wallet.presentation.wallet.subscribers +import com.tangem.common.routing.RoutingFeatureToggle import com.tangem.core.deeplink.DeepLinksRegistry import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase import com.tangem.domain.core.lce.LceFlow @@ -25,6 +26,7 @@ internal class SingleWalletWithTokenListSubscriber( getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase, runPolkadotAccountHealthCheckUseCase: RunPolkadotAccountHealthCheckUseCase, deepLinksRegistry: DeepLinksRegistry, + routingFeatureToggle: RoutingFeatureToggle, ) : BasicTokenListSubscriber( userWallet = userWallet, stateHolder = stateHolder, @@ -34,6 +36,7 @@ internal class SingleWalletWithTokenListSubscriber( getSelectedAppCurrencyUseCase = getSelectedAppCurrencyUseCase, runPolkadotAccountHealthCheckUseCase = runPolkadotAccountHealthCheckUseCase, deepLinksRegistry = deepLinksRegistry, + routingFeatureToggle = routingFeatureToggle, ) { override fun tokenListFlow(coroutineScope: CoroutineScope): LceFlow { From edfaa5320df8cf4d90bb7781d2f41139e96fb13c Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 15 May 2025 18:35:01 +0400 Subject: [PATCH 041/165] Updated on 2026-08-14 --- core/res/src/main/res/values-de/strings.xml | 2 +- core/res/src/main/res/values-ja/strings.xml | 6 +- core/res/src/main/res/values-ru/strings.xml | 2 +- core/res/src/main/res/values/strings.xml | 8 +- .../components/divider/DividerWithPadding.kt | 23 +++ .../res/drawable/ic_nft_placeholder_20.xml | 19 ++ .../main/res/drawable/ic_recieve_new_24.xml | 15 ++ .../src/main/res/drawable/ic_send_new_24.xml | 15 ++ .../blockaid/WcEstimatedWalletChangesUM.kt | 16 ++ .../WcApproveTransactionModalBottomSheet.kt | 15 +- .../ui/blockaid/WcEstimatedWalletChangeRow.kt | 78 +++++++++ .../blockaid/WcEstimatedWalletChangesItem.kt | 165 ++++++++++++++++++ ...cSignTransactionModalBottomSheetContent.kt | 15 +- 13 files changed, 347 insertions(+), 32 deletions(-) create mode 100644 core/ui/src/main/java/com/tangem/core/ui/components/divider/DividerWithPadding.kt create mode 100644 core/ui/src/main/res/drawable/ic_nft_placeholder_20.xml create mode 100644 core/ui/src/main/res/drawable/ic_recieve_new_24.xml create mode 100644 core/ui/src/main/res/drawable/ic_send_new_24.xml create mode 100644 features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/entity/blockaid/WcEstimatedWalletChangesUM.kt create mode 100644 features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/ui/blockaid/WcEstimatedWalletChangeRow.kt create mode 100644 features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/ui/blockaid/WcEstimatedWalletChangesItem.kt diff --git a/core/res/src/main/res/values-de/strings.xml b/core/res/src/main/res/values-de/strings.xml index 12d2744c43..8b478ee2f8 100644 --- a/core/res/src/main/res/values-de/strings.xml +++ b/core/res/src/main/res/values-de/strings.xml @@ -195,6 +195,7 @@ Der Server ist nicht verfügbar. Bitte versuche es später erneut. Teilen Link teilen + Mehr anzeigen Signieren Signieren und senden Staken @@ -450,7 +451,6 @@ Top-Verlierer Beliebt Staking ist der einfachste Weg, um Belohnungen für Deine Kryptowährung zu erhalten. %s - Mehr anzeigen Verdiene bis zu %s APY Über %s diff --git a/core/res/src/main/res/values-ja/strings.xml b/core/res/src/main/res/values-ja/strings.xml index 7979f3a2f3..26dd14488e 100644 --- a/core/res/src/main/res/values-ja/strings.xml +++ b/core/res/src/main/res/values-ja/strings.xml @@ -192,6 +192,8 @@ サーバーが利用できません。しばらくしてからもう一度お試しください。 共有 リンクを共有 + 詳細を非表示 + もっと見る 署名 署名して送信 ステーキング @@ -447,7 +449,6 @@ 下落率上位 トレンド ステーキングは暗号資産で報酬を受け取る最も簡単な方法です。 %s - もっと見る 最大%s APYを獲得 %sについて @@ -552,6 +553,7 @@ 未追加 NFTを送信する 特徴 + 無題のコレクション %1$dコレクションの%2$dNFT ここをタップして最初のNFTを受け取ります NFTコレクション @@ -1265,6 +1267,8 @@ すべての接続を解除する すべてのdAppsの接続解除に関するテキスト すべてのdAppを接続解除する + ウォレットの変更の予測 + 悪意のある取引 このウォレットのプロフィールに%sネットワークを追加する ウォレットに必要なネットワークはありません 新しい接続 diff --git a/core/res/src/main/res/values-ru/strings.xml b/core/res/src/main/res/values-ru/strings.xml index 8689c2d7ff..4d8d60b701 100644 --- a/core/res/src/main/res/values-ru/strings.xml +++ b/core/res/src/main/res/values-ru/strings.xml @@ -199,6 +199,7 @@ Сервер недоступен, повторите попытку позднее Поделиться Поделиться ссылкой + Показать больше Подписать Подписать и отправить Застейкать @@ -454,7 +455,6 @@ Лидеры падения В тренде Стейкинг — простой способ получать доход с вашей криптовалюты. %s - Показать больше Получайте до %s APY О %s diff --git a/core/res/src/main/res/values/strings.xml b/core/res/src/main/res/values/strings.xml index aefa4e7626..51d8a3384e 100644 --- a/core/res/src/main/res/values/strings.xml +++ b/core/res/src/main/res/values/strings.xml @@ -195,6 +195,8 @@ The server is not available, please try again later Share Share Link + Show less + Show more Sign Sign and send Stake @@ -452,7 +454,6 @@ Top Losers Trending Staking is the easiest way to receive rewards on your crypto. %s - Show more Earn up to %s APY About %s @@ -560,6 +561,7 @@ Not Added Send NFT Traits + Untitled collection %1$d NFTs in %2$d collection Tap here to receive first NFT NFT collections @@ -1204,6 +1206,8 @@ Get now with 10% off Access 13,000+ cryptocurrencies. Buy, sell, swap, and stake with a single tap.\nLink up to three cards for a backup. Discover Tangem Wallet + Stay notified on wallet incoming transactions and Tangem updates. + Push Notifications Wallet settings Tangem Use %s or scan a card/ring to unlock access to your wallet @@ -1326,6 +1330,8 @@ Disconnect all Text about discnected all dApps Disconect All dApps + Estimated wallet changes + Malicious transaction Add the %s network to your profile for this wallet The wallet has no required networks New connection diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/divider/DividerWithPadding.kt b/core/ui/src/main/java/com/tangem/core/ui/components/divider/DividerWithPadding.kt new file mode 100644 index 0000000000..8b1181ef25 --- /dev/null +++ b/core/ui/src/main/java/com/tangem/core/ui/components/divider/DividerWithPadding.kt @@ -0,0 +1,23 @@ +package com.tangem.core.ui.components.divider + +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.HorizontalDivider +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp +import com.tangem.core.ui.res.TangemTheme + +@Composable +fun DividerWithPadding(start: Dp = 0.dp, end: Dp = 0.dp, top: Dp = 0.dp, bottom: Dp = 0.dp) { + HorizontalDivider( + modifier = Modifier.padding( + start = start, + end = end, + top = top, + bottom = bottom, + ), + thickness = TangemTheme.dimens.size1, + color = TangemTheme.colors.stroke.primary, + ) +} \ No newline at end of file diff --git a/core/ui/src/main/res/drawable/ic_nft_placeholder_20.xml b/core/ui/src/main/res/drawable/ic_nft_placeholder_20.xml new file mode 100644 index 0000000000..66b350b330 --- /dev/null +++ b/core/ui/src/main/res/drawable/ic_nft_placeholder_20.xml @@ -0,0 +1,19 @@ + + + + + + + diff --git a/core/ui/src/main/res/drawable/ic_recieve_new_24.xml b/core/ui/src/main/res/drawable/ic_recieve_new_24.xml new file mode 100644 index 0000000000..f5465cd20c --- /dev/null +++ b/core/ui/src/main/res/drawable/ic_recieve_new_24.xml @@ -0,0 +1,15 @@ + + + + diff --git a/core/ui/src/main/res/drawable/ic_send_new_24.xml b/core/ui/src/main/res/drawable/ic_send_new_24.xml new file mode 100644 index 0000000000..e938cd57cd --- /dev/null +++ b/core/ui/src/main/res/drawable/ic_send_new_24.xml @@ -0,0 +1,15 @@ + + + + diff --git a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/entity/blockaid/WcEstimatedWalletChangesUM.kt b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/entity/blockaid/WcEstimatedWalletChangesUM.kt new file mode 100644 index 0000000000..59551a7bd9 --- /dev/null +++ b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/entity/blockaid/WcEstimatedWalletChangesUM.kt @@ -0,0 +1,16 @@ +package com.tangem.features.walletconnect.transaction.entity.blockaid + +import androidx.annotation.DrawableRes +import com.tangem.core.ui.extensions.TextReference +import kotlinx.collections.immutable.ImmutableList + +internal data class WcEstimatedWalletChangesUM( + val items: ImmutableList, +) + +internal data class WcEstimatedWalletChangeUM( + @DrawableRes val iconRes: Int, + val title: TextReference, + val description: String, + val tokenIconUrl: String, +) \ No newline at end of file diff --git a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/ui/approve/WcApproveTransactionModalBottomSheet.kt b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/ui/approve/WcApproveTransactionModalBottomSheet.kt index a64be58984..3df8a72cf5 100644 --- a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/ui/approve/WcApproveTransactionModalBottomSheet.kt +++ b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/ui/approve/WcApproveTransactionModalBottomSheet.kt @@ -6,7 +6,6 @@ import androidx.compose.foundation.background import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.* import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.material3.HorizontalDivider import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip @@ -14,11 +13,11 @@ import androidx.compose.ui.tooling.preview.Devices import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider -import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig import com.tangem.core.ui.components.bottomsheets.modal.TangemModalBottomSheet import com.tangem.core.ui.components.bottomsheets.modal.TangemModalBottomSheetTitle +import com.tangem.core.ui.components.divider.DividerWithPadding import com.tangem.core.ui.extensions.resourceReference import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreview @@ -112,18 +111,6 @@ private fun WcApproveTransactionItems(state: WcApproveTransactionItemUM) { } } -@Composable -internal fun DividerWithPadding(start: Dp, end: Dp) { - HorizontalDivider( - modifier = Modifier.padding( - start = start, - end = end, - ), - thickness = TangemTheme.dimens.size1, - color = TangemTheme.colors.stroke.primary, - ) -} - @Composable @Preview(showBackground = true, device = Devices.PIXEL_7_PRO) @Preview(showBackground = true, device = Devices.PIXEL_7_PRO, uiMode = Configuration.UI_MODE_NIGHT_YES) diff --git a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/ui/blockaid/WcEstimatedWalletChangeRow.kt b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/ui/blockaid/WcEstimatedWalletChangeRow.kt new file mode 100644 index 0000000000..e4dcdea2ea --- /dev/null +++ b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/ui/blockaid/WcEstimatedWalletChangeRow.kt @@ -0,0 +1,78 @@ +package com.tangem.features.walletconnect.transaction.ui.blockaid + +import androidx.compose.foundation.Image +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Text +import androidx.compose.runtime.* +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.unit.dp +import coil.compose.AsyncImage +import com.tangem.core.ui.extensions.resolveReference +import com.tangem.core.ui.res.TangemTheme +import com.tangem.features.walletconnect.impl.R +import com.tangem.features.walletconnect.transaction.entity.blockaid.WcEstimatedWalletChangeUM + +@Composable +internal fun WcEstimatedWalletChangeRow(item: WcEstimatedWalletChangeUM, modifier: Modifier = Modifier) { + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = modifier + .fillMaxWidth(), + ) { + Image( + painter = painterResource(id = item.iconRes), + contentDescription = null, + modifier = Modifier.size(24.dp), + ) + + Spacer(modifier = Modifier.width(12.dp)) + + Box( + modifier = Modifier.weight(1f), + contentAlignment = Alignment.CenterStart, + ) { + Row( + modifier = Modifier.fillMaxWidth(), + verticalAlignment = Alignment.CenterVertically, + ) { + Text( + text = item.title.resolveReference(), + style = TangemTheme.typography.body1, + color = TangemTheme.colors.text.primary1, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + ) + + Spacer(modifier = Modifier.width(8.dp)) + + Text( + text = item.description, + style = TangemTheme.typography.body1, + color = TangemTheme.colors.text.tertiary, + textAlign = TextAlign.End, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + modifier = Modifier.weight(1f), + ) + } + } + + Spacer(modifier = Modifier.width(12.dp)) + + AsyncImage( + model = item.tokenIconUrl, + contentDescription = null, + modifier = Modifier + .size(24.dp) + .clip(RoundedCornerShape(4.dp)), + placeholder = painterResource(R.drawable.ic_nft_placeholder_20), + error = painterResource(R.drawable.ic_nft_placeholder_20), + ) + } +} \ No newline at end of file diff --git a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/ui/blockaid/WcEstimatedWalletChangesItem.kt b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/ui/blockaid/WcEstimatedWalletChangesItem.kt new file mode 100644 index 0000000000..b0b37f31ac --- /dev/null +++ b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/ui/blockaid/WcEstimatedWalletChangesItem.kt @@ -0,0 +1,165 @@ +package com.tangem.features.walletconnect.transaction.ui.blockaid + +import android.content.res.Configuration +import androidx.compose.animation.animateContentSize +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Icon +import androidx.compose.material3.Text +import androidx.compose.runtime.* +import androidx.compose.ui.Alignment +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.Devices +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.tooling.preview.PreviewParameter +import androidx.compose.ui.tooling.preview.PreviewParameterProvider +import androidx.compose.ui.unit.dp +import com.tangem.core.ui.extensions.resourceReference +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.graphics.vector.rememberVectorPainter +import androidx.compose.ui.res.vectorResource +import com.tangem.core.ui.components.divider.DividerWithPadding +import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.res.TangemThemePreview +import com.tangem.features.walletconnect.impl.R +import com.tangem.features.walletconnect.transaction.entity.blockaid.WcEstimatedWalletChangeUM +import com.tangem.features.walletconnect.transaction.entity.blockaid.WcEstimatedWalletChangesUM +import com.tangem.features.walletconnect.transaction.ui.common.WcSmallTitleItem +import kotlinx.collections.immutable.persistentListOf +import kotlinx.collections.immutable.toPersistentList + +private const val MAX_CHANGES_SIZE = 4 + +@Composable +internal fun WcEstimatedWalletChangesItem(item: WcEstimatedWalletChangesUM, modifier: Modifier = Modifier) { + var isExpanded by remember { mutableStateOf(false) } + + Column( + modifier = modifier + .clip(RoundedCornerShape(14.dp)) + .background(color = TangemTheme.colors.background.action) + .fillMaxWidth() + .animateContentSize(), + ) { + WcSmallTitleItem( + textRex = R.string.wc_estimated_wallet_changes, + modifier = Modifier + .padding(bottom = 16.dp), + ) + + val itemsToDisplay = if (isExpanded) item.items else item.items.take(MAX_CHANGES_SIZE) + + itemsToDisplay.forEachIndexed { idx, row -> + key(row.hashCode()) { + WcEstimatedWalletChangeRow( + item = row, + modifier = Modifier.padding(horizontal = 12.dp), + ) + if (idx == item.items.lastIndex) { + Spacer(modifier = Modifier.height(12.dp)) + } else { + DividerWithPadding(start = 48.dp, end = 12.dp, top = 8.dp, bottom = 8.dp) + } + } + } + + if (item.items.size > MAX_CHANGES_SIZE) { + Row( + modifier = Modifier + .clickable { isExpanded = !isExpanded } + .padding(bottom = 12.dp, start = 12.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + Text( + text = stringResource(if (isExpanded) R.string.common_show_less else R.string.common_show_more), + style = TangemTheme.typography.body1, + color = TangemTheme.colors.text.tertiary, + ) + Icon( + painter = rememberVectorPainter( + image = ImageVector.vectorResource(id = R.drawable.ic_chevron_24), + ), + tint = TangemTheme.colors.icon.informative, + modifier = Modifier + .size(20.dp) + .padding(start = 2.dp), + contentDescription = null, + ) + } + } + } +} + +@Composable +@Preview(showBackground = true, device = Devices.PIXEL_7_PRO) +@Preview(showBackground = true, device = Devices.PIXEL_7_PRO, uiMode = Configuration.UI_MODE_NIGHT_YES) +private fun EstimatedWalletChangesPreviewTwoItems( + @PreviewParameter(EstimatedWalletChangesPreviewProviderTwoItems::class) item: WcEstimatedWalletChangesUM, +) { + TangemThemePreview { + Box( + modifier = Modifier + .background(TangemTheme.colors.background.tertiary), + ) { + WcEstimatedWalletChangesItem(item = item) + } + } +} + +@Composable +@Preview(showBackground = true, device = Devices.PIXEL_7_PRO) +@Preview(showBackground = true, device = Devices.PIXEL_7_PRO, uiMode = Configuration.UI_MODE_NIGHT_YES) +private fun EstimatedWalletChangesPreviewMoreThanFour( + @PreviewParameter(EstimatedWalletChangesPreviewProviderManyItems::class) item: WcEstimatedWalletChangesUM, +) { + TangemThemePreview { + Box( + modifier = Modifier + .background(TangemTheme.colors.background.tertiary), + ) { + WcEstimatedWalletChangesItem(item = item) + } + } +} + +private class EstimatedWalletChangesPreviewProviderTwoItems : + PreviewParameterProvider { + override val values = sequenceOf( + WcEstimatedWalletChangesUM( + items = persistentListOf( + WcEstimatedWalletChangeUM( + iconRes = R.drawable.ic_send_new_24, + title = resourceReference(R.string.common_send), + description = "- 42 USDT", + tokenIconUrl = "https://tangem.com", + ), + WcEstimatedWalletChangeUM( + iconRes = R.drawable.ic_recieve_new_24, + title = resourceReference(R.string.common_receive), + description = "+ 1,131.46 MATIC", + tokenIconUrl = "https://tangem.com", + ), + ), + ), + ) +} + +private class EstimatedWalletChangesPreviewProviderManyItems : + PreviewParameterProvider { + override val values = sequenceOf( + WcEstimatedWalletChangesUM( + items = (1..6).map { + WcEstimatedWalletChangeUM( + iconRes = R.drawable.ic_send_new_24, + title = resourceReference(R.string.common_send), + description = "Nethers #1111", + tokenIconUrl = "https://tangem.com", + ) + }.toPersistentList(), + ), + ) +} \ No newline at end of file diff --git a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/ui/sign/WcSignTransactionModalBottomSheetContent.kt b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/ui/sign/WcSignTransactionModalBottomSheetContent.kt index 4596bcc2b6..395b577374 100644 --- a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/ui/sign/WcSignTransactionModalBottomSheetContent.kt +++ b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/ui/sign/WcSignTransactionModalBottomSheetContent.kt @@ -8,7 +8,6 @@ import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.material3.HorizontalDivider import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip @@ -16,11 +15,11 @@ import androidx.compose.ui.tooling.preview.Devices import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider -import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig import com.tangem.core.ui.components.bottomsheets.modal.TangemModalBottomSheet import com.tangem.core.ui.components.bottomsheets.modal.TangemModalBottomSheetTitle +import com.tangem.core.ui.components.divider.DividerWithPadding import com.tangem.core.ui.extensions.resourceReference import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreview @@ -109,18 +108,6 @@ private fun WcSignTransactionItems(state: WcSignTransactionItemUM) { } } -@Composable -internal fun DividerWithPadding(start: Dp, end: Dp) { - HorizontalDivider( - modifier = Modifier.padding( - start = start, - end = end, - ), - thickness = 1.dp, - color = TangemTheme.colors.stroke.primary, - ) -} - @Composable @Preview(showBackground = true, device = Devices.PIXEL_7_PRO) @Preview(showBackground = true, device = Devices.PIXEL_7_PRO, uiMode = Configuration.UI_MODE_NIGHT_YES) From 4cd56a1588d3bf2c3eeec03d53b7c61ef13980d2 Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 15 May 2025 21:43:57 +0700 Subject: [PATCH 042/165] Updated on 2026-08-14 --- .../walletconnect/WalletConnectSdkHelper.kt | 2 +- .../network/ethereum/WcEthMessageSignUseCase.kt | 16 +++++++--------- .../data/walletconnect/sign/BaseWcSignUseCase.kt | 10 ---------- .../walletconnect/sign/WcSignUseCaseDelegate.kt | 7 +++++++ 4 files changed, 15 insertions(+), 20 deletions(-) diff --git a/app/src/main/java/com/tangem/tap/domain/walletconnect/WalletConnectSdkHelper.kt b/app/src/main/java/com/tangem/tap/domain/walletconnect/WalletConnectSdkHelper.kt index 2ac037d2ec..0d3aaef26b 100644 --- a/app/src/main/java/com/tangem/tap/domain/walletconnect/WalletConnectSdkHelper.kt +++ b/app/src/main/java/com/tangem/tap/domain/walletconnect/WalletConnectSdkHelper.kt @@ -393,7 +393,7 @@ class WalletConnectSdkHelper { signature = signedHash, hash = hashToSign, publicKey = wallet.publicKey.blockchainKey.toDecompressedPublicKey(), - ).asRSVLegacyEVM().toHexString() + ).asRSVLegacyEVM().toHexString().formatHex() } } is CompletionResult.Failure -> { diff --git a/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/network/ethereum/WcEthMessageSignUseCase.kt b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/network/ethereum/WcEthMessageSignUseCase.kt index b1ec5e840d..b848c5812f 100644 --- a/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/network/ethereum/WcEthMessageSignUseCase.kt +++ b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/network/ethereum/WcEthMessageSignUseCase.kt @@ -5,6 +5,7 @@ import com.tangem.blockchain.blockchains.ethereum.EthereumUtils.toKeccak import com.tangem.blockchain.common.HEX_PREFIX import com.tangem.blockchain.common.UnmarshalHelper import com.tangem.blockchain.common.WalletManager +import com.tangem.blockchain.extensions.formatHex import com.tangem.blockchain.extensions.isAscii import com.tangem.common.extensions.hexToBytes import com.tangem.common.extensions.toDecompressedPublicKey @@ -81,15 +82,12 @@ internal class WcEthMessageSignUseCase @AssistedInject constructor( object LegacySdkHelper { private const val ETH_MESSAGE_PREFIX = "\u0019Ethereum Signed Message:\n" - internal fun prepareToSendMessageData( - signedHash: ByteArray, - hashToSign: ByteArray, - walletManager: WalletManager, - ): String = UnmarshalHelper.unmarshalSignatureExtended( - signature = signedHash, - hash = hashToSign, - publicKey = walletManager.wallet.publicKey.blockchainKey.toDecompressedPublicKey(), - ).asRSVLegacyEVM().toHexString() + fun prepareToSendMessageData(signedHash: ByteArray, hashToSign: ByteArray, walletManager: WalletManager): String = + UnmarshalHelper.unmarshalSignatureExtended( + signature = signedHash, + hash = hashToSign, + publicKey = walletManager.wallet.publicKey.blockchainKey.toDecompressedPublicKey(), + ).asRSVLegacyEVM().toHexString().formatHex() fun createMessageData(message: String): ByteArray { val messageData = try { diff --git a/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/sign/BaseWcSignUseCase.kt b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/sign/BaseWcSignUseCase.kt index 55ea7f8786..01c372e675 100644 --- a/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/sign/BaseWcSignUseCase.kt +++ b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/sign/BaseWcSignUseCase.kt @@ -35,16 +35,6 @@ internal abstract class BaseWcSignUseCase : ) } - init { - analytics.send( - WcAnalyticEvents.SignatureRequestReceived( - session = context.session, - rawRequest = context.rawSdkRequest, - network = context.network, - ), - ) - } - override suspend fun onCancel(currentState: WcSignState) { defaultReject() } diff --git a/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/sign/WcSignUseCaseDelegate.kt b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/sign/WcSignUseCaseDelegate.kt index 55c77f3de3..68bd9a22b5 100644 --- a/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/sign/WcSignUseCaseDelegate.kt +++ b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/sign/WcSignUseCaseDelegate.kt @@ -39,6 +39,13 @@ internal class WcSignUseCaseDelegate( operator fun invoke(initModel: SignModel) = channelFlow { val state = MutableStateFlow(WcSignState(initModel, WcSignStep.PreSign)) + analytics.send( + WcAnalyticEvents.SignatureRequestReceived( + session = context.session, + rawRequest = context.rawSdkRequest, + network = context.network, + ), + ) state .onEach { newState -> channel.send(newState) } From 17ae97606e4e9f714cd7c6b933414e3c3231e612 Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 15 May 2025 23:47:29 +0500 Subject: [PATCH 043/165] Updated on 2026-08-14 --- core/datasource/build.gradle.kts | 1 + .../local/nft/DefaultNFTRuntimeStore.kt | 2 +- .../converter/NFTSdkCollectionConverter.kt | 23 ++++++- .../NFTSdkCollectionIdentifierConverter.kt | 4 +- data/nft/build.gradle.kts | 2 +- .../tangem/data/nft/DefaultNFTRepository.kt | 7 ++- .../com/tangem/data/nft/di/NFTDataModule.kt | 35 +++++++++-- .../tangem/domain/nft/models/NFTCollection.kt | 2 +- .../nft/collections/entity/NFTCollectionUM.kt | 2 +- .../transformer/UpdateDataStateTransformer.kt | 62 ++++++------------- .../collections/model/NFTCollectionsModel.kt | 36 ++++++++++- .../nft/collections/ui/NFTCollection.kt | 3 +- gradle/tangem_dependencies.toml | 4 +- 13 files changed, 119 insertions(+), 64 deletions(-) diff --git a/core/datasource/build.gradle.kts b/core/datasource/build.gradle.kts index ebde9b43cd..b524e010af 100644 --- a/core/datasource/build.gradle.kts +++ b/core/datasource/build.gradle.kts @@ -23,6 +23,7 @@ dependencies { /** Project */ implementation(projects.core.analytics) implementation(projects.core.utils) + implementation(projects.core.res) implementation(projects.libs.auth) implementation(projects.domain.appTheme.models) implementation(projects.domain.core) diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/nft/DefaultNFTRuntimeStore.kt b/core/datasource/src/main/java/com/tangem/datasource/local/nft/DefaultNFTRuntimeStore.kt index 31a46a4a12..cd70259814 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/local/nft/DefaultNFTRuntimeStore.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/local/nft/DefaultNFTRuntimeStore.kt @@ -122,7 +122,7 @@ internal class DefaultNFTRuntimeStore( ) } ?.filter { it.count > 0 } - ?.sortedBy { it.name }, + ?.sortedBy { it.name?.lowercase() }, source = this.source, ) diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/nft/converter/NFTSdkCollectionConverter.kt b/core/datasource/src/main/java/com/tangem/datasource/local/nft/converter/NFTSdkCollectionConverter.kt index 603e003ccb..93662ba168 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/local/nft/converter/NFTSdkCollectionConverter.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/local/nft/converter/NFTSdkCollectionConverter.kt @@ -1,5 +1,7 @@ package com.tangem.datasource.local.nft.converter +import android.content.res.Resources +import com.tangem.datasource.R import com.tangem.domain.models.StatusSource import com.tangem.domain.nft.models.NFTAsset import com.tangem.domain.nft.models.NFTCollection @@ -7,14 +9,31 @@ import com.tangem.domain.tokens.model.Network import com.tangem.utils.converter.Converter import com.tangem.blockchain.nft.models.NFTCollection as SdkNFTCollection -object NFTSdkCollectionConverter : Converter, NFTCollection> { +class NFTSdkCollectionConverter( + private val resources: Resources, +) : Converter, NFTCollection> { override fun convert(value: Pair): NFTCollection { val (network, collection) = value val collectionId = NFTSdkCollectionIdentifierConverter.convert(collection.identifier) return NFTCollection( id = collectionId, network = network, - name = collection.name, + // We use localised strings from resources here to proceed sorting and searching correctly + // Sorting is invoked on data layer, searching is simplified to filtering for now and invoked in Model + name = when (collectionId) { + is NFTCollection.Identifier.EVM -> collection.name + is NFTCollection.Identifier.TON -> when { + collectionId.contractAddress == null -> resources.getString(R.string.nft_no_collection) + collection.name.isNullOrEmpty() -> resources.getString(R.string.nft_untitled_collection) + else -> collection.name + } + is NFTCollection.Identifier.Solana -> when { + collectionId.collectionAddress == null -> resources.getString(R.string.nft_no_collection) + collection.name.isNullOrEmpty() -> resources.getString(R.string.nft_untitled_collection) + else -> collection.name + } + NFTCollection.Identifier.Unknown -> null + }, description = collection.description, logoUrl = collection.logoUrl, count = collection.count, diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/nft/converter/NFTSdkCollectionIdentifierConverter.kt b/core/datasource/src/main/java/com/tangem/datasource/local/nft/converter/NFTSdkCollectionIdentifierConverter.kt index 44f1df3f89..f97ef9c812 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/local/nft/converter/NFTSdkCollectionIdentifierConverter.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/local/nft/converter/NFTSdkCollectionIdentifierConverter.kt @@ -13,7 +13,7 @@ object NFTSdkCollectionIdentifierConverter : TwoWayConverter NFTCollection.Identifier.Solana( - collection = value.collection, + collectionAddress = value.collectionAddress, ) is SdkNFTCollection.Identifier.Unknown -> NFTCollection.Identifier.Unknown } @@ -26,7 +26,7 @@ object NFTSdkCollectionIdentifierConverter : TwoWayConverter SdkNFTCollection.Identifier.Solana( - collection = value.collection, + collectionAddress = value.collectionAddress, ) is NFTCollection.Identifier.Unknown -> SdkNFTCollection.Identifier.Unknown } diff --git a/data/nft/build.gradle.kts b/data/nft/build.gradle.kts index 2e0f7cb455..63ceb134a2 100644 --- a/data/nft/build.gradle.kts +++ b/data/nft/build.gradle.kts @@ -49,6 +49,6 @@ dependencies { implementation(tangemDeps.card.core) /** DI */ - implementation(deps.hilt.core) + implementation(deps.hilt.android) kapt(deps.hilt.kapt) } \ No newline at end of file diff --git a/data/nft/src/main/kotlin/com/tangem/data/nft/DefaultNFTRepository.kt b/data/nft/src/main/kotlin/com/tangem/data/nft/DefaultNFTRepository.kt index 2747f7c697..d5d3a63d50 100644 --- a/data/nft/src/main/kotlin/com/tangem/data/nft/DefaultNFTRepository.kt +++ b/data/nft/src/main/kotlin/com/tangem/data/nft/DefaultNFTRepository.kt @@ -1,5 +1,6 @@ package com.tangem.data.nft +import android.content.res.Resources import arrow.core.Either import com.tangem.blockchain.common.Blockchain import com.tangem.blockchainsdk.utils.ExcludedBlockchains @@ -48,6 +49,7 @@ internal class DefaultNFTRepository @Inject constructor( private val excludedBlockchains: ExcludedBlockchains, private val userWalletsStore: UserWalletsStore, private val nftFeatureToggles: NFTFeatureToggles, + resources: Resources, ) : NFTRepository { private val networkJobs = ConcurrentHashMap() @@ -59,6 +61,7 @@ internal class DefaultNFTRepository @Inject constructor( private val collectionIdConverter = NFTSdkCollectionIdentifierConverter private val assetIdConverter = NFTSdkAssetIdentifierConverter + private val nftSdkCollectionConverter by lazy { NFTSdkCollectionConverter(resources) } override fun observeCollections(userWalletId: UserWalletId, networks: List): Flow> = flow { emitAll(observeCollectionsInternal(userWalletId, networks)) } @@ -319,7 +322,7 @@ internal class DefaultNFTRepository @Inject constructor( content = NFTCollections.Content.Collections( collections = collections .map { collection -> - NFTSdkCollectionConverter.convert(network to collection) + nftSdkCollectionConverter.convert(network to collection) } .filter { it.id !is NFTCollection.Identifier.Unknown @@ -417,7 +420,7 @@ internal class DefaultNFTRepository @Inject constructor( content = NFTCollections.Content.Collections( collections = it ?.map { collection -> - NFTSdkCollectionConverter.convert(network to collection) + nftSdkCollectionConverter.convert(network to collection) } ?.filter { it.id !is NFTCollection.Identifier.Unknown diff --git a/data/nft/src/main/kotlin/com/tangem/data/nft/di/NFTDataModule.kt b/data/nft/src/main/kotlin/com/tangem/data/nft/di/NFTDataModule.kt index c0af92def9..e66df70d44 100644 --- a/data/nft/src/main/kotlin/com/tangem/data/nft/di/NFTDataModule.kt +++ b/data/nft/src/main/kotlin/com/tangem/data/nft/di/NFTDataModule.kt @@ -1,18 +1,45 @@ package com.tangem.data.nft.di +import android.content.Context +import com.tangem.blockchainsdk.utils.ExcludedBlockchains import com.tangem.data.nft.DefaultNFTRepository +import com.tangem.datasource.local.nft.NFTPersistenceStoreFactory +import com.tangem.datasource.local.nft.NFTRuntimeStoreFactory +import com.tangem.datasource.local.userwallet.UserWalletsStore import com.tangem.domain.nft.repository.NFTRepository -import dagger.Binds +import com.tangem.domain.walletmanager.WalletManagersFacade +import com.tangem.features.nft.NFTFeatureToggles +import com.tangem.utils.coroutines.CoroutineDispatcherProvider import dagger.Module +import dagger.Provides import dagger.hilt.InstallIn +import dagger.hilt.android.qualifiers.ApplicationContext import dagger.hilt.components.SingletonComponent import javax.inject.Singleton @Module @InstallIn(SingletonComponent::class) -internal interface NFTDataModule { +internal object NFTDataModule { - @Binds + @Provides @Singleton - fun bindNFTRepository(repository: DefaultNFTRepository): NFTRepository + fun provideNFTRepository( + @ApplicationContext context: Context, + nftPersistenceStoreFactory: NFTPersistenceStoreFactory, + nftRuntimeStoreFactory: NFTRuntimeStoreFactory, + walletManagersFacade: WalletManagersFacade, + dispatchers: CoroutineDispatcherProvider, + excludedBlockchains: ExcludedBlockchains, + userWalletsStore: UserWalletsStore, + nftFeatureToggles: NFTFeatureToggles, + ): NFTRepository = DefaultNFTRepository( + nftPersistenceStoreFactory = nftPersistenceStoreFactory, + nftRuntimeStoreFactory = nftRuntimeStoreFactory, + walletManagersFacade = walletManagersFacade, + dispatchers = dispatchers, + excludedBlockchains = excludedBlockchains, + userWalletsStore = userWalletsStore, + nftFeatureToggles = nftFeatureToggles, + resources = context.resources, + ) } \ No newline at end of file diff --git a/domain/nft/models/src/main/kotlin/com/tangem/domain/nft/models/NFTCollection.kt b/domain/nft/models/src/main/kotlin/com/tangem/domain/nft/models/NFTCollection.kt index eee0569d01..c524870cf1 100644 --- a/domain/nft/models/src/main/kotlin/com/tangem/domain/nft/models/NFTCollection.kt +++ b/domain/nft/models/src/main/kotlin/com/tangem/domain/nft/models/NFTCollection.kt @@ -41,7 +41,7 @@ data class NFTCollection( data class TON(val contractAddress: String?) : Identifier() @Serializable - data class Solana(val collection: String?) : Identifier() + data class Solana(val collectionAddress: String?) : Identifier() @Serializable data object Unknown : Identifier() diff --git a/features/nft/impl/src/main/kotlin/com/tangem/features/nft/collections/entity/NFTCollectionUM.kt b/features/nft/impl/src/main/kotlin/com/tangem/features/nft/collections/entity/NFTCollectionUM.kt index 771f9515fc..e7e24ded3b 100644 --- a/features/nft/impl/src/main/kotlin/com/tangem/features/nft/collections/entity/NFTCollectionUM.kt +++ b/features/nft/impl/src/main/kotlin/com/tangem/features/nft/collections/entity/NFTCollectionUM.kt @@ -5,7 +5,7 @@ import com.tangem.core.ui.extensions.TextReference internal data class NFTCollectionUM( val id: String, - val name: String?, + val name: String, @DrawableRes val networkIconId: Int, val logoUrl: String?, val description: TextReference, diff --git a/features/nft/impl/src/main/kotlin/com/tangem/features/nft/collections/entity/transformer/UpdateDataStateTransformer.kt b/features/nft/impl/src/main/kotlin/com/tangem/features/nft/collections/entity/transformer/UpdateDataStateTransformer.kt index 9a7e3bc93d..cefa97a073 100644 --- a/features/nft/impl/src/main/kotlin/com/tangem/features/nft/collections/entity/transformer/UpdateDataStateTransformer.kt +++ b/features/nft/impl/src/main/kotlin/com/tangem/features/nft/collections/entity/transformer/UpdateDataStateTransformer.kt @@ -15,7 +15,6 @@ import kotlinx.collections.immutable.toPersistentList @Suppress("LongParameterList") internal class UpdateDataStateTransformer( private val nftCollections: List, - private val searchQuery: String, private val onReceiveClick: () -> Unit, private val onRetryClick: () -> Unit, private val onExpandCollectionClick: (NFTCollection) -> Unit, @@ -55,7 +54,7 @@ internal class UpdateDataStateTransformer( .map { it.content } .asSequence() .filterIsInstance() - .map { it.collections.orEmpty().transform(prevState, searchQuery) } + .map { it.collections.orEmpty().transform(prevState) } .flatten() .toPersistentList(), warnings = transformNotifications(), @@ -74,48 +73,23 @@ internal class UpdateDataStateTransformer( ) } - private fun List.transform( - state: NFTCollectionsStateUM, - query: String, - ): ImmutableList = mapNotNull { - val assetsFulfillQuery = if (query.isEmpty()) { - true - } else { - when (val assets = it.assets) { - is NFTCollection.Assets.Empty, - is NFTCollection.Assets.Failed, - is NFTCollection.Assets.Loading, - -> false - is NFTCollection.Assets.Value -> { - assets.items.any { asset -> - asset.name?.lowercase()?.contains(query.lowercase()) == true - } - } - } - } - - val collectionFulfillQuery = query.isEmpty() || it.name?.lowercase()?.contains(query.lowercase()) == true - - if (collectionFulfillQuery || assetsFulfillQuery) { - NFTCollectionUM( - id = it.collectionIdProvider(), - networkIconId = getActiveIconRes(it.network.id.value), - name = it.name, - 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), - ) - } else { - null - } + private fun List.transform(state: NFTCollectionsStateUM): ImmutableList = map { + NFTCollectionUM( + id = it.collectionIdProvider(), + networkIconId = getActiveIconRes(it.network.id.value), + 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), + ) }.toPersistentList() private fun transformNotifications(): ImmutableList = buildList { 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 1b83ed39e4..bf8bd9382e 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 @@ -11,6 +11,7 @@ import com.tangem.domain.nft.FetchNFTCollectionAssetsUseCase import com.tangem.domain.nft.GetNFTCollectionsUseCase import com.tangem.domain.nft.RefreshAllNFTUseCase import com.tangem.domain.nft.models.NFTCollection +import com.tangem.domain.nft.models.NFTCollections import com.tangem.features.nft.collections.NFTCollectionsComponent import com.tangem.features.nft.collections.entity.NFTCollectionsStateUM import com.tangem.features.nft.collections.entity.NFTCollectionsUM @@ -71,8 +72,7 @@ internal class NFTCollectionsModel @Inject constructor( ) { nftCollections, query -> _state.update { UpdateDataStateTransformer( - nftCollections = nftCollections, - searchQuery = query, + nftCollections = nftCollections.filter(query), onReceiveClick = { params.onReceiveClick() }, @@ -91,6 +91,38 @@ internal class NFTCollectionsModel @Inject constructor( .launchIn(modelScope) } + private fun List.filter(query: String): List = map { + it.copy( + content = when (val content = it.content) { + is NFTCollections.Content.Collections -> 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, + -> false + is NFTCollection.Assets.Value -> { + assets.items.any { asset -> + asset.name?.lowercase()?.contains(query.lowercase()) == true + } + } + } + } + + val collectionFulfillQuery = + query.isEmpty() || it.name?.lowercase()?.contains(query.lowercase()) == true + + collectionFulfillQuery || assetsFulfillQuery + }, + ) + is NFTCollections.Content.Error -> it.content + }, + ) + } + private fun onRefresh() { modelScope.launch { _state.update { ChangeRefreshingStateTransformer(true).transform(it) } diff --git a/features/nft/impl/src/main/kotlin/com/tangem/features/nft/collections/ui/NFTCollection.kt b/features/nft/impl/src/main/kotlin/com/tangem/features/nft/collections/ui/NFTCollection.kt index 3851542d90..e89b5bbe41 100644 --- a/features/nft/impl/src/main/kotlin/com/tangem/features/nft/collections/ui/NFTCollection.kt +++ b/features/nft/impl/src/main/kotlin/com/tangem/features/nft/collections/ui/NFTCollection.kt @@ -18,7 +18,6 @@ import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider import com.tangem.core.ui.extensions.TextReference import com.tangem.core.ui.extensions.resolveReference -import com.tangem.core.ui.extensions.stringResourceSafe import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreview import com.tangem.features.nft.collections.entity.NFTCollectionAssetsListUM @@ -90,7 +89,7 @@ private fun RowScope.Text(state: NFTCollectionUM) { ) { Text( modifier = Modifier, - text = state.name.takeUnless { it.isNullOrEmpty() } ?: stringResourceSafe(R.string.nft_no_collection), + text = state.name, style = TangemTheme.typography.subtitle2, color = TangemTheme.colors.text.primary1, maxLines = 1, diff --git a/gradle/tangem_dependencies.toml b/gradle/tangem_dependencies.toml index ccc406b5ff..994dc52f3d 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-1063" +tangemBlockchainSdk = "develop-1064" #tangemBlockchainSdk = "0.0.1" # Keep it! - used for local builds tangemCardSdk = "develop-468" #tangemCardSdk = "0.0.1" # Keep it! - used for local builds ^ @@ -21,4 +21,4 @@ card-core = { module = "com.tangem.tangem-sdk-kotlin:core", version.ref = "tange vico-compose = { group = "com.tangem.vico", name = "compose", version.ref = "tangemVico" } vico-compose-m3 = { group = "com.tangem.vico", name = "compose-m3", version.ref = "tangemVico" } -vico-core = { group = "com.tangem.vico", name = "core", version.ref = "tangemVico" } \ No newline at end of file +vico-core = { group = "com.tangem.vico", name = "core", version.ref = "tangemVico" } From 7be6ce9dba5e5cac5f1c2703c3c2e54858944194 Mon Sep 17 00:00:00 2001 From: Tangem Date: Fri, 16 May 2025 12:29:11 +0500 Subject: [PATCH 044/165] Updated on 2026-08-14 --- .../tangem/feature/walletsettings/model/WalletSettingsModel.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 3ff00087f0..b22130a159 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 @@ -103,7 +103,7 @@ internal class WalletSettingsModel @Inject constructor( isManageTokensAvailable = userWallet.isMultiCurrency, isRenameWalletAvailable = isRenameWalletAvailable, renameWallet = { openRenameWalletDialog(userWallet, dialogNavigation) }, - isNFTFeatureEnabled = isNFTFeatureEnabled, + isNFTFeatureEnabled = isNFTFeatureEnabled && userWallet.isMultiCurrency, isNFTEnabled = isNFTEnabled, onCheckedNFTChange = ::onCheckedNFTChange, forgetWallet = { From 0cd80d7c50271f34b01c6714ef429823de6cc733 Mon Sep 17 00:00:00 2001 From: Tangem Date: Fri, 16 May 2025 12:52:27 +0500 Subject: [PATCH 045/165] Updated on 2026-08-14 --- .../tangem/tap/di/domain/NFTDomainModule.kt | 23 +++++++++++++++++++ .../local/nft/DefaultNFTPersistenceStore.kt | 5 ++++ .../local/nft/DefaultNFTRuntimeStore.kt | 10 ++++++++ .../local/nft/NFTPersistenceStore.kt | 2 ++ .../datasource/local/nft/NFTRuntimeStore.kt | 2 ++ .../tangem/data/nft/DefaultNFTRepository.kt | 7 ++++++ domain/nft/build.gradle.kts | 1 + .../domain/nft/DisableWalletNFTUseCase.kt | 21 +++++++++++++++++ .../domain/nft/EnableWalletNFTUseCase.kt | 13 +++++++++++ .../domain/nft/GetWalletNFTEnabledUseCase.kt | 13 +++++++++++ .../domain/nft/repository/NFTRepository.kt | 2 ++ .../wallet-settings/impl/build.gradle.kts | 1 + .../model/WalletSettingsModel.kt | 14 +++++++---- 13 files changed, 109 insertions(+), 5 deletions(-) create mode 100644 domain/nft/src/main/kotlin/com/tangem/domain/nft/DisableWalletNFTUseCase.kt create mode 100644 domain/nft/src/main/kotlin/com/tangem/domain/nft/EnableWalletNFTUseCase.kt create mode 100644 domain/nft/src/main/kotlin/com/tangem/domain/nft/GetWalletNFTEnabledUseCase.kt diff --git a/app/src/main/java/com/tangem/tap/di/domain/NFTDomainModule.kt b/app/src/main/java/com/tangem/tap/di/domain/NFTDomainModule.kt index def43853a4..26161f3d76 100644 --- a/app/src/main/java/com/tangem/tap/di/domain/NFTDomainModule.kt +++ b/app/src/main/java/com/tangem/tap/di/domain/NFTDomainModule.kt @@ -6,6 +6,7 @@ import com.tangem.domain.nft.repository.NFTRepository import com.tangem.domain.quotes.single.SingleQuoteFetcher import com.tangem.domain.quotes.single.SingleQuoteSupplier import com.tangem.domain.tokens.repository.CurrenciesRepository +import com.tangem.domain.wallets.repository.WalletsRepository import com.tangem.utils.coroutines.CoroutineDispatcherProvider import dagger.Module import dagger.Provides @@ -104,4 +105,26 @@ internal object NFTDomainModule { ): FetchNFTPriceUseCase { return FetchNFTPriceUseCase(nftRepository, singleQuoteFetcher) } + + @Provides + @Singleton + fun provideEnableWalletNFTUseCase(walletsRepository: WalletsRepository): EnableWalletNFTUseCase { + return EnableWalletNFTUseCase(walletsRepository) + } + + @Provides + @Singleton + fun provideDisableWalletNFTUseCase( + walletsRepository: WalletsRepository, + nftRepository: NFTRepository, + currenciesRepository: CurrenciesRepository, + ): DisableWalletNFTUseCase { + return DisableWalletNFTUseCase(walletsRepository, nftRepository, currenciesRepository) + } + + @Provides + @Singleton + fun provideGetWalletNFTEnabledUseCase(walletsRepository: WalletsRepository): GetWalletNFTEnabledUseCase { + return GetWalletNFTEnabledUseCase(walletsRepository) + } } \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/nft/DefaultNFTPersistenceStore.kt b/core/datasource/src/main/java/com/tangem/datasource/local/nft/DefaultNFTPersistenceStore.kt index 51b17b2bbf..3018b1b4be 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/local/nft/DefaultNFTPersistenceStore.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/local/nft/DefaultNFTPersistenceStore.kt @@ -47,5 +47,10 @@ internal class DefaultNFTPersistenceStore( } } + override suspend fun clear() { + collectionsPersistenceStore.updateData { emptyList() } + pricesPersistenceStore.updateData { emptyList() } + } + private fun NFTCollection.getAsset(assetId: NFTAsset.Identifier) = assets.firstOrNull { it.identifier == assetId } } \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/nft/DefaultNFTRuntimeStore.kt b/core/datasource/src/main/java/com/tangem/datasource/local/nft/DefaultNFTRuntimeStore.kt index cd70259814..0b615f36ae 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/local/nft/DefaultNFTRuntimeStore.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/local/nft/DefaultNFTRuntimeStore.kt @@ -71,6 +71,16 @@ internal class DefaultNFTRuntimeStore( } } + override suspend fun clear() { + collectionsRuntimeStore.store( + NFTCollections( + network = network, + content = NFTCollections.Content.Collections(null, StatusSource.ONLY_CACHE), + ), + ) + pricesRuntimeStore.store(emptyMap()) + } + private fun NFTCollections.getCollection(collectionId: NFTCollection.Identifier): NFTCollection? = (content as? NFTCollections.Content.Collections) ?.collections diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/nft/NFTPersistenceStore.kt b/core/datasource/src/main/java/com/tangem/datasource/local/nft/NFTPersistenceStore.kt index 3a6b27d9f3..feee48db2d 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/local/nft/NFTPersistenceStore.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/local/nft/NFTPersistenceStore.kt @@ -18,4 +18,6 @@ interface NFTPersistenceStore { suspend fun saveCollections(collections: List) suspend fun saveSalePrice(assetId: NFTAsset.Identifier, salePrice: NFTAsset.SalePrice) + + suspend fun clear() } \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/nft/NFTRuntimeStore.kt b/core/datasource/src/main/java/com/tangem/datasource/local/nft/NFTRuntimeStore.kt index 8c211d1f8e..92b2292e76 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/local/nft/NFTRuntimeStore.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/local/nft/NFTRuntimeStore.kt @@ -23,4 +23,6 @@ interface NFTRuntimeStore { suspend fun saveCollections(collections: NFTCollections) suspend fun saveSalePrice(salePrice: NFTSalePrice) + + suspend fun clear() } \ No newline at end of file diff --git a/data/nft/src/main/kotlin/com/tangem/data/nft/DefaultNFTRepository.kt b/data/nft/src/main/kotlin/com/tangem/data/nft/DefaultNFTRepository.kt index d5d3a63d50..d32cf88b34 100644 --- a/data/nft/src/main/kotlin/com/tangem/data/nft/DefaultNFTRepository.kt +++ b/data/nft/src/main/kotlin/com/tangem/data/nft/DefaultNFTRepository.kt @@ -215,6 +215,13 @@ internal class DefaultNFTRepository @Inject constructor( assetIdentifier = assetIdConverter.convertBack(assetIdentifier), ) + override suspend fun clearCache(userWalletId: UserWalletId, networks: List) { + networks.forEach { + getNFTPersistenceStore(userWalletId, it).clear() + getNFTRuntimeStore(userWalletId, it).clear() + } + } + private suspend fun refreshCollectionsInternal( userWalletId: UserWalletId, networks: List, diff --git a/domain/nft/build.gradle.kts b/domain/nft/build.gradle.kts index 6151f94e1b..6752c4736d 100644 --- a/domain/nft/build.gradle.kts +++ b/domain/nft/build.gradle.kts @@ -23,6 +23,7 @@ dependencies { implementation(projects.domain.quotes) implementation(projects.domain.tokens) implementation(projects.domain.tokens.models) + implementation(projects.domain.wallets) implementation(projects.domain.wallets.models) // endregion diff --git a/domain/nft/src/main/kotlin/com/tangem/domain/nft/DisableWalletNFTUseCase.kt b/domain/nft/src/main/kotlin/com/tangem/domain/nft/DisableWalletNFTUseCase.kt new file mode 100644 index 0000000000..75afd3e094 --- /dev/null +++ b/domain/nft/src/main/kotlin/com/tangem/domain/nft/DisableWalletNFTUseCase.kt @@ -0,0 +1,21 @@ +package com.tangem.domain.nft + +import com.tangem.domain.nft.repository.NFTRepository +import com.tangem.domain.tokens.repository.CurrenciesRepository +import com.tangem.domain.wallets.models.UserWalletId +import com.tangem.domain.wallets.repository.WalletsRepository + +class DisableWalletNFTUseCase( + private val walletsRepository: WalletsRepository, + private val nftRepository: NFTRepository, + private val currenciesRepository: CurrenciesRepository, +) { + + suspend operator fun invoke(userWalletId: UserWalletId) { + walletsRepository.disableNFT(userWalletId) + + val currencies = currenciesRepository.getMultiCurrencyWalletCachedCurrenciesSync(userWalletId) + val networks = currencies.map { it.network } + nftRepository.clearCache(userWalletId, networks) + } +} \ No newline at end of file diff --git a/domain/nft/src/main/kotlin/com/tangem/domain/nft/EnableWalletNFTUseCase.kt b/domain/nft/src/main/kotlin/com/tangem/domain/nft/EnableWalletNFTUseCase.kt new file mode 100644 index 0000000000..4df3fa1c23 --- /dev/null +++ b/domain/nft/src/main/kotlin/com/tangem/domain/nft/EnableWalletNFTUseCase.kt @@ -0,0 +1,13 @@ +package com.tangem.domain.nft + +import com.tangem.domain.wallets.models.UserWalletId +import com.tangem.domain.wallets.repository.WalletsRepository + +class EnableWalletNFTUseCase( + private val walletsRepository: WalletsRepository, +) { + + suspend operator fun invoke(userWalletId: UserWalletId) { + walletsRepository.enableNFT(userWalletId) + } +} \ No newline at end of file diff --git a/domain/nft/src/main/kotlin/com/tangem/domain/nft/GetWalletNFTEnabledUseCase.kt b/domain/nft/src/main/kotlin/com/tangem/domain/nft/GetWalletNFTEnabledUseCase.kt new file mode 100644 index 0000000000..db97202c30 --- /dev/null +++ b/domain/nft/src/main/kotlin/com/tangem/domain/nft/GetWalletNFTEnabledUseCase.kt @@ -0,0 +1,13 @@ +package com.tangem.domain.nft + +import com.tangem.domain.wallets.models.UserWalletId +import com.tangem.domain.wallets.repository.WalletsRepository +import kotlinx.coroutines.flow.Flow + +class GetWalletNFTEnabledUseCase( + private val walletsRepository: WalletsRepository, +) { + + operator fun invoke(userWalletId: UserWalletId): Flow = walletsRepository + .nftEnabledStatus(userWalletId) +} \ No newline at end of file diff --git a/domain/nft/src/main/kotlin/com/tangem/domain/nft/repository/NFTRepository.kt b/domain/nft/src/main/kotlin/com/tangem/domain/nft/repository/NFTRepository.kt index 5f8817884c..fd6548f6b8 100644 --- a/domain/nft/src/main/kotlin/com/tangem/domain/nft/repository/NFTRepository.kt +++ b/domain/nft/src/main/kotlin/com/tangem/domain/nft/repository/NFTRepository.kt @@ -32,4 +32,6 @@ interface NFTRepository { suspend fun getNFTSupportedNetworks(userWalletId: UserWalletId): List suspend fun getNFTExploreUrl(network: Network, assetIdentifier: NFTAsset.Identifier): String? + + suspend fun clearCache(userWalletId: UserWalletId, networks: List) } \ No newline at end of file diff --git a/features/wallet-settings/impl/build.gradle.kts b/features/wallet-settings/impl/build.gradle.kts index 25dc7dd5c6..9b694a9073 100644 --- a/features/wallet-settings/impl/build.gradle.kts +++ b/features/wallet-settings/impl/build.gradle.kts @@ -34,6 +34,7 @@ dependencies { implementation(projects.domain.wallets) implementation(projects.domain.wallets.models) implementation(projects.domain.demo) + implementation(projects.domain.nft) /* AndroidX */ implementation(deps.androidx.fragment.ktx) 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 b22130a159..7e895820be 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 @@ -19,8 +19,10 @@ import com.tangem.domain.common.util.cardTypesResolver import com.tangem.domain.demo.IsDemoCardUseCase import com.tangem.domain.models.scan.CardDTO import com.tangem.domain.models.scan.ScanResponse +import com.tangem.domain.nft.DisableWalletNFTUseCase +import com.tangem.domain.nft.EnableWalletNFTUseCase +import com.tangem.domain.nft.GetWalletNFTEnabledUseCase import com.tangem.domain.wallets.models.UserWallet -import com.tangem.domain.wallets.repository.WalletsRepository import com.tangem.domain.wallets.usecase.DeleteWalletUseCase import com.tangem.domain.wallets.usecase.GetUserWalletUseCase import com.tangem.domain.wallets.usecase.ShouldSaveUserWalletsSyncUseCase @@ -54,8 +56,10 @@ internal class WalletSettingsModel @Inject constructor( private val analyticsContextProxy: AnalyticsContextProxy, private val getShouldSaveUserWalletsSyncUseCase: ShouldSaveUserWalletsSyncUseCase, private val isDemoCardUseCase: IsDemoCardUseCase, - private val walletsRepository: WalletsRepository, private val nftFeatureToggles: NFTFeatureToggles, + private val getWalletNFTEnabledUseCase: GetWalletNFTEnabledUseCase, + private val enableWalletNFTUseCase: EnableWalletNFTUseCase, + private val disableWalletNFTUseCase: DisableWalletNFTUseCase, ) : Model() { val params: WalletSettingsComponent.Params = paramsContainer.require() @@ -71,7 +75,7 @@ internal class WalletSettingsModel @Inject constructor( init { getWalletUseCase.invokeFlow(params.userWalletId) .distinctUntilChanged() - .combine(walletsRepository.nftEnabledStatus(params.userWalletId)) { maybeWallet, nftEnabled -> + .combine(getWalletNFTEnabledUseCase.invoke(params.userWalletId)) { maybeWallet, nftEnabled -> val wallet = maybeWallet.getOrNull() ?: return@combine val isRenameWalletAvailable = getShouldSaveUserWalletsSyncUseCase() state.update { value -> @@ -169,9 +173,9 @@ internal class WalletSettingsModel @Inject constructor( private fun onCheckedNFTChange(isChecked: Boolean) { modelScope.launch { if (isChecked) { - walletsRepository.enableNFT(params.userWalletId) + enableWalletNFTUseCase.invoke(params.userWalletId) } else { - walletsRepository.disableNFT(params.userWalletId) + disableWalletNFTUseCase.invoke(params.userWalletId) } } } From 7f95a6b5181c780832aab2c969153850ba85f5ea Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 14 May 2025 19:38:07 +0400 Subject: [PATCH 046/165] Updated on 2026-08-14 --- .../com/tangem/tap/ApplicationEntryPoint.kt | 3 + .../java/com/tangem/tap/TangemApplication.kt | 14 ++-- .../tangem/tap/features/main/MainViewModel.kt | 73 ++++++++++++------- .../config/managers/ApiConfigsManager.kt | 6 +- .../config/managers/DevApiConfigsManager.kt | 12 ++- .../config/managers/ProdApiConfigsManager.kt | 6 ++ .../com/tangem/datasource/di/NetworkModule.kt | 12 ++- 7 files changed, 92 insertions(+), 34 deletions(-) diff --git a/app/src/main/java/com/tangem/tap/ApplicationEntryPoint.kt b/app/src/main/java/com/tangem/tap/ApplicationEntryPoint.kt index ee95e2ba40..52a6dc9646 100644 --- a/app/src/main/java/com/tangem/tap/ApplicationEntryPoint.kt +++ b/app/src/main/java/com/tangem/tap/ApplicationEntryPoint.kt @@ -15,6 +15,7 @@ import com.tangem.core.navigation.share.ShareManager import com.tangem.core.navigation.url.UrlOpener import com.tangem.core.ui.clipboard.ClipboardManager import com.tangem.data.card.TransactionSignerFactory +import com.tangem.datasource.api.common.config.managers.ApiConfigsManager import com.tangem.datasource.connection.NetworkConnectionManager import com.tangem.datasource.local.config.environment.EnvironmentConfigStorage import com.tangem.datasource.local.config.issuers.IssuersConfigStorage @@ -142,4 +143,6 @@ interface ApplicationEntryPoint { fun getOnlineCardVerifier(): OnlineCardVerifier fun getUserWalletBuilderFactory(): UserWalletBuilder.Factory + + fun getApiConfigsManager(): ApiConfigsManager } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/TangemApplication.kt b/app/src/main/java/com/tangem/tap/TangemApplication.kt index ed0018b6d9..03c39c9727 100644 --- a/app/src/main/java/com/tangem/tap/TangemApplication.kt +++ b/app/src/main/java/com/tangem/tap/TangemApplication.kt @@ -30,6 +30,7 @@ import com.tangem.core.navigation.settings.SettingsManager import com.tangem.core.ui.clipboard.ClipboardManager import com.tangem.data.card.TransactionSignerFactory import com.tangem.datasource.api.common.MoshiConverter +import com.tangem.datasource.api.common.config.managers.ApiConfigsManager import com.tangem.datasource.api.common.createNetworkLoggingInterceptor import com.tangem.datasource.connection.NetworkConnectionManager import com.tangem.datasource.local.config.environment.EnvironmentConfig @@ -228,6 +229,9 @@ abstract class TangemApplication : Application(), ImageLoaderFactory, Configurat private val userWalletBuilderFactory: UserWalletBuilder.Factory get() = entryPoint.getUserWalletBuilderFactory() + private val apiConfigsManager: ApiConfigsManager + get() = entryPoint.getApiConfigsManager() + // endregion private val appScope = MainScope() @@ -275,6 +279,8 @@ abstract class TangemApplication : Application(), ImageLoaderFactory, Configurat } fun init() { + apiConfigsManager.initialize() + store = createReduxStore() tangemAppLoggerInitializer.initialize() @@ -285,12 +291,8 @@ abstract class TangemApplication : Application(), ImageLoaderFactory, Configurat // We need to initialize the toggles and excludedBlockchainsManager before the MainActivity starts using them. runBlocking { awaitAll( - async { - featureTogglesManager.init() - }, - async { - excludedBlockchainsManager.init() - }, + async { featureTogglesManager.init() }, + async { excludedBlockchainsManager.init() }, ) initWithConfigDependency(environmentConfig = environmentConfigStorage.initialize()) } diff --git a/app/src/main/java/com/tangem/tap/features/main/MainViewModel.kt b/app/src/main/java/com/tangem/tap/features/main/MainViewModel.kt index 8911eebb8c..d2b77135d2 100644 --- a/app/src/main/java/com/tangem/tap/features/main/MainViewModel.kt +++ b/app/src/main/java/com/tangem/tap/features/main/MainViewModel.kt @@ -11,6 +11,7 @@ import com.tangem.core.analytics.models.event.TechAnalyticsEvent import com.tangem.core.decompose.di.GlobalUiMessageSender import com.tangem.core.decompose.ui.UiMessageSender import com.tangem.core.deeplink.DeepLinksRegistry +import com.tangem.core.ui.BuildConfig import com.tangem.core.ui.R import com.tangem.core.ui.coil.ImagePreloader import com.tangem.core.ui.extensions.resourceReference @@ -40,10 +41,13 @@ import com.tangem.tap.features.onboarding.products.wallet.redux.BackupDialog import com.tangem.tap.store import com.tangem.utils.coroutines.CoroutineDispatcherProvider import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.* import kotlinx.coroutines.launch +import kotlinx.coroutines.withTimeout import timber.log.Timber import javax.inject.Inject +import kotlin.time.Duration.Companion.seconds @Suppress("LongParameterList") @HiltViewModel @@ -57,7 +61,6 @@ internal class MainViewModel @Inject constructor( private val userWalletsListManager: UserWalletsListManager, private val dispatchers: CoroutineDispatcherProvider, private val fetchStakingTokensUseCase: FetchStakingTokensUseCase, - private val apiConfigsManager: ApiConfigsManager, private val fetchUserCountryUseCase: FetchUserCountryUseCase, @GlobalUiMessageSender private val messageSender: UiMessageSender, private val keyboardValidator: KeyboardValidator, @@ -68,6 +71,7 @@ internal class MainViewModel @Inject constructor( private val onboardingRepository: OnboardingRepository, private val deepLinksRegistry: DeepLinksRegistry, private val onrampDeepLinkFactory: OnrampDeepLink.Factory, + private val apiConfigsManager: ApiConfigsManager, routingFeatureToggle: RoutingFeatureToggle, getBalanceHidingSettingsUseCase: GetBalanceHidingSettingsUseCase, ) : ViewModel() { @@ -79,25 +83,27 @@ internal class MainViewModel @Inject constructor( private set init { + /** + * Run any data initialization here that needs to happen before the app starts + * and is hidden behind the SplashScreen + */ loadApplicationResources() - viewModelScope.launch(dispatchers.main) { incrementAppLaunchCounterUseCase() } + /** Run any API data load here that runs in parallel and does not block the app from starting */ + launchAPIRequests { + launch { fetchHotCryptoUseCase() } - viewModelScope.launch { - fetchUserCountryUseCase().onLeft { - Timber.e("Unable to fetch the user country code $it") - } + launch { fetchAppCurrenciesUseCase() } + + launch { fetchStakingTokens() } } - viewModelScope.launch { fetchHotCryptoUseCase() } + viewModelScope.launch { incrementAppLaunchCounterUseCase() } - updateAppCurrencies() observeFlips() displayBalancesHidingStatusToast() displayHiddenBalancesModalNotification() - fetchStakingTokens() - deleteDeprecatedLogsUseCase() sendKeyboardIdentifierEvent() @@ -118,16 +124,41 @@ internal class MainViewModel @Inject constructor( /** Loading the resources needed to run the application */ private fun loadApplicationResources() { - viewModelScope.launch(dispatchers.main) { - apiConfigsManager.initialize() + viewModelScope.launch { + launchAPIRequests { + launch { blockchainSDKFactory.init() } + + launch { + withTimeout(timeMillis = 1.seconds.inWholeMilliseconds) { fetchUserCountry() } + } + } - blockchainSDKFactory.init() prepareSelectedWalletFeedback() isSplashScreenShown = false } } + private suspend fun fetchUserCountry() { + fetchUserCountryUseCase().onLeft { + Timber.e("Unable to fetch the user country code $it") + } + } + + private fun launchAPIRequests(function: suspend CoroutineScope.() -> Unit) { + viewModelScope.launch { + if (BuildConfig.TESTER_MENU_ENABLED) { + apiConfigsManager.isInitialized + .filter { it } + .first() // wait until isInitialized becomes true + + function() + } else { + function() + } + } + } + private fun prepareSelectedWalletFeedback() { userWalletsListManager.selectedUserWallet .distinctUntilChanged() @@ -138,18 +169,10 @@ internal class MainViewModel @Inject constructor( .launchIn(viewModelScope) } - private fun updateAppCurrencies() { - viewModelScope.launch(dispatchers.main) { - fetchAppCurrenciesUseCase.invoke() - } - } - - private fun fetchStakingTokens() { - viewModelScope.launch(dispatchers.main) { - fetchStakingTokensUseCase() - .onLeft { Timber.e(it.toString(), "Unable to fetch the staking tokens list") } - .onRight { Timber.d("Staking token list was fetched successfully") } - } + private suspend fun fetchStakingTokens() { + fetchStakingTokensUseCase() + .onLeft { Timber.e(it.toString(), "Unable to fetch the staking tokens list") } + .onRight { Timber.d("Staking token list was fetched successfully") } } private fun observeFlips() { diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/common/config/managers/ApiConfigsManager.kt b/core/datasource/src/main/java/com/tangem/datasource/api/common/config/managers/ApiConfigsManager.kt index e7c3054219..a111d2b2a0 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/common/config/managers/ApiConfigsManager.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/common/config/managers/ApiConfigsManager.kt @@ -2,6 +2,7 @@ package com.tangem.datasource.api.common.config.managers import com.tangem.datasource.api.common.config.ApiConfig import com.tangem.datasource.api.common.config.ApiEnvironmentConfig +import kotlinx.coroutines.flow.StateFlow /** * Api configs manager @@ -10,8 +11,11 @@ import com.tangem.datasource.api.common.config.ApiEnvironmentConfig */ interface ApiConfigsManager { + /** Flag that determines whether the manager is initialized */ + val isInitialized: StateFlow + /** Initialize resources */ - fun initialize() {} + fun initialize() /** Get environment config by [id] */ fun getEnvironmentConfig(id: ApiConfig.ID): ApiEnvironmentConfig diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/common/config/managers/DevApiConfigsManager.kt b/core/datasource/src/main/java/com/tangem/datasource/api/common/config/managers/DevApiConfigsManager.kt index 7d60844b81..4327ec28df 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/common/config/managers/DevApiConfigsManager.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/common/config/managers/DevApiConfigsManager.kt @@ -29,7 +29,13 @@ internal class DevApiConfigsManager( private val _apiConfigs = MutableStateFlow(value = apiConfigs.associateWith { it.defaultEnvironment }) + override val isInitialized: StateFlow get() = _isInitialized.asStateFlow() + + private val _isInitialized = MutableStateFlow(value = false) + override fun initialize() { + _isInitialized.value = false + // We can't use appPreferencesStore.getObjectMap as base flow, // because we should keep possibility to work with configs synchronous. // See [getBaseUrl] @@ -42,8 +48,12 @@ internal class DevApiConfigsManager( savedEnvironments[config.id.name] ?: currentEnvironment } } + + if (!_isInitialized.value) { + _isInitialized.value = true + } } - .launchIn(CoroutineScope(SupervisorJob() + dispatchers.main)) + .launchIn(CoroutineScope(SupervisorJob() + dispatchers.default)) } override fun getEnvironmentConfig(id: ApiConfig.ID): ApiEnvironmentConfig { diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/common/config/managers/ProdApiConfigsManager.kt b/core/datasource/src/main/java/com/tangem/datasource/api/common/config/managers/ProdApiConfigsManager.kt index fd5638c151..8b77808141 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/common/config/managers/ProdApiConfigsManager.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/common/config/managers/ProdApiConfigsManager.kt @@ -3,6 +3,8 @@ package com.tangem.datasource.api.common.config.managers import com.tangem.datasource.api.common.config.ApiConfig import com.tangem.datasource.api.common.config.ApiConfigs import com.tangem.datasource.api.common.config.ApiEnvironmentConfig +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow /** * Implementation of [ApiConfigsManager] in PROD environment @@ -13,6 +15,10 @@ internal class ProdApiConfigsManager( private val apiConfigs: ApiConfigs, ) : ApiConfigsManager { + override val isInitialized: StateFlow = MutableStateFlow(value = true) + + override fun initialize() = Unit + override fun getEnvironmentConfig(id: ApiConfig.ID): ApiEnvironmentConfig { val config = apiConfigs.firstOrNull { it.id == id } ?: error("Api config with id [$id] not found. Check that ApiConfig with id [$id] was provided into DI") diff --git a/core/datasource/src/main/java/com/tangem/datasource/di/NetworkModule.kt b/core/datasource/src/main/java/com/tangem/datasource/di/NetworkModule.kt index a4b0eb363a..bfb13f75b1 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/di/NetworkModule.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/di/NetworkModule.kt @@ -43,6 +43,10 @@ internal object NetworkModule { private const val TANGEM_TECH_MARKETS_SERVICE_TIMEOUT_SECONDS = 60L private const val STAKE_KIT_API_TIMEOUT_SECONDS = 60L + private val excludedApiForLogging: Set = setOf( + // ApiConfig.ID.StakeKit, + ) + @Provides @Singleton fun provideApiConfigManager( @@ -317,7 +321,7 @@ internal object NetworkModule { } b } - .addLoggers(context) + .addLoggers(context = context, id = id) .clientBuilder() .build(), ) @@ -325,6 +329,12 @@ internal object NetworkModule { .create(T::class.java) } + private fun OkHttpClient.Builder.addLoggers(context: Context, id: ApiConfig.ID): OkHttpClient.Builder { + if (id in excludedApiForLogging) return this + + return addLoggers(context) + } + private data class Timeouts( val callTimeoutSeconds: Long? = null, val connectTimeoutSeconds: Long? = null, From 2fee75371efbd11322b862867ed4d10f359c1e65 Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 15 May 2025 17:09:17 +0400 Subject: [PATCH 047/165] Updated on 2026-08-14 --- .../converters/NetworkAddressConverter.kt | 48 +-- .../NetworkStatusDataModelConverter.kt | 14 +- .../SimpleNetworkStatusConverter.kt | 10 +- .../converters/NetworkAddressConverterTest.kt | 291 ++++++++++++++++++ .../converters/NetworkAmountsConverterTest.kt | 76 +++++ .../NetworkDerivationPathConverterTest.kt | 103 +++++++ .../NetworkStatusDataModelConverterTest.kt | 145 +++++++++ .../SimpleNetworkStatusConverterTest.kt | 291 ++++++++++++++++++ tangem-android-tools | 2 +- 9 files changed, 949 insertions(+), 31 deletions(-) create mode 100644 data/networks/src/test/java/com/tangem/data/networks/converters/NetworkAddressConverterTest.kt create mode 100644 data/networks/src/test/java/com/tangem/data/networks/converters/NetworkAmountsConverterTest.kt create mode 100644 data/networks/src/test/java/com/tangem/data/networks/converters/NetworkDerivationPathConverterTest.kt create mode 100644 data/networks/src/test/java/com/tangem/data/networks/converters/NetworkStatusDataModelConverterTest.kt create mode 100644 data/networks/src/test/java/com/tangem/data/networks/converters/SimpleNetworkStatusConverterTest.kt diff --git a/data/networks/src/main/java/com/tangem/data/networks/converters/NetworkAddressConverter.kt b/data/networks/src/main/java/com/tangem/data/networks/converters/NetworkAddressConverter.kt index f3e2e962e2..d0919d7d75 100644 --- a/data/networks/src/main/java/com/tangem/data/networks/converters/NetworkAddressConverter.kt +++ b/data/networks/src/main/java/com/tangem/data/networks/converters/NetworkAddressConverter.kt @@ -6,43 +6,49 @@ import com.tangem.utils.converter.TwoWayConverter import timber.log.Timber /** - * Converter from [Set] to [NetworkAddress] and vice versa + * Converter from [NetworkAddressConverter.Value] to [NetworkAddress] and vice versa * [REDACTED_AUTHOR] */ -internal class NetworkAddressConverter( - private val selectedAddress: String, -) : TwoWayConverter, NetworkAddress> { +internal object NetworkAddressConverter : TwoWayConverter { - override fun convert(value: Set): NetworkAddress { - val defaultAddress = value - .firstOrNull { it.value == selectedAddress } + data class Value( + val selectedAddress: String, + val addresses: Set, + ) + + override fun convert(value: Value): NetworkAddress { + val defaultAddress = value.addresses + .firstOrNull { it.value == value.selectedAddress } ?.let(::toNetworkAddress) requireNotNull(defaultAddress) { "Selected address must not be null" } - return if (value.size != 1) { + return if (value.addresses.size != 1) { NetworkAddress.Selectable( defaultAddress = defaultAddress, - availableAddresses = value.mapTo(destination = hashSetOf(), transform = ::toNetworkAddress), + availableAddresses = value.addresses.mapTo(destination = hashSetOf(), transform = ::toNetworkAddress), ) } else { NetworkAddress.Single(defaultAddress = defaultAddress) } } - override fun convertBack(value: NetworkAddress): Set { - return value.availableAddresses - .map { address -> - NetworkStatusDM.Address( - value = address.value, - type = when (address.type) { - NetworkAddress.Address.Type.Primary -> NetworkStatusDM.Address.Type.Primary - NetworkAddress.Address.Type.Secondary -> NetworkStatusDM.Address.Type.Secondary - }, - ) - } - .toSet() + override fun convertBack(value: NetworkAddress): Value { + return Value( + selectedAddress = value.defaultAddress.value, + addresses = value.availableAddresses + .map { address -> + NetworkStatusDM.Address( + value = address.value, + type = when (address.type) { + NetworkAddress.Address.Type.Primary -> NetworkStatusDM.Address.Type.Primary + NetworkAddress.Address.Type.Secondary -> NetworkStatusDM.Address.Type.Secondary + }, + ) + } + .toSet(), + ) } private fun toNetworkAddress(address: NetworkStatusDM.Address): NetworkAddress.Address { diff --git a/data/networks/src/main/java/com/tangem/data/networks/converters/NetworkStatusDataModelConverter.kt b/data/networks/src/main/java/com/tangem/data/networks/converters/NetworkStatusDataModelConverter.kt index f0c3cfb572..2e2d2e966c 100644 --- a/data/networks/src/main/java/com/tangem/data/networks/converters/NetworkStatusDataModelConverter.kt +++ b/data/networks/src/main/java/com/tangem/data/networks/converters/NetworkStatusDataModelConverter.kt @@ -14,22 +14,24 @@ internal object NetworkStatusDataModelConverter : Converter { + val address = NetworkAddressConverter.convertBack(value = status.address) + NetworkStatusDM.Verified( networkId = value.network.id, derivationPath = NetworkDerivationPathConverter.convertBack(value = value.network.derivationPath), - selectedAddress = status.address.defaultAddress.value, - availableAddresses = NetworkAddressConverter(selectedAddress = status.address.defaultAddress.value) - .convertBack(value = status.address), + selectedAddress = address.selectedAddress, + availableAddresses = address.addresses, amounts = NetworkAmountsConverter.convertBack(value = status.amounts), ) } is NetworkStatus.NoAccount -> { + val address = NetworkAddressConverter.convertBack(value = status.address) + NetworkStatusDM.NoAccount( networkId = value.network.id, derivationPath = NetworkDerivationPathConverter.convertBack(value = value.network.derivationPath), - selectedAddress = status.address.defaultAddress.value, - availableAddresses = NetworkAddressConverter(selectedAddress = status.address.defaultAddress.value) - .convertBack(value = status.address), + selectedAddress = address.selectedAddress, + availableAddresses = address.addresses, amountToCreateAccount = status.amountToCreateAccount, errorMessage = status.errorMessage, ) diff --git a/data/networks/src/main/java/com/tangem/data/networks/converters/SimpleNetworkStatusConverter.kt b/data/networks/src/main/java/com/tangem/data/networks/converters/SimpleNetworkStatusConverter.kt index 314a02853a..c9e794c53d 100644 --- a/data/networks/src/main/java/com/tangem/data/networks/converters/SimpleNetworkStatusConverter.kt +++ b/data/networks/src/main/java/com/tangem/data/networks/converters/SimpleNetworkStatusConverter.kt @@ -14,15 +14,19 @@ import com.tangem.utils.converter.Converter internal object SimpleNetworkStatusConverter : Converter { override fun convert(value: NetworkStatusDM): SimpleNetworkStatus { - val address = NetworkAddressConverter(selectedAddress = value.selectedAddress) - .convert(value = value.availableAddresses) + val address = NetworkAddressConverter.convert( + value = NetworkAddressConverter.Value( + selectedAddress = value.selectedAddress, + addresses = value.availableAddresses, + ), + ) val status = when (value) { is NetworkStatusDM.Verified -> { NetworkStatus.Verified( address = address, amounts = NetworkAmountsConverter.convert(value = value.amounts), - pendingTransactions = mapOf(), + pendingTransactions = emptyMap(), source = StatusSource.CACHE, ) } diff --git a/data/networks/src/test/java/com/tangem/data/networks/converters/NetworkAddressConverterTest.kt b/data/networks/src/test/java/com/tangem/data/networks/converters/NetworkAddressConverterTest.kt new file mode 100644 index 0000000000..19b6a2730a --- /dev/null +++ b/data/networks/src/test/java/com/tangem/data/networks/converters/NetworkAddressConverterTest.kt @@ -0,0 +1,291 @@ +package com.tangem.data.networks.converters + +import com.google.common.truth.Truth +import com.tangem.datasource.local.network.entity.NetworkStatusDM +import com.tangem.domain.tokens.model.NetworkAddress +import org.junit.jupiter.api.Nested +import org.junit.jupiter.api.TestInstance +import org.junit.jupiter.params.ParameterizedTest +import org.junit.jupiter.params.provider.MethodSource + +/** +[REDACTED_AUTHOR] + */ +internal class NetworkAddressConverterTest { + + @Nested + @TestInstance(TestInstance.Lifecycle.PER_CLASS) + inner class Convert { + + @ParameterizedTest + @MethodSource("provideTestModels") + fun convert(model: ConvertModel) { + // Act + val actual = runCatching { NetworkAddressConverter.convert(value = model.value) } + + // Assert + actual + .onSuccess { + Truth.assertThat(it).isEqualTo(model.expected.getOrNull()) + } + .onFailure { + val expected = model.expected.exceptionOrNull()!! + + Truth.assertThat(it).isInstanceOf(expected::class.java) + Truth.assertThat(it).hasMessageThat().isEqualTo(expected.message) + } + } + + private fun provideTestModels(): Collection = listOf( + ConvertModel( + value = NetworkAddressConverter.Value( + selectedAddress = "0x1", + addresses = setOf( + NetworkStatusDM.Address( + value = "0x1", + type = NetworkStatusDM.Address.Type.Primary, + ), + ), + ), + expected = NetworkAddress.Single( + defaultAddress = NetworkAddress.Address( + value = "0x1", + type = NetworkAddress.Address.Type.Primary, + ), + ).let(Result.Companion::success), + ), + ConvertModel( + value = NetworkAddressConverter.Value( + selectedAddress = "0x1", + addresses = setOf( + NetworkStatusDM.Address( + value = "0x1", + type = NetworkStatusDM.Address.Type.Primary, + ), + NetworkStatusDM.Address( + value = "0x2", + type = NetworkStatusDM.Address.Type.Secondary, + ), + ), + ), + expected = NetworkAddress.Selectable( + defaultAddress = NetworkAddress.Address( + value = "0x1", + type = NetworkAddress.Address.Type.Primary, + ), + availableAddresses = setOf( + NetworkAddress.Address( + value = "0x1", + type = NetworkAddress.Address.Type.Primary, + ), + NetworkAddress.Address( + value = "0x2", + type = NetworkAddress.Address.Type.Secondary, + ), + ), + ).let(Result.Companion::success), + ), + ConvertModel( + value = NetworkAddressConverter.Value( + selectedAddress = "", + addresses = setOf( + NetworkStatusDM.Address( + value = "0x1", + type = NetworkStatusDM.Address.Type.Primary, + ), + NetworkStatusDM.Address( + value = "0x2", + type = NetworkStatusDM.Address.Type.Secondary, + ), + ), + ), + expected = IllegalArgumentException("Selected address must not be null").let(Result.Companion::failure), + ), + ConvertModel( + value = NetworkAddressConverter.Value( + selectedAddress = "0x1", + addresses = setOf( + NetworkStatusDM.Address( + value = "", + type = NetworkStatusDM.Address.Type.Primary, + ), + NetworkStatusDM.Address( + value = "0x2", + type = NetworkStatusDM.Address.Type.Secondary, + ), + ), + ), + expected = IllegalArgumentException("Selected address must not be null").let(Result.Companion::failure), + ), + ConvertModel( + value = NetworkAddressConverter.Value( + selectedAddress = "", + addresses = setOf( + NetworkStatusDM.Address( + value = "", + type = NetworkStatusDM.Address.Type.Primary, + ), + ), + ), + expected = NetworkAddress.Single( + defaultAddress = NetworkAddress.Address( + value = "", + type = NetworkAddress.Address.Type.Primary, + ), + ).let(Result.Companion::success), + ), + ConvertModel( + value = NetworkAddressConverter.Value( + selectedAddress = "0x1", + addresses = setOf( + NetworkStatusDM.Address( + value = "0x1", + type = NetworkStatusDM.Address.Type.Secondary, + ), + ), + ), + expected = NetworkAddress.Single( + defaultAddress = NetworkAddress.Address( + value = "0x1", + type = NetworkAddress.Address.Type.Secondary, + ), + ).let(Result.Companion::success), + ), + ConvertModel( + value = NetworkAddressConverter.Value( + selectedAddress = "0x1", + addresses = setOf( + NetworkStatusDM.Address( + value = "0x1", + type = NetworkStatusDM.Address.Type.Secondary, + ), + NetworkStatusDM.Address( + value = "0x2", + type = NetworkStatusDM.Address.Type.Secondary, + ), + ), + ), + expected = NetworkAddress.Selectable( + defaultAddress = NetworkAddress.Address( + value = "0x1", + type = NetworkAddress.Address.Type.Secondary, + ), + availableAddresses = setOf( + NetworkAddress.Address( + value = "0x1", + type = NetworkAddress.Address.Type.Secondary, + ), + NetworkAddress.Address( + value = "0x2", + type = NetworkAddress.Address.Type.Secondary, + ), + ), + ).let(Result.Companion::success), + ), + ) + } + + @Nested + @TestInstance(TestInstance.Lifecycle.PER_CLASS) + inner class ConvertBack { + + @ParameterizedTest + @MethodSource("provideTestModels") + fun convertBack(model: ConvertBackModel) { + // Act + val actual = NetworkAddressConverter.convertBack(value = model.value) + + // Assert + Truth.assertThat(actual).isEqualTo(model.expected) + } + + private fun provideTestModels(): Collection = listOf( + ConvertBackModel( + value = NetworkAddress.Single( + defaultAddress = NetworkAddress.Address( + value = "0x1", + type = NetworkAddress.Address.Type.Primary, + ), + ), + expected = NetworkAddressConverter.Value( + selectedAddress = "0x1", + addresses = setOf( + NetworkStatusDM.Address( + value = "0x1", + type = NetworkStatusDM.Address.Type.Primary, + ), + ), + ), + ), + ConvertBackModel( + value = NetworkAddress.Selectable( + defaultAddress = NetworkAddress.Address( + value = "0x1", + type = NetworkAddress.Address.Type.Primary, + ), + availableAddresses = setOf( + NetworkAddress.Address( + value = "0x1", + type = NetworkAddress.Address.Type.Primary, + ), + NetworkAddress.Address( + value = "0x2", + type = NetworkAddress.Address.Type.Secondary, + ), + ), + ), + expected = NetworkAddressConverter.Value( + selectedAddress = "0x1", + addresses = setOf( + NetworkStatusDM.Address( + value = "0x1", + type = NetworkStatusDM.Address.Type.Primary, + ), + NetworkStatusDM.Address( + value = "0x2", + type = NetworkStatusDM.Address.Type.Secondary, + ), + ), + ), + ), + ConvertBackModel( + value = NetworkAddress.Single( + defaultAddress = NetworkAddress.Address( + value = "", + type = NetworkAddress.Address.Type.Primary, + ), + ), + expected = NetworkAddressConverter.Value( + selectedAddress = "", + addresses = setOf( + NetworkStatusDM.Address( + value = "", + type = NetworkStatusDM.Address.Type.Primary, + ), + ), + ), + ), + ConvertBackModel( + value = NetworkAddress.Single( + defaultAddress = NetworkAddress.Address( + value = "", + type = NetworkAddress.Address.Type.Secondary, + ), + ), + expected = NetworkAddressConverter.Value( + selectedAddress = "", + addresses = setOf( + NetworkStatusDM.Address( + value = "", + type = NetworkStatusDM.Address.Type.Secondary, + ), + ), + ), + ), + ) + } + + data class ConvertModel(val value: NetworkAddressConverter.Value, val expected: Result) + + data class ConvertBackModel(val value: NetworkAddress, val expected: NetworkAddressConverter.Value) +} \ No newline at end of file diff --git a/data/networks/src/test/java/com/tangem/data/networks/converters/NetworkAmountsConverterTest.kt b/data/networks/src/test/java/com/tangem/data/networks/converters/NetworkAmountsConverterTest.kt new file mode 100644 index 0000000000..bb02d4f48d --- /dev/null +++ b/data/networks/src/test/java/com/tangem/data/networks/converters/NetworkAmountsConverterTest.kt @@ -0,0 +1,76 @@ +package com.tangem.data.networks.converters + +import com.google.common.truth.Truth +import com.tangem.domain.tokens.model.CryptoCurrency.ID +import com.tangem.domain.tokens.model.CryptoCurrency.ID.Body +import com.tangem.domain.tokens.model.CryptoCurrency.ID.Prefix +import com.tangem.domain.tokens.model.CryptoCurrencyAmountStatus +import org.junit.jupiter.api.Test +import java.math.BigDecimal + +/** +[REDACTED_AUTHOR] + */ +internal class NetworkAmountsConverterTest { + + @Test + fun convert() { + // Arrange + val value = mapOf( + "coin⟨BCH⟩bitcoin-cash" to BigDecimal.ZERO, + "coin⟨ETH→12367123⟩ethereum" to BigDecimal.ONE, + ) + + // Act + val actual = NetworkAmountsConverter.convert(value) + + // Assert + val expected = mapOf( + ID( + prefix = Prefix.COIN_PREFIX, + body = Body.NetworkId(rawId = "BCH"), + suffix = ID.Suffix.RawID(rawId = "bitcoin-cash"), + ) to CryptoCurrencyAmountStatus.Loaded(value = BigDecimal.ZERO), + ID( + prefix = Prefix.COIN_PREFIX, + body = Body.NetworkIdWithDerivationPath(rawId = "ETH", derivationPathHashCode = 12367123), + suffix = ID.Suffix.RawID(rawId = "ethereum"), + ) to CryptoCurrencyAmountStatus.Loaded(value = BigDecimal.ONE), + ) + + Truth.assertThat(actual).isEqualTo(expected) + } + + @Test + fun convertBack() { + // Arrange + val value = mapOf( + ID( + prefix = Prefix.COIN_PREFIX, + body = Body.NetworkId(rawId = "BCH"), + suffix = ID.Suffix.RawID(rawId = "bitcoin-cash"), + ) to CryptoCurrencyAmountStatus.Loaded(value = BigDecimal.ZERO), + ID( + prefix = Prefix.COIN_PREFIX, + body = Body.NetworkIdWithDerivationPath(rawId = "ETH", derivationPathHashCode = 12367123), + suffix = ID.Suffix.RawID(rawId = "ethereum"), + ) to CryptoCurrencyAmountStatus.Loaded(value = BigDecimal.ONE), + ID( + prefix = Prefix.COIN_PREFIX, + body = Body.NetworkId(rawId = "BTC"), + suffix = ID.Suffix.RawID(rawId = "bitcoin"), + ) to CryptoCurrencyAmountStatus.NotFound, + ) + + // Act + val actual = NetworkAmountsConverter.convertBack(value) + + // Assert + val expected = mapOf( + "coin⟨BCH⟩bitcoin-cash" to BigDecimal.ZERO, + "coin⟨ETH→12367123⟩ethereum" to BigDecimal.ONE, + ) + + Truth.assertThat(actual).isEqualTo(expected) + } +} \ No newline at end of file diff --git a/data/networks/src/test/java/com/tangem/data/networks/converters/NetworkDerivationPathConverterTest.kt b/data/networks/src/test/java/com/tangem/data/networks/converters/NetworkDerivationPathConverterTest.kt new file mode 100644 index 0000000000..e069087f9a --- /dev/null +++ b/data/networks/src/test/java/com/tangem/data/networks/converters/NetworkDerivationPathConverterTest.kt @@ -0,0 +1,103 @@ +package com.tangem.data.networks.converters + +import com.google.common.truth.Truth +import com.tangem.datasource.local.network.entity.NetworkStatusDM +import com.tangem.domain.tokens.model.Network +import org.junit.jupiter.api.Nested +import org.junit.jupiter.api.TestInstance +import org.junit.jupiter.params.ParameterizedTest +import org.junit.jupiter.params.provider.MethodSource + +/** +[REDACTED_AUTHOR] + */ +class NetworkDerivationPathConverterTest { + + @TestInstance(TestInstance.Lifecycle.PER_CLASS) + @Nested + inner class Convert { + + @ParameterizedTest + @MethodSource("provideTestModels") + fun convert(model: ConvertModel) { + // Act + val actual = NetworkDerivationPathConverter.convert(value = model.value) + + // Assert + Truth.assertThat(actual).isEqualTo(model.expected) + } + + private fun provideTestModels(): Collection = listOf( + ConvertModel( + value = NetworkStatusDM.DerivationPath( + value = "card", + type = NetworkStatusDM.DerivationPath.Type.CARD, + ), + expected = Network.DerivationPath.Card("card"), + ), + ConvertModel( + value = NetworkStatusDM.DerivationPath( + value = "custom", + type = NetworkStatusDM.DerivationPath.Type.CUSTOM, + ), + expected = Network.DerivationPath.Custom("custom"), + ), + ConvertModel( + value = NetworkStatusDM.DerivationPath( + value = "", + type = NetworkStatusDM.DerivationPath.Type.NONE, + ), + expected = Network.DerivationPath.None, + ), + ) + } + + @TestInstance(TestInstance.Lifecycle.PER_CLASS) + @Nested + inner class ConvertBack { + + @ParameterizedTest + @MethodSource("provideTestModels") + fun convertBack(model: ConvertBackModel) { + // Act + val actual = NetworkDerivationPathConverter.convertBack(value = model.value) + + // Assert + Truth.assertThat(actual).isEqualTo(model.expected) + } + + private fun provideTestModels(): Collection = listOf( + ConvertBackModel( + value = Network.DerivationPath.Card("card"), + expected = NetworkStatusDM.DerivationPath( + value = "card", + type = NetworkStatusDM.DerivationPath.Type.CARD, + ), + ), + ConvertBackModel( + value = Network.DerivationPath.Custom("custom"), + expected = NetworkStatusDM.DerivationPath( + value = "custom", + type = NetworkStatusDM.DerivationPath.Type.CUSTOM, + ), + ), + ConvertBackModel( + value = Network.DerivationPath.None, + expected = NetworkStatusDM.DerivationPath( + value = "", + type = NetworkStatusDM.DerivationPath.Type.NONE, + ), + ), + ) + } + + data class ConvertModel( + val value: NetworkStatusDM.DerivationPath, + val expected: Network.DerivationPath, + ) + + data class ConvertBackModel( + val value: Network.DerivationPath, + val expected: NetworkStatusDM.DerivationPath, + ) +} \ No newline at end of file diff --git a/data/networks/src/test/java/com/tangem/data/networks/converters/NetworkStatusDataModelConverterTest.kt b/data/networks/src/test/java/com/tangem/data/networks/converters/NetworkStatusDataModelConverterTest.kt new file mode 100644 index 0000000000..8f00a9b734 --- /dev/null +++ b/data/networks/src/test/java/com/tangem/data/networks/converters/NetworkStatusDataModelConverterTest.kt @@ -0,0 +1,145 @@ +package com.tangem.data.networks.converters + +import com.google.common.truth.Truth +import com.tangem.common.test.domain.token.MockCryptoCurrencyFactory +import com.tangem.datasource.local.network.entity.NetworkStatusDM +import com.tangem.domain.models.StatusSource +import com.tangem.domain.tokens.model.CryptoCurrency.ID +import com.tangem.domain.tokens.model.CryptoCurrency.ID.Body +import com.tangem.domain.tokens.model.CryptoCurrency.ID.Prefix +import com.tangem.domain.tokens.model.CryptoCurrencyAmountStatus +import com.tangem.domain.tokens.model.Network +import com.tangem.domain.tokens.model.NetworkAddress +import com.tangem.domain.tokens.model.NetworkStatus +import org.junit.jupiter.api.TestInstance +import org.junit.jupiter.params.ParameterizedTest +import org.junit.jupiter.params.provider.MethodSource +import java.math.BigDecimal + +/** +[REDACTED_AUTHOR] + */ +@TestInstance(TestInstance.Lifecycle.PER_CLASS) +internal class NetworkStatusDataModelConverterTest { + + private val network: Network = MockCryptoCurrencyFactory().ethereum.network + + @ParameterizedTest + @MethodSource("provideTestModels") + fun convert(model: ConvertModel) { + // Act + val actual = NetworkStatusDataModelConverter.convert(value = model.value) + + // Assert + Truth.assertThat(actual).isEqualTo(model.expected) + } + + private fun provideTestModels() = listOf( + // region Verified + ConvertModel( + value = NetworkStatus( + network = network, + value = NetworkStatus.Verified( + address = NetworkAddress.Single( + defaultAddress = NetworkAddress.Address( + value = "0x123", + type = NetworkAddress.Address.Type.Primary, + ), + ), + amounts = mapOf( + ID( + prefix = Prefix.COIN_PREFIX, + body = Body.NetworkId(rawId = "BCH"), + suffix = ID.Suffix.RawID(rawId = "bitcoin-cash"), + ) to CryptoCurrencyAmountStatus.Loaded(value = BigDecimal.ZERO), + ID( + prefix = Prefix.COIN_PREFIX, + body = Body.NetworkId(rawId = "BTC"), + suffix = ID.Suffix.RawID(rawId = "bitcoin"), + ) to CryptoCurrencyAmountStatus.NotFound, + ), + pendingTransactions = mapOf(), // doesn't matter + source = StatusSource.ACTUAL, // doesn't matter + ), + ), + expected = NetworkStatusDM.Verified( + networkId = network.id, + derivationPath = NetworkStatusDM.DerivationPath( + value = "", + type = NetworkStatusDM.DerivationPath.Type.NONE, + ), + selectedAddress = "0x123", + availableAddresses = setOf( + NetworkStatusDM.Address( + value = "0x123", + type = NetworkStatusDM.Address.Type.Primary, + ), + ), + amounts = mapOf("coin⟨BCH⟩bitcoin-cash" to BigDecimal.ZERO), + ), + ), + // endregion + + // region NoAccount + ConvertModel( + value = NetworkStatus( + network = network, + value = NetworkStatus.NoAccount( + address = NetworkAddress.Single( + defaultAddress = NetworkAddress.Address( + value = "0x123", + type = NetworkAddress.Address.Type.Primary, + ), + ), + amountToCreateAccount = BigDecimal.ONE, + errorMessage = "errorMessage", + source = StatusSource.ACTUAL, // doesn't matter + ), + ), + expected = NetworkStatusDM.NoAccount( + networkId = network.id, + derivationPath = NetworkStatusDM.DerivationPath( + value = "", + type = NetworkStatusDM.DerivationPath.Type.NONE, + ), + selectedAddress = "0x123", + availableAddresses = setOf( + NetworkStatusDM.Address( + value = "0x123", + type = NetworkStatusDM.Address.Type.Primary, + ), + ), + amountToCreateAccount = BigDecimal.ONE, + errorMessage = "errorMessage", + ), + ), + // endregion + + // region Other NetworkStatus + ConvertModel( + value = NetworkStatus(network = network, value = NetworkStatus.Unreachable(address = null)), + expected = null, + ), + ConvertModel( + value = NetworkStatus( + network = network, + value = NetworkStatus.Unreachable( + address = NetworkAddress.Single( + defaultAddress = NetworkAddress.Address( + value = "0x123", + type = NetworkAddress.Address.Type.Primary, + ), + ), + ), + ), + expected = null, + ), + ConvertModel( + value = NetworkStatus(network = network, value = NetworkStatus.MissedDerivation), + expected = null, + ), + // endregion + ) + + data class ConvertModel(val value: NetworkStatus, val expected: NetworkStatusDM?) +} \ No newline at end of file diff --git a/data/networks/src/test/java/com/tangem/data/networks/converters/SimpleNetworkStatusConverterTest.kt b/data/networks/src/test/java/com/tangem/data/networks/converters/SimpleNetworkStatusConverterTest.kt new file mode 100644 index 0000000000..98838ae290 --- /dev/null +++ b/data/networks/src/test/java/com/tangem/data/networks/converters/SimpleNetworkStatusConverterTest.kt @@ -0,0 +1,291 @@ +package com.tangem.data.networks.converters + +import com.google.common.truth.Truth +import com.tangem.common.test.domain.token.MockCryptoCurrencyFactory +import com.tangem.data.networks.models.SimpleNetworkStatus +import com.tangem.datasource.local.network.entity.NetworkStatusDM +import com.tangem.domain.models.StatusSource +import com.tangem.domain.tokens.model.CryptoCurrency.ID +import com.tangem.domain.tokens.model.CryptoCurrency.ID.Body +import com.tangem.domain.tokens.model.CryptoCurrency.ID.Prefix +import com.tangem.domain.tokens.model.CryptoCurrencyAmountStatus +import com.tangem.domain.tokens.model.Network +import com.tangem.domain.tokens.model.NetworkAddress +import com.tangem.domain.tokens.model.NetworkStatus +import org.junit.jupiter.api.TestInstance +import org.junit.jupiter.params.ParameterizedTest +import org.junit.jupiter.params.provider.MethodSource +import java.math.BigDecimal + +/** +[REDACTED_AUTHOR] + */ +@TestInstance(TestInstance.Lifecycle.PER_CLASS) +internal class SimpleNetworkStatusConverterTest { + + private val network: Network = MockCryptoCurrencyFactory().ethereum.network + + @ParameterizedTest + @MethodSource("provideTestModels") + fun convert(model: ConvertModel) { + // Act + val actual = runCatching { SimpleNetworkStatusConverter.convert(value = model.value) } + + // Assert + actual + .onSuccess { + Truth.assertThat(it).isEqualTo(model.expected.getOrNull()) + } + .onFailure { + val expected = model.expected.exceptionOrNull()!! + + Truth.assertThat(it).isInstanceOf(expected::class.java) + Truth.assertThat(it).hasMessageThat().isEqualTo(expected.message) + } + } + + private fun provideTestModels() = listOf( + // region Verified + ConvertModel( + value = NetworkStatusDM.Verified( + networkId = network.id, + derivationPath = NetworkStatusDM.DerivationPath( + value = "card", + type = NetworkStatusDM.DerivationPath.Type.CARD, + ), + selectedAddress = "0x1", + availableAddresses = setOf( + NetworkStatusDM.Address( + value = "0x1", + type = NetworkStatusDM.Address.Type.Primary, + ), + NetworkStatusDM.Address( + value = "0x2", + type = NetworkStatusDM.Address.Type.Secondary, + ), + ), + amounts = mapOf( + "coin⟨BCH⟩bitcoin-cash" to BigDecimal.ZERO, + "coin⟨ETH→12367123⟩ethereum" to BigDecimal.ONE, + ), + ), + expected = SimpleNetworkStatus( + id = SimpleNetworkStatus.Id( + networkId = network.id, + derivationPath = Network.DerivationPath.Card(value = "card"), + ), + value = NetworkStatus.Verified( + address = NetworkAddress.Selectable( + defaultAddress = NetworkAddress.Address( + value = "0x1", + type = NetworkAddress.Address.Type.Primary, + ), + availableAddresses = setOf( + NetworkAddress.Address( + value = "0x2", + type = NetworkAddress.Address.Type.Secondary, + ), + NetworkAddress.Address( + value = "0x1", + type = NetworkAddress.Address.Type.Primary, + ), + ), + ), + amounts = mapOf( + ID( + prefix = Prefix.COIN_PREFIX, + body = Body.NetworkId(rawId = "BCH"), + suffix = ID.Suffix.RawID(rawId = "bitcoin-cash"), + ) to CryptoCurrencyAmountStatus.Loaded(value = BigDecimal.ZERO), + ID( + prefix = Prefix.COIN_PREFIX, + body = Body.NetworkIdWithDerivationPath(rawId = "ETH", derivationPathHashCode = 12367123), + suffix = ID.Suffix.RawID(rawId = "ethereum"), + ) to CryptoCurrencyAmountStatus.Loaded(value = BigDecimal.ONE), + ), + pendingTransactions = emptyMap(), + source = StatusSource.CACHE, + ), + ).let(Result.Companion::success), + ), + // endregion + + // region NoAccount + ConvertModel( + value = NetworkStatusDM.NoAccount( + networkId = network.id, + derivationPath = NetworkStatusDM.DerivationPath( + value = "card", + type = NetworkStatusDM.DerivationPath.Type.CARD, + ), + selectedAddress = "0x1", + availableAddresses = setOf( + NetworkStatusDM.Address( + value = "0x1", + type = NetworkStatusDM.Address.Type.Primary, + ), + NetworkStatusDM.Address( + value = "0x2", + type = NetworkStatusDM.Address.Type.Secondary, + ), + ), + amountToCreateAccount = BigDecimal.ONE, + errorMessage = "errorMessage", + ), + expected = SimpleNetworkStatus( + id = SimpleNetworkStatus.Id( + networkId = network.id, + derivationPath = Network.DerivationPath.Card(value = "card"), + ), + value = NetworkStatus.NoAccount( + address = NetworkAddress.Selectable( + defaultAddress = NetworkAddress.Address( + value = "0x1", + type = NetworkAddress.Address.Type.Primary, + ), + availableAddresses = setOf( + NetworkAddress.Address( + value = "0x2", + type = NetworkAddress.Address.Type.Secondary, + ), + NetworkAddress.Address( + value = "0x1", + type = NetworkAddress.Address.Type.Primary, + ), + ), + ), + amountToCreateAccount = BigDecimal.ONE, + errorMessage = "errorMessage", + source = StatusSource.CACHE, + ), + ).let(Result.Companion::success), + ), + // endregion + + // region Error + ConvertModel( + value = NetworkStatusDM.Verified( + networkId = network.id, + derivationPath = NetworkStatusDM.DerivationPath( + value = "card", + type = NetworkStatusDM.DerivationPath.Type.CARD, + ), + selectedAddress = "0x1", + availableAddresses = setOf( + NetworkStatusDM.Address( + value = "0x2", + type = NetworkStatusDM.Address.Type.Primary, + ), + ), + amounts = emptyMap(), + ), + expected = Result.failure( + exception = IllegalArgumentException("Selected address must not be null"), + ), + ), + ConvertModel( + value = NetworkStatusDM.Verified( + networkId = network.id, + derivationPath = NetworkStatusDM.DerivationPath( + value = "card", + type = NetworkStatusDM.DerivationPath.Type.CARD, + ), + selectedAddress = "0x1", + availableAddresses = setOf(), + amounts = emptyMap(), + ), + expected = Result.failure( + exception = IllegalArgumentException("Selected address must not be null"), + ), + ), + ConvertModel( + value = NetworkStatusDM.Verified( + networkId = network.id, + derivationPath = NetworkStatusDM.DerivationPath( + value = "card", + type = NetworkStatusDM.DerivationPath.Type.CARD, + ), + selectedAddress = "", + availableAddresses = setOf( + NetworkStatusDM.Address( + value = "0x1", + type = NetworkStatusDM.Address.Type.Primary, + ), + NetworkStatusDM.Address( + value = "0x2", + type = NetworkStatusDM.Address.Type.Secondary, + ), + ), + amounts = emptyMap(), + ), + expected = Result.failure( + exception = IllegalArgumentException("Selected address must not be null"), + ), + ), + ConvertModel( + value = NetworkStatusDM.NoAccount( + networkId = network.id, + derivationPath = NetworkStatusDM.DerivationPath( + value = "card", + type = NetworkStatusDM.DerivationPath.Type.CARD, + ), + selectedAddress = "", + availableAddresses = setOf( + NetworkStatusDM.Address( + value = "0x1", + type = NetworkStatusDM.Address.Type.Primary, + ), + NetworkStatusDM.Address( + value = "0x2", + type = NetworkStatusDM.Address.Type.Secondary, + ), + ), + amountToCreateAccount = BigDecimal.ONE, + errorMessage = "errorMessage", + ), + expected = Result.failure( + exception = IllegalArgumentException("Selected address must not be null"), + ), + ), + ConvertModel( + value = NetworkStatusDM.NoAccount( + networkId = network.id, + derivationPath = NetworkStatusDM.DerivationPath( + value = "card", + type = NetworkStatusDM.DerivationPath.Type.CARD, + ), + selectedAddress = "0x1", + availableAddresses = setOf( + NetworkStatusDM.Address( + value = "0x2", + type = NetworkStatusDM.Address.Type.Primary, + ), + ), + amountToCreateAccount = BigDecimal.ONE, + errorMessage = "errorMessage", + ), + expected = Result.failure( + exception = IllegalArgumentException("Selected address must not be null"), + ), + ), + ConvertModel( + value = NetworkStatusDM.NoAccount( + networkId = network.id, + derivationPath = NetworkStatusDM.DerivationPath( + value = "card", + type = NetworkStatusDM.DerivationPath.Type.CARD, + ), + selectedAddress = "0x1", + availableAddresses = setOf(), + amountToCreateAccount = BigDecimal.ONE, + errorMessage = "errorMessage", + ), + expected = Result.failure( + exception = IllegalArgumentException("Selected address must not be null"), + ), + ), + // endregion + ) + + data class ConvertModel(val value: NetworkStatusDM, val expected: Result) +} \ No newline at end of file diff --git a/tangem-android-tools b/tangem-android-tools index 0ed07b85e6..764c490ca8 160000 --- a/tangem-android-tools +++ b/tangem-android-tools @@ -1 +1 @@ -Subproject commit 0ed07b85e64805707b2ef6a92f42bc2c4e8b7753 +Subproject commit 764c490ca845d9b8a4be76ff834b827b45ce9ed2 From 6a7e48e5812d2f8037036fcff01d2ff7eaed2599 Mon Sep 17 00:00:00 2001 From: Tangem Date: Fri, 16 May 2025 20:12:56 +0500 Subject: [PATCH 048/165] Updated on 2026-08-14 --- .../data/transaction/DefaultTransactionRepository.kt | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/data/transaction/src/main/java/com/tangem/data/transaction/DefaultTransactionRepository.kt b/data/transaction/src/main/java/com/tangem/data/transaction/DefaultTransactionRepository.kt index 540ffdb8ed..336edbf7cc 100644 --- a/data/transaction/src/main/java/com/tangem/data/transaction/DefaultTransactionRepository.kt +++ b/data/transaction/src/main/java/com/tangem/data/transaction/DefaultTransactionRepository.kt @@ -172,10 +172,15 @@ internal class DefaultTransactionRepository( ): TransactionData.Uncompiled = withContext(coroutineDispatcherProvider.io) { val blockchain = Blockchain.fromId(network.id.value) + // For now transfer one nft asset at a time + val updatedNFTAsset = nftAsset.copy( + amount = BigInteger.ONE, + ) + val nftTransferCallData = SmartContractCallDataProviderFactory.getNFTTransferCallData( destinationAddress = destinationAddress, ownerAddress = ownerAddress, - nftAsset = nftAsset, + nftAsset = updatedNFTAsset, blockchain = blockchain, ) @@ -199,7 +204,7 @@ internal class DefaultTransactionRepository( return@withContext createTransaction( amount = Amount( - value = nftAsset.amount?.toBigDecimal() ?: error("Invalid amount"), + value = updatedNFTAsset.amount?.toBigDecimal() ?: error("Invalid amount"), token = Token( symbol = blockchain.currency, contractAddress = contractAddress, From 295f236805b0d4955de7433b588c890190604a89 Mon Sep 17 00:00:00 2001 From: Tangem Date: Sat, 17 May 2025 17:51:43 +0400 Subject: [PATCH 049/165] Updated on 2026-08-14 --- common/test/build.gradle.kts | 2 + .../network/MockNetworkStatusFactory.kt | 13 +- .../MockUpdateWalletManagerResultFactory.kt | 2 +- .../common/test/utils/ProvideTestModels.kt | 11 + data/common/build.gradle.kts | 7 +- .../currency/CardCryptoCurrencyFactory.kt | 13 + .../DefaultCardCryptoCurrencyFactory.kt | 33 +- .../DefaultCardCryptoCurrencyFactoryTest.kt | 753 +++++++++++------- .../fetcher/CommonNetworkStatusFetcher.kt | 69 ++ .../fetcher/CommonNetworkStatusFetcherTest.kt | 161 ++++ 10 files changed, 747 insertions(+), 317 deletions(-) create mode 100644 common/test/src/main/java/com/tangem/common/test/utils/ProvideTestModels.kt create mode 100644 data/networks/src/main/java/com/tangem/data/networks/fetcher/CommonNetworkStatusFetcher.kt create mode 100644 data/networks/src/test/java/com/tangem/data/networks/fetcher/CommonNetworkStatusFetcherTest.kt diff --git a/common/test/build.gradle.kts b/common/test/build.gradle.kts index 3013d947d3..52931ce5f3 100644 --- a/common/test/build.gradle.kts +++ b/common/test/build.gradle.kts @@ -31,4 +31,6 @@ dependencies { implementation(tangemDeps.blockchain) implementation(tangemDeps.card.core) + + implementation(deps.test.junit5) } \ No newline at end of file diff --git a/common/test/src/main/java/com/tangem/common/test/domain/network/MockNetworkStatusFactory.kt b/common/test/src/main/java/com/tangem/common/test/domain/network/MockNetworkStatusFactory.kt index 6a1da92250..84cdecffc6 100644 --- a/common/test/src/main/java/com/tangem/common/test/domain/network/MockNetworkStatusFactory.kt +++ b/common/test/src/main/java/com/tangem/common/test/domain/network/MockNetworkStatusFactory.kt @@ -14,20 +14,25 @@ object MockNetworkStatusFactory { private val defaultNetwork = MockCryptoCurrencyFactory().ethereum.network - fun createVerified(network: Network = defaultNetwork, source: StatusSource = StatusSource.ACTUAL): NetworkStatus { + fun createVerified( + network: Network = defaultNetwork, + source: StatusSource = StatusSource.ACTUAL, + transform: (NetworkStatus.Verified) -> NetworkStatus.Verified = { it }, + ): NetworkStatus { return NetworkStatus( network = network, value = NetworkStatus.Verified( address = NetworkAddress.Single( defaultAddress = NetworkAddress.Address( - value = "0x123", + value = "0x1", type = NetworkAddress.Address.Type.Primary, ), ), amounts = mapOf(), pendingTransactions = mapOf(), source = source, - ), + ) + .let(transform), ) } @@ -37,7 +42,7 @@ object MockNetworkStatusFactory { value = NetworkStatus.NoAccount( address = NetworkAddress.Single( defaultAddress = NetworkAddress.Address( - value = "0x123", + value = "0x1", type = NetworkAddress.Address.Type.Primary, ), ), diff --git a/common/test/src/main/java/com/tangem/common/test/domain/walletmanager/MockUpdateWalletManagerResultFactory.kt b/common/test/src/main/java/com/tangem/common/test/domain/walletmanager/MockUpdateWalletManagerResultFactory.kt index b7684db809..e7ae9b4214 100644 --- a/common/test/src/main/java/com/tangem/common/test/domain/walletmanager/MockUpdateWalletManagerResultFactory.kt +++ b/common/test/src/main/java/com/tangem/common/test/domain/walletmanager/MockUpdateWalletManagerResultFactory.kt @@ -27,7 +27,7 @@ class MockUpdateWalletManagerResultFactory { return UpdateWalletManagerResult.NoAccount( selectedAddress = "0x1", addresses = setOf(Address(value = "0x1", type = Address.Type.Primary)), - amountToCreateAccount = BigDecimal.ZERO, + amountToCreateAccount = BigDecimal.ONE, errorMessage = "", ) } diff --git a/common/test/src/main/java/com/tangem/common/test/utils/ProvideTestModels.kt b/common/test/src/main/java/com/tangem/common/test/utils/ProvideTestModels.kt new file mode 100644 index 0000000000..a0e0882940 --- /dev/null +++ b/common/test/src/main/java/com/tangem/common/test/utils/ProvideTestModels.kt @@ -0,0 +1,11 @@ +package com.tangem.common.test.utils + +import org.junit.jupiter.params.provider.MethodSource + +/** +[REDACTED_AUTHOR] + */ +@Target(AnnotationTarget.FUNCTION) +@Retention(AnnotationRetention.RUNTIME) +@MethodSource("provideTestModels") +annotation class ProvideTestModels \ No newline at end of file diff --git a/data/common/build.gradle.kts b/data/common/build.gradle.kts index 05ffe54f76..7d10aec65e 100644 --- a/data/common/build.gradle.kts +++ b/data/common/build.gradle.kts @@ -9,6 +9,10 @@ android { namespace = "com.tangem.data.common" } +tasks.withType().configureEach { + useJUnitPlatform() +} + dependencies { /* Core */ implementation(projects.core.datasource) @@ -39,7 +43,8 @@ dependencies { /* Test */ testImplementation(projects.common.test) testImplementation(deps.test.coroutine) - testImplementation(deps.test.junit) + testImplementation(deps.test.junit5) + testRuntimeOnly(deps.test.junit5.engine) testImplementation(deps.test.mockk) testImplementation(deps.test.truth) } \ No newline at end of file diff --git a/data/common/src/main/kotlin/com/tangem/data/common/currency/CardCryptoCurrencyFactory.kt b/data/common/src/main/kotlin/com/tangem/data/common/currency/CardCryptoCurrencyFactory.kt index fca4ffba1a..1a2df50544 100644 --- a/data/common/src/main/kotlin/com/tangem/data/common/currency/CardCryptoCurrencyFactory.kt +++ b/data/common/src/main/kotlin/com/tangem/data/common/currency/CardCryptoCurrencyFactory.kt @@ -3,6 +3,7 @@ package com.tangem.data.common.currency import com.tangem.domain.models.scan.ScanResponse import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.domain.tokens.model.Network +import com.tangem.domain.wallets.models.UserWallet import com.tangem.domain.wallets.models.UserWalletId /** @@ -21,6 +22,18 @@ interface CardCryptoCurrencyFactory { @Throws suspend fun create(userWalletId: UserWalletId, network: Network): List + /** + * Create currencies for multi currency card + * + * @param userWallet user wallet + * @param networks networks + */ + @Throws + suspend fun createCurrenciesForMultiCurrencyCard( + userWallet: UserWallet, + networks: Set, + ): Map> + /** * Create default coins for multi currency card * diff --git a/data/common/src/main/kotlin/com/tangem/data/common/currency/DefaultCardCryptoCurrencyFactory.kt b/data/common/src/main/kotlin/com/tangem/data/common/currency/DefaultCardCryptoCurrencyFactory.kt index c4da63c87c..186e0da14f 100644 --- a/data/common/src/main/kotlin/com/tangem/data/common/currency/DefaultCardCryptoCurrencyFactory.kt +++ b/data/common/src/main/kotlin/com/tangem/data/common/currency/DefaultCardCryptoCurrencyFactory.kt @@ -37,7 +37,9 @@ internal class DefaultCardCryptoCurrencyFactory( val blockchain = Blockchain.fromNetworkId(networkId = network.backendId) // multi-currency wallet - if (userWallet.isMultiCurrency) return getMultiWalletCurrencies(userWallet = userWallet, network = network) + if (userWallet.isMultiCurrency) { + return getMultiWalletCurrencies(userWallet = userWallet, networks = setOf(network))[network].orEmpty() + } // check if the blockchain of single-currency wallet is the same as network val cardBlockchain = userWallet.scanResponse.cardTypesResolver.getBlockchain() @@ -52,7 +54,18 @@ internal class DefaultCardCryptoCurrencyFactory( return createPrimaryCurrencyForSingleCurrencyCard(userWallet.scanResponse).let(::listOf) } + override suspend fun createCurrenciesForMultiCurrencyCard( + userWallet: UserWallet, + networks: Set, + ): Map> { + require(userWallet.isMultiCurrency) { "It isn't multi-currency wallet" } + + return getMultiWalletCurrencies(userWallet = userWallet, networks = networks) + } + override fun createDefaultCoinsForMultiCurrencyCard(scanResponse: ScanResponse): List { + require(scanResponse.cardTypesResolver.isMultiwalletAllowed()) { "It isn't multi-currency wallet" } + val card = scanResponse.card var blockchains = if (demoConfig.isDemoCardId(card.cardId)) { @@ -75,29 +88,39 @@ internal class DefaultCardCryptoCurrencyFactory( } override fun createPrimaryCurrencyForSingleCurrencyCard(scanResponse: ScanResponse): CryptoCurrency { + require(scanResponse.cardTypesResolver.isSingleWallet()) { "It isn't single-currency wallet" } + return with(getSingleWalletCurrencies(scanResponse)) { primaryToken ?: coin } } override fun createCurrenciesForSingleCurrencyCardWithToken(scanResponse: ScanResponse): List { + require(scanResponse.cardTypesResolver.isSingleWalletWithToken()) { + "It isn't single-currency wallet with token" + } + return with(getSingleWalletCurrencies(scanResponse)) { listOfNotNull(coin, primaryToken) } } - private suspend fun getMultiWalletCurrencies(userWallet: UserWallet, network: Network): List { + private suspend fun getMultiWalletCurrencies( + userWallet: UserWallet, + networks: Set, + ): Map> { val response = userTokensResponseStore.getSyncOrNull(userWalletId = userWallet.walletId) - ?: return emptyList() + ?: return emptyMap() val responseCurrenciesFactory = ResponseCryptoCurrenciesFactory(excludedBlockchains) return responseCurrenciesFactory.createCurrencies( - tokens = response.tokens.filter { - it.networkId == network.backendId && it.derivationPath == network.derivationPath.value + tokens = response.tokens.filter { token -> + networks.any { it.backendId == token.networkId && it.derivationPath.value == token.derivationPath } }, scanResponse = userWallet.scanResponse, ) + .groupBy(CryptoCurrency::network) } private fun getSingleWalletCurrencies(scanResponse: ScanResponse): SingleWalletCurrencies { diff --git a/data/common/src/test/kotlin/com/tangem/data/common/currency/DefaultCardCryptoCurrencyFactoryTest.kt b/data/common/src/test/kotlin/com/tangem/data/common/currency/DefaultCardCryptoCurrencyFactoryTest.kt index e6b868dc39..21ae9dd504 100644 --- a/data/common/src/test/kotlin/com/tangem/data/common/currency/DefaultCardCryptoCurrencyFactoryTest.kt +++ b/data/common/src/test/kotlin/com/tangem/data/common/currency/DefaultCardCryptoCurrencyFactoryTest.kt @@ -7,6 +7,8 @@ import com.tangem.blockchainsdk.utils.ExcludedBlockchains import com.tangem.common.card.WalletData import com.tangem.common.test.domain.card.MockScanResponseFactory import com.tangem.common.test.domain.token.MockCryptoCurrencyFactory +import com.tangem.common.test.utils.ProvideTestModels +import com.tangem.datasource.api.tangemTech.models.UserTokensResponse import com.tangem.datasource.local.token.UserTokensResponseStore import com.tangem.datasource.local.userwallet.UserWalletsStore import com.tangem.domain.common.configs.GenericCardConfig @@ -14,16 +16,20 @@ import com.tangem.domain.common.util.cardTypesResolver import com.tangem.domain.demo.DemoConfig import com.tangem.domain.models.scan.ProductType import com.tangem.domain.tokens.model.CryptoCurrency +import com.tangem.domain.tokens.model.Network import com.tangem.domain.wallets.models.UserWallet import com.tangem.domain.wallets.models.UserWalletId import io.mockk.* import kotlinx.coroutines.test.runTest -import org.junit.Before -import org.junit.Test +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Nested +import org.junit.jupiter.api.TestInstance +import org.junit.jupiter.params.ParameterizedTest /** [REDACTED_AUTHOR] */ +@TestInstance(TestInstance.Lifecycle.PER_CLASS) internal class DefaultCardCryptoCurrencyFactoryTest { private val userWalletsStore: UserWalletsStore = mockk() @@ -36,325 +42,427 @@ internal class DefaultCardCryptoCurrencyFactoryTest { userTokensResponseStore = userTokensResponseStore, ) - @Before - fun setup() { + private val cryptoCurrencyFactory = MockCryptoCurrencyFactory() + private val ethereum = cryptoCurrencyFactory.ethereum.setCanHandleTokens(value = true) + private val bitcoin = cryptoCurrencyFactory.createCoin(blockchain = Blockchain.Bitcoin) + + private val userTokensResponseFactory = UserTokensResponseFactory() + private val iconUri: Uri = mockk() + + @BeforeEach + fun init() { + clearMocks(userWalletsStore, userTokensResponseStore, iconUri) + mockkStatic(Uri::class) - every { Uri.parse(any()) } returns mockk() + every { Uri.parse(any()) } returns iconUri } - @Test - fun `test create if userTokensResponse is not empty`() = runTest { - val multiWallet = createMultiWallet() + @Nested + @TestInstance(TestInstance.Lifecycle.PER_CLASS) + inner class CreateMultiWallet { - val userTokensResponse = UserTokensResponseFactory().createUserTokensResponse( - currencies = listOf(ethereum), - isGroupedByNetwork = false, - isSortedByBalance = false, + @ParameterizedTest + @ProvideTestModels + fun `create currencies in ETH for multi-currency wallet`(model: CreateTestModel.MultiWallet) = runTest { + // Arrange + val userWallet = createMultiWallet() + val userTokensResponse = model.userTokensResponse + val network = ethereum.network + + coEvery { userWalletsStore.getSyncStrict(key = userWallet.walletId) } returns userWallet + coEvery { userTokensResponseStore.getSyncOrNull(userWallet.walletId) } returns userTokensResponse + + // Act + val actual = factory.create(userWalletId = userWallet.walletId, network = network) + + // Assert + val expected = model.expected + + Truth.assertThat(actual).isEqualTo(expected) + + coVerifyOrder { + userWalletsStore.getSyncStrict(key = userWallet.walletId) + userTokensResponseStore.getSyncOrNull(userWalletId = userWallet.walletId) + } + } + + private fun provideTestModels() = listOf( + CreateTestModel.MultiWallet(userTokensResponse = null, expected = emptyList()), + CreateTestModel.MultiWallet(userTokensResponse = createUserTokensResponse(), expected = emptyList()), + CreateTestModel.MultiWallet( + userTokensResponse = createUserTokensResponse(currencies = listOf(ethereum)), + expected = listOf(ethereum), + ), + CreateTestModel.MultiWallet( + userTokensResponse = createUserTokensResponse(listOf(element = bitcoin)), + expected = emptyList(), + ), ) - coEvery { userWalletsStore.getSyncStrict(key = multiWallet.walletId) } returns multiWallet - coEvery { userTokensResponseStore.getSyncOrNull(multiWallet.walletId) } returns userTokensResponse - - val actual = factory.create(userWalletId = multiWallet.walletId, network = ethereum.network) - - coVerifyOrder { - userWalletsStore.getSyncStrict(key = multiWallet.walletId) - userTokensResponseStore.getSyncOrNull(multiWallet.walletId) - } - - val expected = listOf(ethereum) - - Truth.assertThat(actual).isEqualTo(expected) - } - - @Test - fun `test create if userTokensResponse is empty`() = runTest { - val multiWallet = createMultiWallet() - - val userTokensResponse = UserTokensResponseFactory().createUserTokensResponse( - currencies = listOf(), - isGroupedByNetwork = false, - isSortedByBalance = false, - ) - - coEvery { userWalletsStore.getSyncStrict(key = multiWallet.walletId) } returns multiWallet - coEvery { userTokensResponseStore.getSyncOrNull(multiWallet.walletId) } returns userTokensResponse - - val actual = factory.create(userWalletId = multiWallet.walletId, network = ethereum.network) - - coVerifyOrder { - userWalletsStore.getSyncStrict(key = multiWallet.walletId) - userTokensResponseStore.getSyncOrNull(multiWallet.walletId) - } - - val expected = emptyList() - - Truth.assertThat(actual).isEqualTo(expected) - } - - @Test - fun `test create if userTokensResponse is null`() = runTest { - val multiWallet = createMultiWallet() - - coEvery { userWalletsStore.getSyncStrict(key = multiWallet.walletId) } returns multiWallet - coEvery { userTokensResponseStore.getSyncOrNull(multiWallet.walletId) } returns null - - val actual = factory.create(userWalletId = multiWallet.walletId, network = ethereum.network) - - coVerifyOrder { - userWalletsStore.getSyncStrict(key = multiWallet.walletId) - userTokensResponseStore.getSyncOrNull(multiWallet.walletId) - } - - val expected = emptyList() - - Truth.assertThat(actual).isEqualTo(expected) - } - - @Test - fun `test create if userTokensResponse does not contain currency of selected network`() = runTest { - val multiWallet = createMultiWallet() - - val userTokensResponse = UserTokensResponseFactory().createUserTokensResponse( - currencies = listOf(bitcoin), - isGroupedByNetwork = false, - isSortedByBalance = false, - ) - - coEvery { userWalletsStore.getSyncStrict(key = multiWallet.walletId) } returns multiWallet - coEvery { userTokensResponseStore.getSyncOrNull(multiWallet.walletId) } returns userTokensResponse - - val actual = factory.create(userWalletId = multiWallet.walletId, network = ethereum.network) - - coVerifyOrder { - userWalletsStore.getSyncStrict(key = multiWallet.walletId) - userTokensResponseStore.getSyncOrNull(multiWallet.walletId) - } - - val expected = emptyList() - - Truth.assertThat(actual).isEqualTo(expected) - } - - @Test - fun `test create if single wallet has another primary network`() = runTest { - val singleWallet = createSingleWallet() - - coEvery { userWalletsStore.getSyncStrict(key = singleWallet.walletId) } returns singleWallet - - val actual = factory.create(userWalletId = singleWallet.walletId, network = bitcoin.network) - - coVerifyOrder { - userWalletsStore.getSyncStrict(key = singleWallet.walletId) - singleWallet.scanResponse.cardTypesResolver.getBlockchain() - } - - val expected = emptyList() - - Truth.assertThat(actual).isEqualTo(expected) - } - - @Test - fun `test create if card is single wallet`() = runTest { - val singleWallet = createSingleWallet() - - coEvery { userWalletsStore.getSyncStrict(key = singleWallet.walletId) } returns singleWallet - - val actual = factory.create(userWalletId = singleWallet.walletId, network = ethereum.network) - - coVerifyOrder { - userWalletsStore.getSyncStrict(key = singleWallet.walletId) - singleWallet.scanResponse.cardTypesResolver.getBlockchain() - } - - val expected = listOf(ethereum) - - Truth.assertThat(actual).isEqualTo(expected) - } - - @Test - fun `test create if card is single wallet with token`() = runTest { - val singleWallet = createSingleWalletWithToken() - - coEvery { userWalletsStore.getSyncStrict(key = singleWallet.walletId) } returns singleWallet - - val actual = factory.create(userWalletId = singleWallet.walletId, network = ethereum.network) - - val token = CryptoCurrencyFactory(excludedBlockchains = ExcludedBlockchains()).createToken( - sdkToken = singleWallet.scanResponse.cardTypesResolver.getPrimaryToken()!!, - blockchain = Blockchain.Ethereum, - extraDerivationPath = null, - scanResponse = singleWallet.scanResponse, - ) - val expected = listOf(ethereum, token) - - Truth.assertThat(actual).isEqualTo(expected) - } - - @Test - fun `test createDefaultCoinsForMultiCurrencyCard if card is prod`() = runTest { - val multiWallet = createMultiWallet() - - val actual = factory.createDefaultCoinsForMultiCurrencyCard(scanResponse = multiWallet.scanResponse) - - val expected = listOf(bitcoin, ethereum) - - Truth.assertThat(actual).isEqualTo(expected) - } - - @Test - fun `test createDefaultCoinsForMultiCurrencyCard if card is test`() = runTest { - val multiWallet = createMultiWallet().let { - it.copy( - scanResponse = it.scanResponse.copy( - card = it.scanResponse.card.copy(cardId = "FF99", batchId = "99FF"), - ), + private fun createUserTokensResponse(currencies: List = emptyList()): UserTokensResponse { + return userTokensResponseFactory.createUserTokensResponse( + currencies = currencies, + isGroupedByNetwork = false, + isSortedByBalance = false, ) } - - val actual = factory.createDefaultCoinsForMultiCurrencyCard(scanResponse = multiWallet.scanResponse) - - val expected = listOf( - cryptoCurrencyFactory.createCoin(blockchain = Blockchain.BitcoinTestnet), - cryptoCurrencyFactory.createCoin(blockchain = Blockchain.EthereumTestnet).setCanHandleTokens(true), - ) - - Truth.assertThat(actual).isEqualTo(expected) } - @Test - fun `test createDefaultCoinsForMultiCurrencyCard if card is demo`() = runTest { - val multiWallet = createMultiWallet().let { - it.copy( - scanResponse = it.scanResponse.copy( - card = it.scanResponse.card.copy(cardId = "AC01000000041225"), + @Nested + @TestInstance(TestInstance.Lifecycle.PER_CLASS) + inner class CreateSingleWallet { + + @ParameterizedTest + @ProvideTestModels + fun `create currencies for single-currency wallet (ETH)`(model: CreateTestModel.SingleWallet) = runTest { + // Arrange + val userWallet = createSingleWallet() + + coEvery { userWalletsStore.getSyncStrict(key = userWallet.walletId) } returns userWallet + + // Act + val actual = factory.create(userWalletId = userWallet.walletId, network = model.network) + + // Assert + val expected = model.expected + + Truth.assertThat(actual).isEqualTo(expected) + + coVerifyOrder { + userWalletsStore.getSyncStrict(key = userWallet.walletId) + userWallet.scanResponse.cardTypesResolver.getBlockchain() + } + + coVerify(inverse = true) { + userTokensResponseStore.getSyncOrNull(userWalletId = any()) + } + } + + private fun provideTestModels() = listOf( + CreateTestModel.SingleWallet(network = ethereum.network, expected = listOf(ethereum)), + CreateTestModel.SingleWallet(network = bitcoin.network, expected = emptyList()), + ) + } + + @Nested + @TestInstance(TestInstance.Lifecycle.PER_CLASS) + inner class CreateSingleWalletWithToken { + + @ParameterizedTest + @ProvideTestModels + fun `create currencies for single-currency wallet with token (ETH)`( + model: CreateTestModel.SingleWalletWithToken, + ) = runTest { + // Arrange + val userWallet = createSingleWalletWithToken() + + coEvery { userWalletsStore.getSyncStrict(key = userWallet.walletId) } returns userWallet + + // Act + val actual = factory.create(userWalletId = userWallet.walletId, network = model.network) + + // Assert + val primaryToken = if (model.isPrimaryTokenExpected) { + createPrimaryToken(blockchain = Blockchain.Ethereum) + } else { + null + } + + val expected = listOfNotNull(*model.expected.toTypedArray(), primaryToken) + + Truth.assertThat(actual).isEqualTo(expected) + + coVerifyOrder { + userWalletsStore.getSyncStrict(key = userWallet.walletId) + userWallet.scanResponse.cardTypesResolver.getBlockchain() + } + + coVerify(inverse = true) { + userTokensResponseStore.getSyncOrNull(userWalletId = any()) + } + } + + private fun provideTestModels() = listOf( + CreateTestModel.SingleWalletWithToken( + network = ethereum.network, + isPrimaryTokenExpected = true, + expected = listOf(ethereum), + ), + CreateTestModel.SingleWalletWithToken( + network = bitcoin.network, + isPrimaryTokenExpected = false, + expected = emptyList(), + ), + ) + } + + sealed interface CreateTestModel { + + val expected: List + + data class MultiWallet( + val userTokensResponse: UserTokensResponse?, + override val expected: List, + ) : CreateTestModel + + data class SingleWallet( + val network: Network, + override val expected: List, + ) : CreateTestModel + + data class SingleWalletWithToken( + val network: Network, + val isPrimaryTokenExpected: Boolean, + override val expected: List, + ) : CreateTestModel + } + + @Nested + @TestInstance(TestInstance.Lifecycle.PER_CLASS) + inner class CreateCurrenciesForMultiCurrencyCard { + + @ParameterizedTest + @ProvideTestModels + fun `create currencies in ETH and BTC for multi-currency card`(model: CreateCurrenciesForMultiWalletModel) = + runTest { + // Arrange + val userWallet = model.multiWallet + val networks = setOf(ethereum.network, bitcoin.network) + val userTokensResponse = model.userTokensResponse + + coEvery { userTokensResponseStore.getSyncOrNull(userWallet.walletId) } returns userTokensResponse + + // Act + val actual = runCatching { + factory.createCurrenciesForMultiCurrencyCard(userWallet = userWallet, networks = networks) + } + + // Assert + actual + .onSuccess { + val expected = model.expected.getOrNull()!! + + Truth.assertThat(it).isEqualTo(expected) + } + .onFailure { + val exception = model.expected.exceptionOrNull()!! + + Truth.assertThat(it).isInstanceOf(exception::class.java) + Truth.assertThat(it).hasMessageThat().isEqualTo(exception.message) + } + } + + private fun provideTestModels() = listOf( + CreateCurrenciesForMultiWalletModel( + multiWallet = createMultiWallet(), + userTokensResponse = null, + expected = Result.success(emptyMap()), + ), + CreateCurrenciesForMultiWalletModel( + multiWallet = createMultiWallet(), + userTokensResponse = createUserTokensResponse(), + expected = Result.success(emptyMap()), + ), + CreateCurrenciesForMultiWalletModel( + multiWallet = createMultiWallet(), + userTokensResponse = createUserTokensResponse(currencies = listOf(bitcoin, ethereum)), + expected = mapOf( + bitcoin.network to listOf(bitcoin), + ethereum.network to listOf(ethereum), + ).let(Result.Companion::success), + ), + CreateCurrenciesForMultiWalletModel( + multiWallet = createSingleWallet(), + userTokensResponse = null, + expected = Result.failure(IllegalArgumentException("It isn't multi-currency wallet")), + ), + CreateCurrenciesForMultiWalletModel( + multiWallet = createSingleWalletWithToken(), + userTokensResponse = null, + expected = Result.failure(IllegalArgumentException("It isn't multi-currency wallet")), + ), + ) + } + + data class CreateCurrenciesForMultiWalletModel( + val multiWallet: UserWallet, + val userTokensResponse: UserTokensResponse?, + val expected: Result>>, + ) + + @Nested + @TestInstance(TestInstance.Lifecycle.PER_CLASS) + inner class CreateDefaultCoinsForMultiCurrencyCard { + + @ParameterizedTest + @ProvideTestModels + fun `create default coins for multi currency card`(model: CreateDefaultForMultiWalletModel) = runTest { + // Arrange + val multiWallet = model.multiWallet + + // Act + val actual = factory.createDefaultCoinsForMultiCurrencyCard(scanResponse = multiWallet.scanResponse) + + // Assert + val expected = model.expected + + Truth.assertThat(actual).isEqualTo(expected) + } + + private fun provideTestModels() = listOf( + // PROD card + CreateDefaultForMultiWalletModel(multiWallet = createMultiWallet(), expected = listOf(bitcoin, ethereum)), + // TEST card + CreateDefaultForMultiWalletModel( + multiWallet = createMultiWallet(cardId = "FF99", batchId = "99FF"), + expected = listOf( + cryptoCurrencyFactory.createCoin(blockchain = Blockchain.BitcoinTestnet), + cryptoCurrencyFactory.createCoin(blockchain = Blockchain.EthereumTestnet).setCanHandleTokens(true), ), - ) - } - - val actual = factory.createDefaultCoinsForMultiCurrencyCard(scanResponse = multiWallet.scanResponse) - - val expected = listOf( - bitcoin, - ethereum, - cryptoCurrencyFactory.createCoin(blockchain = Blockchain.Dogecoin), - cryptoCurrencyFactory.createCoin(blockchain = Blockchain.Solana), - ) - - Truth.assertThat(actual).isEqualTo(expected) - } - - @Test - fun `test createPrimaryCurrencyForSingleCurrencyCard if unable to create token`() = runTest { - val singleWallet = UserWallet( - name = "Note", - walletId = UserWalletId("011"), - cardsInWallet = setOf(), - isMultiCurrency = false, - scanResponse = MockScanResponseFactory.create( - cardConfig = GenericCardConfig(maxWalletCount = 2), - derivedKeys = emptyMap(), ), - hasBackupError = false, - ) - - val actual = runCatching { - factory.createPrimaryCurrencyForSingleCurrencyCard(scanResponse = singleWallet.scanResponse) - } - - val exception = IllegalArgumentException("Coin for the single currency card cannot be null") - - Truth.assertThat(actual.isFailure).isTrue() - Truth.assertThat(actual.exceptionOrNull()).isInstanceOf(exception::class.java) - Truth.assertThat(actual.exceptionOrNull()).hasMessageThat().isEqualTo(exception.message) - } - - @Test - fun `test createPrimaryCurrencyForSingleCurrencyCard if primaryToken is null`() = runTest { - val singleWallet = createSingleWallet() - - val actual = factory.createPrimaryCurrencyForSingleCurrencyCard(scanResponse = singleWallet.scanResponse) - - val expected = ethereum - - Truth.assertThat(actual).isEqualTo(expected) - } - - @Test - fun `test createPrimaryCurrencyForSingleCurrencyCard if primaryToken is not null`() = runTest { - val singleWallet = createSingleWalletWithToken() - - val actual = factory.createPrimaryCurrencyForSingleCurrencyCard(scanResponse = singleWallet.scanResponse) - - val expected = CryptoCurrencyFactory(excludedBlockchains = ExcludedBlockchains()).createToken( - sdkToken = singleWallet.scanResponse.cardTypesResolver.getPrimaryToken()!!, - blockchain = Blockchain.Ethereum, - extraDerivationPath = null, - scanResponse = singleWallet.scanResponse, - ) - - Truth.assertThat(actual).isEqualTo(expected) - } - - @Test - fun `test createCurrenciesForSingleCurrencyCardWithToken if unable to create token`() = runTest { - val singleWalletWithToken = UserWallet( - name = "Note", - walletId = UserWalletId("011"), - cardsInWallet = setOf(), - isMultiCurrency = false, - scanResponse = MockScanResponseFactory.create( - cardConfig = GenericCardConfig(maxWalletCount = 2), - derivedKeys = emptyMap(), + // // DEMO card + CreateDefaultForMultiWalletModel( + multiWallet = createMultiWallet(cardId = "AC01000000041225"), + expected = listOf( + bitcoin, + ethereum, + cryptoCurrencyFactory.createCoin(blockchain = Blockchain.Dogecoin), + cryptoCurrencyFactory.createCoin(blockchain = Blockchain.Solana), + ), ), - hasBackupError = false, ) - - val actual = runCatching { - factory.createCurrenciesForSingleCurrencyCardWithToken(scanResponse = singleWalletWithToken.scanResponse) - } - - val exception = IllegalArgumentException("Coin for the single currency card cannot be null") - - Truth.assertThat(actual.isFailure).isTrue() - Truth.assertThat(actual.exceptionOrNull()).isInstanceOf(exception::class.java) - Truth.assertThat(actual.exceptionOrNull()).hasMessageThat().isEqualTo(exception.message) } - @Test - fun `test createCurrenciesForSingleCurrencyCardWithToken if primaryToken is null`() = runTest { - val singleWalletWithToken = createSingleWallet() + data class CreateDefaultForMultiWalletModel(val multiWallet: UserWallet, val expected: List) - val actual = factory.createCurrenciesForSingleCurrencyCardWithToken( - scanResponse = singleWalletWithToken.scanResponse, + @Nested + @TestInstance(TestInstance.Lifecycle.PER_CLASS) + inner class CreatePrimaryCurrencyForSingleCurrencyCard { + + @ParameterizedTest + @ProvideTestModels + fun `create primary currency for single currency card`(model: CreatePrimaryCurrencyForSingleWalletModel) = + runTest { + // Arrange + val singleWallet = model.singleWallet + + // Act + val actual = runCatching { + factory.createPrimaryCurrencyForSingleCurrencyCard(scanResponse = singleWallet.scanResponse) + } + + // Assert + actual + .onSuccess { + Truth.assertThat(actual).isEqualTo(model.expected) + } + .onFailure { + val exception = model.expected.exceptionOrNull()!! + + Truth.assertThat(actual.exceptionOrNull()).isInstanceOf(exception::class.java) + Truth.assertThat(actual.exceptionOrNull()).hasMessageThat().isEqualTo(exception.message) + } + } + + private fun provideTestModels() = listOf( + CreatePrimaryCurrencyForSingleWalletModel( + singleWallet = createSingleWallet(), + expected = Result.success(ethereum), + ), + CreatePrimaryCurrencyForSingleWalletModel( + singleWallet = createSingleWallet(batchId = ""), + expected = Result.failure(IllegalArgumentException("Coin for the single currency card cannot be null")), + ), + CreatePrimaryCurrencyForSingleWalletModel( + singleWallet = createSingleWallet(addWalletData = true), + expected = Result.failure(IllegalArgumentException("It isn't single-currency wallet")), + ), ) - - val expected = listOf(ethereum) - - Truth.assertThat(actual).isEqualTo(expected) } - @Test - fun `test createCurrenciesForSingleCurrencyCardWithToken if primaryToken is not null`() = runTest { - val singleWalletWithToken = createSingleWalletWithToken() + data class CreatePrimaryCurrencyForSingleWalletModel( + val singleWallet: UserWallet, + val expected: Result, + ) - val actual = factory.createCurrenciesForSingleCurrencyCardWithToken( - scanResponse = singleWalletWithToken.scanResponse, + @Nested + @TestInstance(TestInstance.Lifecycle.PER_CLASS) + inner class CreateCurrenciesForSingleCurrencyCardWithToken { + + @ParameterizedTest + @ProvideTestModels + fun `create currencies for single currency card with token`(model: CreateForSingleWalletWithTokenModel) = + runTest { + // Arrange + val userWallet = model.singleWalletWithToken + + // Act + val actual = runCatching { + factory.createCurrenciesForSingleCurrencyCardWithToken(scanResponse = userWallet.scanResponse) + } + + // Assert + actual + .onSuccess { + val primaryToken = if (model.isPrimaryTokenExpected) { + createPrimaryToken(blockchain = Blockchain.Ethereum) + } else { + null + } + + val expected = listOfNotNull(*model.expected.getOrNull()!!.toTypedArray(), primaryToken) + + Truth.assertThat(it).isEqualTo(expected) + } + .onFailure { + val exception = model.expected.exceptionOrNull()!! + + Truth.assertThat(it).isInstanceOf(exception::class.java) + Truth.assertThat(it).hasMessageThat().isEqualTo(exception.message) + } + } + + private fun provideTestModels() = listOf( + CreateForSingleWalletWithTokenModel( + singleWalletWithToken = UserWallet( + name = "NODL", + walletId = UserWalletId("011"), + cardsInWallet = setOf(), + isMultiCurrency = false, + scanResponse = MockScanResponseFactory.create( + cardConfig = GenericCardConfig(maxWalletCount = 2), + derivedKeys = emptyMap(), + ).copy( + productType = ProductType.Note, + walletData = WalletData( + blockchain = "", + token = WalletData.Token( + name = "Ethereum", + symbol = "ETH", + contractAddress = "0x", + decimals = 8, + ), + ), + ), + hasBackupError = false, + ), + expected = Result.failure(IllegalArgumentException("Coin for the single currency card cannot be null")), + ), + CreateForSingleWalletWithTokenModel( + singleWalletWithToken = createSingleWalletWithToken(), + isPrimaryTokenExpected = true, + expected = Result.success(listOf(ethereum)), + ), ) - - val token = CryptoCurrencyFactory(excludedBlockchains = ExcludedBlockchains()).createToken( - sdkToken = singleWalletWithToken.scanResponse.cardTypesResolver.getPrimaryToken()!!, - blockchain = Blockchain.Ethereum, - extraDerivationPath = null, - scanResponse = singleWalletWithToken.scanResponse, - ) - - val expected = listOf(ethereum, token) - - Truth.assertThat(actual).isEqualTo(expected) } - private fun createMultiWallet(): UserWallet { + data class CreateForSingleWalletWithTokenModel( + val singleWalletWithToken: UserWallet, + val isPrimaryTokenExpected: Boolean = false, + val expected: Result>, + ) + + private fun createMultiWallet(cardId: String? = null, batchId: String? = null): UserWallet { return UserWallet( name = "Wallet 1", walletId = UserWalletId("011"), @@ -363,12 +471,19 @@ internal class DefaultCardCryptoCurrencyFactoryTest { scanResponse = MockScanResponseFactory.create( cardConfig = GenericCardConfig(maxWalletCount = 2), derivedKeys = emptyMap(), - ), + ).let { + it.copy( + card = it.card.copy( + cardId = cardId ?: it.card.cardId, + batchId = batchId ?: it.card.batchId, + ), + ) + }, hasBackupError = false, ) } - private fun createSingleWallet(): UserWallet { + private fun createSingleWallet(batchId: String? = null, addWalletData: Boolean = false): UserWallet { return UserWallet( name = "Note", walletId = UserWalletId("011"), @@ -379,8 +494,21 @@ internal class DefaultCardCryptoCurrencyFactoryTest { derivedKeys = emptyMap(), ).let { it.copy( - card = it.card.copy(batchId = "AB10"), + card = it.card.copy(batchId = batchId ?: "AB10"), productType = ProductType.Note, + walletData = if (addWalletData) { + WalletData( + blockchain = "ETH", + token = WalletData.Token( + name = "Ethereum", + symbol = "ETH", + contractAddress = "0x", + decimals = 8, + ), + ) + } else { + it.walletData + }, ) }, hasBackupError = false, @@ -412,14 +540,27 @@ internal class DefaultCardCryptoCurrencyFactoryTest { ) } + private fun createPrimaryToken(blockchain: Blockchain): CryptoCurrency.Token { + val userWallet = createSingleWalletWithToken() + + return CryptoCurrencyFactory(excludedBlockchains = ExcludedBlockchains()).createToken( + sdkToken = userWallet.scanResponse.cardTypesResolver.getPrimaryToken()!!, + blockchain = blockchain, + extraDerivationPath = null, + scanResponse = userWallet.scanResponse, + )!! + } + + private fun createUserTokensResponse(currencies: List = emptyList()): UserTokensResponse { + return userTokensResponseFactory.createUserTokensResponse( + currencies = currencies, + isGroupedByNetwork = false, + isSortedByBalance = false, + ) + } + private companion object { - val cryptoCurrencyFactory = MockCryptoCurrencyFactory() - - val ethereum = cryptoCurrencyFactory.ethereum.setCanHandleTokens(value = true) - - val bitcoin = cryptoCurrencyFactory.createCoin(blockchain = Blockchain.Bitcoin) - fun CryptoCurrency.setCanHandleTokens(value: Boolean): CryptoCurrency { return when (this) { is CryptoCurrency.Coin -> copy(network = network.copy(canHandleTokens = value)) diff --git a/data/networks/src/main/java/com/tangem/data/networks/fetcher/CommonNetworkStatusFetcher.kt b/data/networks/src/main/java/com/tangem/data/networks/fetcher/CommonNetworkStatusFetcher.kt new file mode 100644 index 0000000000..ba28c59281 --- /dev/null +++ b/data/networks/src/main/java/com/tangem/data/networks/fetcher/CommonNetworkStatusFetcher.kt @@ -0,0 +1,69 @@ +package com.tangem.data.networks.fetcher + +import arrow.core.Either +import com.tangem.data.networks.store.NetworksStatusesStoreV2 +import com.tangem.data.networks.store.setSourceAsOnlyCache +import com.tangem.data.networks.store.storeStatus +import com.tangem.data.networks.utils.NetworkStatusFactory +import com.tangem.domain.core.utils.catchOn +import com.tangem.domain.tokens.model.CryptoCurrency +import com.tangem.domain.tokens.model.Network +import com.tangem.domain.walletmanager.WalletManagersFacade +import com.tangem.domain.wallets.models.UserWalletId +import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import kotlinx.coroutines.withContext +import timber.log.Timber +import javax.inject.Inject + +/** + * Common implementation of network status fetcher + * + * @property walletManagersFacade wallet managers facade + * @property networksStatusesStore networks statuses store + * @property dispatchers dispatchers + * +[REDACTED_AUTHOR] + */ +internal class CommonNetworkStatusFetcher @Inject constructor( + private val walletManagersFacade: WalletManagersFacade, + private val networksStatusesStore: NetworksStatusesStoreV2, + private val dispatchers: CoroutineDispatcherProvider, +) { + + /** + * Fetch + * + * @param userWalletId user wallet id + * @param network network + * @param networkCurrencies network currencies + */ + suspend fun fetch( + userWalletId: UserWalletId, + network: Network, + networkCurrencies: Set, + ): Either { + return Either.catchOn(dispatchers.default) { + val result = withContext(dispatchers.io) { + walletManagersFacade.update( + userWalletId = userWalletId, + network = network, + extraTokens = networkCurrencies + .filterIsInstance() + .toSet(), + ) + } + + val status = NetworkStatusFactory.create( + network = network, + updatingResult = result, + addedCurrencies = networkCurrencies, + ) + + networksStatusesStore.storeStatus(userWalletId = userWalletId, status = status) + } + .onLeft { + Timber.e("Failed to fetch network status for $userWalletId [${network.id.value}]: $it") + networksStatusesStore.setSourceAsOnlyCache(userWalletId = userWalletId, network = network) + } + } +} \ No newline at end of file diff --git a/data/networks/src/test/java/com/tangem/data/networks/fetcher/CommonNetworkStatusFetcherTest.kt b/data/networks/src/test/java/com/tangem/data/networks/fetcher/CommonNetworkStatusFetcherTest.kt new file mode 100644 index 0000000000..da1191c0f2 --- /dev/null +++ b/data/networks/src/test/java/com/tangem/data/networks/fetcher/CommonNetworkStatusFetcherTest.kt @@ -0,0 +1,161 @@ +package com.tangem.data.networks.fetcher + +import arrow.core.Either +import com.google.common.truth.Truth +import com.tangem.blockchain.common.Blockchain +import com.tangem.common.test.domain.network.MockNetworkStatusFactory +import com.tangem.common.test.domain.token.MockCryptoCurrencyFactory +import com.tangem.common.test.domain.walletmanager.MockUpdateWalletManagerResultFactory +import com.tangem.common.test.utils.ProvideTestModels +import com.tangem.data.networks.store.NetworksStatusesStoreV2 +import com.tangem.data.networks.store.setSourceAsOnlyCache +import com.tangem.data.networks.store.storeStatus +import com.tangem.domain.tokens.model.CryptoCurrency +import com.tangem.domain.tokens.model.CryptoCurrencyAmountStatus +import com.tangem.domain.tokens.model.NetworkAddress +import com.tangem.domain.tokens.model.NetworkStatus +import com.tangem.domain.walletmanager.WalletManagersFacade +import com.tangem.domain.walletmanager.model.UpdateWalletManagerResult +import com.tangem.domain.wallets.models.UserWalletId +import com.tangem.utils.coroutines.TestingCoroutineDispatcherProvider +import io.mockk.* +import kotlinx.coroutines.test.runTest +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.TestInstance +import org.junit.jupiter.params.ParameterizedTest + +/** +[REDACTED_AUTHOR] + */ +@TestInstance(TestInstance.Lifecycle.PER_CLASS) +internal class CommonNetworkStatusFetcherTest { + + private val walletManagersFacade: WalletManagersFacade = mockk() + private val networksStatusesStore: NetworksStatusesStoreV2 = mockk(relaxUnitFun = true) + + private val fetcher = CommonNetworkStatusFetcher( + walletManagersFacade = walletManagersFacade, + networksStatusesStore = networksStatusesStore, + dispatchers = TestingCoroutineDispatcherProvider(), + ) + + private val cryptoCurrencyFactory = MockCryptoCurrencyFactory() + + @BeforeEach + fun resetMocks() { + clearMocks(walletManagersFacade, networksStatusesStore) + } + + @Test + fun `fetch failure if walletManagersFacade throws exception`() = runTest { + // Arrange + val userWalletId = UserWalletId("011") + val network = cryptoCurrencyFactory.ethereum.network + val extraTokens = setOf( + cryptoCurrencyFactory.createToken(Blockchain.Ethereum) as CryptoCurrency.Token, + ) + val updateException = IllegalStateException() + + coEvery { walletManagersFacade.update(userWalletId, network, extraTokens) } throws updateException + + // Act + val actual = fetcher.fetch(userWalletId = userWalletId, network = network, networkCurrencies = extraTokens) + + // Assert + val expected = Either.Left(updateException) + + Truth.assertThat(actual.isLeft()).isTrue() + Truth.assertThat(actual.leftOrNull()).isInstanceOf(expected.leftOrNull()!!::class.java) + Truth.assertThat(actual.leftOrNull()).hasMessageThat().isEqualTo(expected.leftOrNull()!!.message) + + coVerifyOrder { + walletManagersFacade.update(userWalletId, network, extraTokens) + } + } + + @ParameterizedTest + @ProvideTestModels + fun `fetch successfully for any result of walletManagersFacade`(model: SuccessTestModel) = runTest { + // Arrange + val userWalletId = UserWalletId("011") + val network = cryptoCurrencyFactory.ethereum.network + val extraTokens = setOf( + cryptoCurrencyFactory.createToken(Blockchain.Ethereum) as CryptoCurrency.Token, + ) + val updateResult = model.updateResult + val status = model.status + + coEvery { walletManagersFacade.update(userWalletId, network, extraTokens) } returns updateResult + coEvery { networksStatusesStore.storeStatus(userWalletId = userWalletId, status = status) } returns Unit + + // Act + val actual = fetcher.fetch(userWalletId = userWalletId, network = network, networkCurrencies = extraTokens) + + // Assert + val expected = Either.Right(Unit) + + Truth.assertThat(actual).isEqualTo(expected) + + coVerifyOrder { + walletManagersFacade.update(userWalletId, network, extraTokens) + networksStatusesStore.storeStatus(userWalletId = userWalletId, status = status) + } + + coVerify(inverse = true) { + networksStatusesStore.setSourceAsOnlyCache(userWalletId = any(), network = any()) + } + } + + private fun provideTestModels() = listOf( + SuccessTestModel( + updateResult = UpdateWalletManagerResult.MissedDerivation, + status = MockNetworkStatusFactory.createMissedDerivation(), + ), + SuccessTestModel( + updateResult = MockUpdateWalletManagerResultFactory().createUnreachable(), + status = NetworkStatus( + network = cryptoCurrencyFactory.ethereum.network, + value = NetworkStatus.Unreachable(address = null), + ), + ), + SuccessTestModel( + updateResult = MockUpdateWalletManagerResultFactory().createUnreachableWithAddress(), + status = NetworkStatus( + network = cryptoCurrencyFactory.ethereum.network, + value = NetworkStatus.Unreachable( + address = NetworkAddress.Single( + defaultAddress = NetworkAddress.Address( + value = "0x1", + type = NetworkAddress.Address.Type.Primary, + ), + ), + ), + ), + ), + SuccessTestModel( + updateResult = MockUpdateWalletManagerResultFactory().createNoAccount(), + status = MockNetworkStatusFactory.createNoAccount(), + ), + SuccessTestModel( + updateResult = MockUpdateWalletManagerResultFactory().createVerified(), + status = MockNetworkStatusFactory.createVerified(cryptoCurrencyFactory.ethereum.network) { + it.copy( + amounts = mapOf( + CryptoCurrency.ID.fromValue( + value = "token⟨ETH⟩NEVER-MIND⚓NEVER-MIND", + ) to CryptoCurrencyAmountStatus.NotFound, + ), + pendingTransactions = mapOf( + CryptoCurrency.ID.fromValue(value = "token⟨ETH⟩NEVER-MIND⚓NEVER-MIND") to emptySet(), + ), + ) + }, + ), + ) + + data class SuccessTestModel( + val updateResult: UpdateWalletManagerResult, + val status: NetworkStatus, + ) +} \ No newline at end of file From 03dce384bd68a59b792a7b00184566e1511efb6c Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 19 May 2025 13:47:12 +0500 Subject: [PATCH 050/165] Updated on 2026-08-14 --- .../modal/TangemModalBottomSheetWithFooter.kt | 311 ++++++++++++++++++ 1 file changed, 311 insertions(+) create mode 100644 core/ui/src/main/java/com/tangem/core/ui/components/bottomsheets/modal/TangemModalBottomSheetWithFooter.kt diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/bottomsheets/modal/TangemModalBottomSheetWithFooter.kt b/core/ui/src/main/java/com/tangem/core/ui/components/bottomsheets/modal/TangemModalBottomSheetWithFooter.kt new file mode 100644 index 0000000000..7b38499c71 --- /dev/null +++ b/core/ui/src/main/java/com/tangem/core/ui/components/bottomsheets/modal/TangemModalBottomSheetWithFooter.kt @@ -0,0 +1,311 @@ +package com.tangem.core.ui.components.bottomsheets.modal + +import android.content.res.Configuration +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.verticalScroll +import androidx.compose.material3.* +import androidx.compose.material3.SheetValue.Expanded +import androidx.compose.runtime.* +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Brush +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.graphics.vector.rememberVectorPainter +import androidx.compose.ui.platform.LocalConfiguration +import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.res.vectorResource +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import com.tangem.core.ui.R +import com.tangem.core.ui.components.* +import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig +import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfigContent +import com.tangem.core.ui.components.bottomsheets.internal.ModalBottomSheetWithBackHandling +import com.tangem.core.ui.components.bottomsheets.internal.collapse +import com.tangem.core.ui.res.LocalBottomSheetAlwaysVisible +import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.res.TangemThemePreview +import com.tangem.core.ui.utils.WindowInsetsZero + +/** + * Modal bottom sheet with [content], [footer] and optional [title]. + * + * Maximum height of sheet is 80% screen height + * + * [Show in Figma](https://www.figma.com/design/09KKG4ZVuFDZhj8WLv5rGJ/%F0%9F%9A%A7-App-experience?node-id=3254-61208&t=vixa7id6ggALcxfF-4) + */ +@Composable +inline fun TangemModalBottomSheetWithFooter( + config: TangemBottomSheetConfig, + containerColor: Color = TangemTheme.colors.background.primary, + skipPartiallyExpanded: Boolean = true, + noinline onBack: (() -> Unit)? = null, + crossinline title: @Composable BoxScope.(T) -> Unit = {}, + crossinline content: @Composable (T) -> Unit, + noinline footer: @Composable (BoxScope.(T) -> Unit), +) { + val isAlwaysVisible = LocalBottomSheetAlwaysVisible.current + + if (isAlwaysVisible) { + PreviewModalBottomSheetWithFooter( + config = config, + containerColor = containerColor, + title = title, + content = content, + footer = footer, + skipPartiallyExpanded = skipPartiallyExpanded, + ) + } else { + DefaultModalBottomSheetWithFooter( + config = config, + containerColor = containerColor, + title = title, + content = content, + footer = footer, + onBack = onBack, + skipPartiallyExpanded = skipPartiallyExpanded, + ) + } +} + +@Composable +@OptIn(ExperimentalMaterial3Api::class) +inline fun DefaultModalBottomSheetWithFooter( + config: TangemBottomSheetConfig, + containerColor: Color, + skipPartiallyExpanded: Boolean = true, + noinline onBack: (() -> Unit)? = null, + crossinline title: @Composable BoxScope.(T) -> Unit, + crossinline content: @Composable (T) -> Unit, + noinline footer: @Composable (BoxScope.(T) -> Unit), +) { + var isVisible by remember { mutableStateOf(value = config.isShown) } + val sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = skipPartiallyExpanded) + + if (isVisible && config.content is T) { + BasicModalBottomSheetWithFooter( + config = config, + sheetState = sheetState, + containerColor = containerColor, + title = title, + onBack = onBack, + content = content, + footer = footer, + ) + } + + LaunchedEffect(key1 = config.isShown) { + if (config.isShown) { + isVisible = true + } else { + sheetState.collapse { isVisible = false } + } + } +} + +@Composable +@OptIn(ExperimentalMaterial3Api::class) +inline fun PreviewModalBottomSheetWithFooter( + config: TangemBottomSheetConfig, + containerColor: Color, + skipPartiallyExpanded: Boolean = true, + crossinline title: @Composable BoxScope.(T) -> Unit, + crossinline content: @Composable (T) -> Unit, + noinline footer: @Composable BoxScope.(T) -> Unit, +) { + BasicModalBottomSheetWithFooter( + config = config, + sheetState = SheetState( + skipPartiallyExpanded = skipPartiallyExpanded, + initialValue = Expanded, + density = LocalDensity.current, + ), + onBack = null, + containerColor = containerColor, + title = title, + content = content, + footer = footer, + ) +} + +@Suppress("LongParameterList", "LongMethod") +@OptIn(ExperimentalMaterial3Api::class) +@Composable +inline fun BasicModalBottomSheetWithFooter( + config: TangemBottomSheetConfig, + sheetState: SheetState, + containerColor: Color, + noinline onBack: (() -> Unit)? = null, + crossinline title: @Composable BoxScope.(T) -> Unit, + crossinline content: @Composable (T) -> Unit, + noinline footer: @Composable (BoxScope.(T) -> Unit), + modifier: Modifier = Modifier, +) { + val model = config.content as? T ?: return + + val bsContent: @Composable ColumnScope.() -> Unit = { + val maxHeight = LocalConfiguration.current.screenHeightDp * MODAL_SHEET_MAX_HEIGHT + val initial = 0 + val scrollState = rememberScrollState(initial = initial) + + Column( + modifier = Modifier + .systemBarsPadding() + .padding(horizontal = 8.dp, vertical = 8.dp) + .clip(TangemTheme.shapes.roundedCornersLarge) + .background(containerColor) + .heightIn(max = maxHeight.dp) + .fillMaxWidth(), + ) { + Box(modifier = Modifier.fillMaxWidth()) { + title(model) + } + // Title bottom shadow(elevation) while content is scrolling + if (scrollState.value != initial) { + Box( + modifier = Modifier + .fillMaxWidth() + .height(2.dp) + .background( + brush = Brush.verticalGradient( + colors = listOf( + TangemTheme.colors.background.secondary.copy(alpha = 0.9F), + Color.Transparent, + ), + ), + ), + ) + } + Box(modifier = Modifier.weight(1f, fill = false)) { + val bottomBarHeight = with(LocalDensity.current) { + WindowInsets.systemBars.getBottom(density = this).toDp() + } + Column( + modifier = Modifier + .verticalScroll(state = scrollState) + .padding(bottom = TangemTheme.dimens.spacing76 + bottomBarHeight), + ) { + content(model) + } + if (scrollState.canScrollForward && scrollState.maxValue != Int.MAX_VALUE) { + BottomFade( + modifier = Modifier.align(Alignment.BottomCenter), + backgroundColor = TangemTheme.colors.background.primary, + ) + } + Box( + modifier = Modifier + .fillMaxWidth() + .align(Alignment.BottomCenter), + ) { + footer(model) + } + } + } + } + + if (onBack != null) { + ModalBottomSheetWithBackHandling( + modifier = modifier, + onDismissRequest = config.onDismissRequest, + sheetState = sheetState, + containerColor = Color.Transparent, + shape = TangemTheme.shapes.roundedCornersLarge, + contentWindowInsets = { WindowInsetsZero }, + onBack = onBack, + dragHandle = null, + content = bsContent, + ) + } else { + ModalBottomSheet( + modifier = modifier, + onDismissRequest = config.onDismissRequest, + sheetState = sheetState, + containerColor = Color.Transparent, + shape = TangemTheme.shapes.roundedCornersLarge, + contentWindowInsets = { WindowInsetsZero }, + dragHandle = null, + content = bsContent, + ) + } +} + +// region Preview +@Preview(showBackground = true, widthDp = 360, heightDp = 800) +@Preview(showBackground = true, widthDp = 360, heightDp = 800, uiMode = Configuration.UI_MODE_NIGHT_YES) +@Composable +private fun TangemModalBottomSheetWithFooter_Preview() { + TangemThemePreview { + TangemModalBottomSheetWithFooter( + config = TangemBottomSheetConfig( + isShown = true, + onDismissRequest = {}, + content = TangemBottomSheetConfigContent.Empty, + ), + title = { TangemModalBottomSheetTitle(endIconRes = R.drawable.ic_close_24, onEndClick = {}) }, + content = { + Column( + modifier = Modifier.padding(start = 16.dp, end = 16.dp, bottom = 16.dp), + horizontalAlignment = Alignment.CenterHorizontally, + ) { + Icon( + modifier = Modifier + .size(56.dp) + .clip(RoundedCornerShape(100)) + .background(TangemTheme.colors.icon.informative.copy(alpha = 0.1f)) + .padding(12.dp), + painter = rememberVectorPainter( + ImageVector.vectorResource(R.drawable.ic_alert_24), + ), + tint = TangemTheme.colors.icon.informative, + contentDescription = null, + ) + SpacerH24() + Text( + text = "Unsuported networks", + style = TangemTheme.typography.h3, + color = TangemTheme.colors.text.primary1, + textAlign = TextAlign.Center, + ) + SpacerH8() + Text( + text = "Tangem does not currently support a required network by React App.", + style = TangemTheme.typography.body2, + color = TangemTheme.colors.text.secondary, + textAlign = TextAlign.Center, + ) + SpacerH(48.dp) + Text( + text = "Long text to show scrollable content and bottom fade." + + "\nLorem ipsum dolor sit amet, consectetur adipiscing elit. In imperdiet metus non leo " + + "ultricies pulvinar. Pellentesque sed condimentum odio. Sed venenatis ac felis non " + + "consequat. Nunc erat dolor, maximus nec mattis a, tempus at eros. Duis sit amet neque " + + "dui. Donec consectetur nisl id dui convallis, in posuere dolor eleifend. Pellentesque " + + "habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. " + + "Pellentesque consequat scelerisque justo quis tristique. Mauris laoreet venenatis " + + "pharetra. Morbi sed faucibus leo. Praesent elementum pretium posuere. Morbi et felis a " + + "turpis pellentesque rhoncus.", + style = TangemTheme.typography.body1, + color = TangemTheme.colors.text.secondary, + textAlign = TextAlign.Center, + ) + } + }, + footer = { + PrimaryButton( + modifier = Modifier + .fillMaxWidth() + .padding(16.dp), + text = "Go it", + onClick = {}, + ) + }, + ) + } +} \ No newline at end of file From c4603c4cbd856ca465af6011ef4e7dc0e109c0ea Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 19 May 2025 14:10:44 +0500 Subject: [PATCH 051/165] Updated on 2026-08-14 --- app/build.gradle.kts | 1 + .../FirebasePushNotificationsTokenProvider.kt | 3 +- .../di/domain/NotificationsDomainModule.kt | 2 - .../tap/di/domain/WalletsDomainModule.kt | 20 +++++ .../tangem/tap/features/main/MainViewModel.kt | 33 +++++++- .../api/tangemTech/TangemTechApi.kt | 2 +- .../DefaultNotificationsRepositoryTest.kt | 2 +- .../notifications/SendPushTokenUseCase.kt | 6 +- .../notifications/SendPushTokenUseCaseTest.kt | 21 +---- ...ssociateWalletsWithApplicationIdUseCase.kt | 11 ++- .../usecase/GetSavedWalletChangesUseCase.kt | 27 +++++++ .../usecase/UpdateRemoteWalletsInfoUseCase.kt | 6 +- .../GetSavedWalletChangesUseCaseTest.kt | 76 +++++++++++++++++++ 13 files changed, 171 insertions(+), 39 deletions(-) create mode 100644 domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/GetSavedWalletChangesUseCase.kt create mode 100644 domain/wallets/src/test/java/com/tangem/domain/wallets/usecase/GetSavedWalletChangesUseCaseTest.kt diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 37e399bc70..0c9d9ac315 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -111,6 +111,7 @@ dependencies { implementation(projects.domain.networks) implementation(projects.domain.quotes) implementation(projects.domain.notifications) + implementation(projects.domain.notifications.models) implementation(projects.domain.notifications.toggles) implementation(projects.common) diff --git a/app/src/main/java/com/tangem/tap/data/FirebasePushNotificationsTokenProvider.kt b/app/src/main/java/com/tangem/tap/data/FirebasePushNotificationsTokenProvider.kt index 02a9b8be72..6167cea5a0 100644 --- a/app/src/main/java/com/tangem/tap/data/FirebasePushNotificationsTokenProvider.kt +++ b/app/src/main/java/com/tangem/tap/data/FirebasePushNotificationsTokenProvider.kt @@ -3,8 +3,9 @@ package com.tangem.tap.data import com.google.firebase.messaging.FirebaseMessaging import com.tangem.utils.notifications.PushNotificationsTokenProvider import kotlinx.coroutines.tasks.await +import javax.inject.Inject -internal class FirebasePushNotificationsTokenProvider : PushNotificationsTokenProvider { +internal class FirebasePushNotificationsTokenProvider @Inject constructor() : PushNotificationsTokenProvider { override suspend fun getToken(): String { return FirebaseMessaging.getInstance().token.await() } diff --git a/app/src/main/java/com/tangem/tap/di/domain/NotificationsDomainModule.kt b/app/src/main/java/com/tangem/tap/di/domain/NotificationsDomainModule.kt index 706ef792e1..14df43efb0 100644 --- a/app/src/main/java/com/tangem/tap/di/domain/NotificationsDomainModule.kt +++ b/app/src/main/java/com/tangem/tap/di/domain/NotificationsDomainModule.kt @@ -31,12 +31,10 @@ internal object NotificationsDomainModule { @Singleton fun providesSendPushTokenUseCase( notificationsRepository: NotificationsRepository, - getApplicationIdUseCase: GetApplicationIdUseCase, pushNotificationsTokenProvider: PushNotificationsTokenProvider, ): SendPushTokenUseCase { return SendPushTokenUseCase( notificationsRepository = notificationsRepository, - getApplicationIdUseCase = getApplicationIdUseCase, pushNotificationsTokenProvider = pushNotificationsTokenProvider, ) } diff --git a/app/src/main/java/com/tangem/tap/di/domain/WalletsDomainModule.kt b/app/src/main/java/com/tangem/tap/di/domain/WalletsDomainModule.kt index b153b0e0de..1fbf5cb42e 100644 --- a/app/src/main/java/com/tangem/tap/di/domain/WalletsDomainModule.kt +++ b/app/src/main/java/com/tangem/tap/di/domain/WalletsDomainModule.kt @@ -225,4 +225,24 @@ internal object WalletsDomainModule { userWalletsSyncDelegate = userWalletsSyncDelegate, ) } + + @Provides + @Singleton + fun providesGetSavedWalletChangesIdUseCase( + userWalletsListManager: UserWalletsListManager, + ): GetSavedWalletChangesUseCase { + return GetSavedWalletChangesUseCase( + userWalletsListManager = userWalletsListManager, + ) + } + + @Provides + @Singleton + fun providesAssociateWalletsWithApplicationIdUseCase( + walletsRepository: WalletsRepository, + ): AssociateWalletsWithApplicationIdUseCase { + return AssociateWalletsWithApplicationIdUseCase( + walletsRepository = walletsRepository, + ) + } } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/main/MainViewModel.kt b/app/src/main/java/com/tangem/tap/features/main/MainViewModel.kt index d2b77135d2..7dee2ef45c 100644 --- a/app/src/main/java/com/tangem/tap/features/main/MainViewModel.kt +++ b/app/src/main/java/com/tangem/tap/features/main/MainViewModel.kt @@ -11,7 +11,6 @@ import com.tangem.core.analytics.models.event.TechAnalyticsEvent import com.tangem.core.decompose.di.GlobalUiMessageSender import com.tangem.core.decompose.ui.UiMessageSender import com.tangem.core.deeplink.DeepLinksRegistry -import com.tangem.core.ui.BuildConfig import com.tangem.core.ui.R import com.tangem.core.ui.coil.ImagePreloader import com.tangem.core.ui.extensions.resourceReference @@ -24,6 +23,10 @@ import com.tangem.domain.balancehiding.BalanceHidingSettings import com.tangem.domain.balancehiding.GetBalanceHidingSettingsUseCase import com.tangem.domain.balancehiding.ListenToFlipsUseCase import com.tangem.domain.balancehiding.UpdateBalanceHidingSettingsUseCase +import com.tangem.domain.notifications.GetApplicationIdUseCase +import com.tangem.domain.notifications.SendPushTokenUseCase +import com.tangem.domain.notifications.models.ApplicationId +import com.tangem.domain.notifications.toggles.NotificationsFeatureToggles import com.tangem.domain.onboarding.repository.OnboardingRepository import com.tangem.domain.onramp.FetchHotCryptoUseCase import com.tangem.domain.promo.GetStoryContentUseCase @@ -33,6 +36,9 @@ import com.tangem.domain.settings.IncrementAppLaunchCounterUseCase import com.tangem.domain.settings.usercountry.FetchUserCountryUseCase import com.tangem.domain.staking.FetchStakingTokensUseCase import com.tangem.domain.wallets.legacy.UserWalletsListManager +import com.tangem.domain.wallets.usecase.AssociateWalletsWithApplicationIdUseCase +import com.tangem.domain.wallets.usecase.GetSavedWalletChangesUseCase +import com.tangem.domain.wallets.usecase.UpdateRemoteWalletsInfoUseCase import com.tangem.feature.swap.analytics.StoriesEvents import com.tangem.features.onramp.deeplink.OnrampDeepLink import com.tangem.tap.common.extensions.setContext @@ -40,6 +46,7 @@ import com.tangem.tap.common.redux.global.GlobalAction import com.tangem.tap.features.onboarding.products.wallet.redux.BackupDialog import com.tangem.tap.store import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import com.tangem.wallet.BuildConfig import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.* @@ -71,6 +78,12 @@ internal class MainViewModel @Inject constructor( private val onboardingRepository: OnboardingRepository, private val deepLinksRegistry: DeepLinksRegistry, private val onrampDeepLinkFactory: OnrampDeepLink.Factory, + private val notificationsToggles: NotificationsFeatureToggles, + private val getApplicationIdUseCase: GetApplicationIdUseCase, + private val subscribeOnWalletsUseCase: GetSavedWalletChangesUseCase, + private val associateWalletsWithApplicationIdUseCase: AssociateWalletsWithApplicationIdUseCase, + private val updateRemoteWalletsInfoUseCase: UpdateRemoteWalletsInfoUseCase, + private val sendPushTokenUseCase: SendPushTokenUseCase, private val apiConfigsManager: ApiConfigsManager, routingFeatureToggle: RoutingFeatureToggle, getBalanceHidingSettingsUseCase: GetBalanceHidingSettingsUseCase, @@ -96,6 +109,8 @@ internal class MainViewModel @Inject constructor( launch { fetchAppCurrenciesUseCase() } launch { fetchStakingTokens() } + + launch { initPushNotifications() } } viewModelScope.launch { incrementAppLaunchCounterUseCase() } @@ -370,4 +385,20 @@ internal class MainViewModel @Inject constructor( private fun initializeDeepLinks() { deepLinksRegistry.register(onrampDeepLinkFactory.create(viewModelScope)) } + + private suspend fun initPushNotifications() { + if (notificationsToggles.isNotificationsEnabled) { + getApplicationIdUseCase().onRight { applicationId -> + sendPushTokenUseCase(applicationId = applicationId) + associateWalletsWithApplicationId(applicationId = applicationId) + updateRemoteWalletsInfoUseCase(applicationId = applicationId) + }.onLeft { Timber.e(it.toString()) } + } + } + + private fun associateWalletsWithApplicationId(applicationId: ApplicationId) { + subscribeOnWalletsUseCase().onEach { wallets -> + associateWalletsWithApplicationIdUseCase(applicationId, wallets) + }.launchIn(viewModelScope) + } } \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/TangemTechApi.kt b/core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/TangemTechApi.kt index d4db8f1fa1..81b01211dc 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/TangemTechApi.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/TangemTechApi.kt @@ -154,7 +154,7 @@ interface TangemTechApi { suspend fun updatePushTokenForApplicationId( @Path("application_id") applicationId: String, @Body body: NotificationApplicationCreateBody, - ): ApiResponse + ): ApiResponse @PATCH("user-wallets/wallets/{wallet_id}/notify") suspend fun setNotificationsEnabled(@Path("wallet_id") walletId: String, @Body body: WalletBody): ApiResponse diff --git a/data/notifications/src/test/java/com/tangem/data/notifications/DefaultNotificationsRepositoryTest.kt b/data/notifications/src/test/java/com/tangem/data/notifications/DefaultNotificationsRepositoryTest.kt index 691dff0714..46c37951b1 100644 --- a/data/notifications/src/test/java/com/tangem/data/notifications/DefaultNotificationsRepositoryTest.kt +++ b/data/notifications/src/test/java/com/tangem/data/notifications/DefaultNotificationsRepositoryTest.kt @@ -121,7 +121,7 @@ class DefaultNotificationsRepositoryTest { timezone = null, ), ) - } returns ApiResponse.Success(appId.value) + } returns ApiResponse.Success(Unit) // WHEN repository.sendPushToken(appId, pushToken) diff --git a/domain/notifications/src/main/java/com/tangem/domain/notifications/SendPushTokenUseCase.kt b/domain/notifications/src/main/java/com/tangem/domain/notifications/SendPushTokenUseCase.kt index 45617cf6e4..63247b9a7d 100644 --- a/domain/notifications/src/main/java/com/tangem/domain/notifications/SendPushTokenUseCase.kt +++ b/domain/notifications/src/main/java/com/tangem/domain/notifications/SendPushTokenUseCase.kt @@ -1,18 +1,16 @@ package com.tangem.domain.notifications import arrow.core.Either +import com.tangem.domain.notifications.models.ApplicationId import com.tangem.domain.notifications.repository.NotificationsRepository import com.tangem.utils.notifications.PushNotificationsTokenProvider class SendPushTokenUseCase( private val notificationsRepository: NotificationsRepository, - private val getApplicationIdUseCase: GetApplicationIdUseCase, private val pushNotificationsTokenProvider: PushNotificationsTokenProvider, ) { - suspend operator fun invoke(): Either = Either.catch { - val applicationId = getApplicationIdUseCase().getOrNull() - ?: error("Application ID not found") + suspend operator fun invoke(applicationId: ApplicationId): Either = Either.catch { val token = pushNotificationsTokenProvider.getToken() notificationsRepository.sendPushToken(applicationId, token) } diff --git a/domain/notifications/src/test/java/com/tangem/domain/notifications/SendPushTokenUseCaseTest.kt b/domain/notifications/src/test/java/com/tangem/domain/notifications/SendPushTokenUseCaseTest.kt index f079a021d1..18d716fdf0 100644 --- a/domain/notifications/src/test/java/com/tangem/domain/notifications/SendPushTokenUseCaseTest.kt +++ b/domain/notifications/src/test/java/com/tangem/domain/notifications/SendPushTokenUseCaseTest.kt @@ -15,18 +15,15 @@ import org.junit.Test class SendPushTokenUseCaseTest { private lateinit var notificationsRepository: NotificationsRepository - private lateinit var getApplicationIdUseCase: GetApplicationIdUseCase private lateinit var pushNotificationsTokenProvider: PushNotificationsTokenProvider private lateinit var sendPushTokenUseCase: SendPushTokenUseCase @Before fun setup() { notificationsRepository = mockk() - getApplicationIdUseCase = mockk() pushNotificationsTokenProvider = mockk() sendPushTokenUseCase = SendPushTokenUseCase( notificationsRepository = notificationsRepository, - getApplicationIdUseCase = getApplicationIdUseCase, pushNotificationsTokenProvider = pushNotificationsTokenProvider, ) } @@ -36,42 +33,28 @@ class SendPushTokenUseCaseTest { // GIVEN val applicationId = ApplicationId("test-app-id") val token = "test-token" - coEvery { getApplicationIdUseCase() } returns Either.Right(applicationId) coEvery { pushNotificationsTokenProvider.getToken() } returns token coEvery { notificationsRepository.sendPushToken(applicationId, token) } returns Unit // WHEN - val result = sendPushTokenUseCase() + val result = sendPushTokenUseCase(applicationId) // THEN assertThat(result).isEqualTo(Either.Right(Unit)) coVerify(exactly = 1) { notificationsRepository.sendPushToken(applicationId, token) } } - @Test - fun `GIVEN application ID is not found WHEN invoke THEN throws error`() = runTest { - // GIVEN - coEvery { getApplicationIdUseCase() } returns Either.Left(Throwable("Application ID not found")) - - // WHEN & THEN - val result = sendPushTokenUseCase() - assertThat(result.isLeft()).isTrue() - assertThat(result.fold({ it.message }, { null })).isEqualTo("Application ID not found") - coVerify(exactly = 0) { notificationsRepository.sendPushToken(any(), any()) } - } - @Test fun `GIVEN repository throws error WHEN invoke THEN returns error`() = runTest { // GIVEN val applicationId = ApplicationId("test-app-id") val token = "test-token" val expectedError = RuntimeException("Network error") - coEvery { getApplicationIdUseCase() } returns Either.Right(applicationId) coEvery { pushNotificationsTokenProvider.getToken() } returns token coEvery { notificationsRepository.sendPushToken(applicationId, token) } throws expectedError // WHEN - val result = sendPushTokenUseCase() + val result = sendPushTokenUseCase(applicationId) // THEN assertThat(result).isEqualTo(Either.Left(expectedError)) diff --git a/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/AssociateWalletsWithApplicationIdUseCase.kt b/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/AssociateWalletsWithApplicationIdUseCase.kt index e67bf5b744..664a03af37 100644 --- a/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/AssociateWalletsWithApplicationIdUseCase.kt +++ b/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/AssociateWalletsWithApplicationIdUseCase.kt @@ -2,16 +2,15 @@ package com.tangem.domain.wallets.usecase import arrow.core.Either import com.tangem.domain.notifications.models.ApplicationId -import com.tangem.domain.wallets.legacy.UserWalletsListManager +import com.tangem.domain.wallets.models.UserWallet import com.tangem.domain.wallets.repository.WalletsRepository class AssociateWalletsWithApplicationIdUseCase( - private val userWalletsListManager: UserWalletsListManager, private val walletsRepository: WalletsRepository, ) { - suspend operator fun invoke(applicationId: ApplicationId): Either = Either.catch { - val wallets = userWalletsListManager.userWalletsSync - walletsRepository.associateWallets(applicationId.value, wallets) - } + suspend operator fun invoke(applicationId: ApplicationId, wallets: List): Either = + Either.catch { + walletsRepository.associateWallets(applicationId.value, wallets) + } } \ No newline at end of file diff --git a/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/GetSavedWalletChangesUseCase.kt b/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/GetSavedWalletChangesUseCase.kt new file mode 100644 index 0000000000..afcfe40a89 --- /dev/null +++ b/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/GetSavedWalletChangesUseCase.kt @@ -0,0 +1,27 @@ +package com.tangem.domain.wallets.usecase + +import com.tangem.domain.wallets.legacy.UserWalletsListManager +import com.tangem.domain.wallets.legacy.asLockable +import com.tangem.domain.wallets.legacy.isLockedSync +import com.tangem.domain.wallets.models.UserWallet +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.filter + +class GetSavedWalletChangesUseCase( + private val userWalletsListManager: UserWalletsListManager, +) { + + operator fun invoke(): Flow> { + return userWalletsListManager.userWallets + .filter { + userWalletsListManager.asLockable() ?: return@filter false + return@filter if (userWalletsListManager.isLockedSync.not()) { + it.isNotEmpty() + } else { + false + } + } + .distinctUntilChanged() + } +} \ No newline at end of file diff --git a/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/UpdateRemoteWalletsInfoUseCase.kt b/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/UpdateRemoteWalletsInfoUseCase.kt index 5305089cd7..8ae0a158b0 100644 --- a/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/UpdateRemoteWalletsInfoUseCase.kt +++ b/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/UpdateRemoteWalletsInfoUseCase.kt @@ -1,10 +1,8 @@ package com.tangem.domain.wallets.usecase import arrow.core.Either -import arrow.core.raise.either import com.tangem.domain.notifications.models.ApplicationId import com.tangem.domain.wallets.delegate.UserWalletsSyncDelegate -import com.tangem.domain.wallets.models.UpdateWalletError import com.tangem.domain.wallets.repository.WalletsRepository class UpdateRemoteWalletsInfoUseCase( @@ -12,8 +10,8 @@ class UpdateRemoteWalletsInfoUseCase( private val userWalletsSyncDelegate: UserWalletsSyncDelegate, ) { - suspend operator fun invoke(applicationId: ApplicationId): Either = either { + suspend operator fun invoke(applicationId: ApplicationId): Either = Either.catch { val walletsInfo = walletsRepository.getWalletsInfo(applicationId.value) - userWalletsSyncDelegate.syncWallets(walletsInfo).bind() + userWalletsSyncDelegate.syncWallets(walletsInfo) } } \ No newline at end of file diff --git a/domain/wallets/src/test/java/com/tangem/domain/wallets/usecase/GetSavedWalletChangesUseCaseTest.kt b/domain/wallets/src/test/java/com/tangem/domain/wallets/usecase/GetSavedWalletChangesUseCaseTest.kt new file mode 100644 index 0000000000..25e0cb17db --- /dev/null +++ b/domain/wallets/src/test/java/com/tangem/domain/wallets/usecase/GetSavedWalletChangesUseCaseTest.kt @@ -0,0 +1,76 @@ +package com.tangem.domain.wallets.usecase + +import com.google.common.truth.Truth.assertThat +import com.tangem.domain.wallets.legacy.UserWalletsListManager +import com.tangem.domain.wallets.legacy.asLockable +import com.tangem.domain.wallets.legacy.isLockedSync +import com.tangem.domain.wallets.models.UserWallet +import io.mockk.every +import io.mockk.mockk +import io.mockk.mockkStatic +import kotlinx.coroutines.flow.firstOrNull +import kotlinx.coroutines.flow.flowOf +import kotlinx.coroutines.test.runTest +import org.junit.Before +import org.junit.Test + +class GetSavedWalletChangesUseCaseTest { + + private lateinit var useCase: GetSavedWalletChangesUseCase + private lateinit var userWalletsListManager: UserWalletsListManager + + @Before + fun setup() { + userWalletsListManager = mockk() + useCase = GetSavedWalletChangesUseCase(userWalletsListManager) + mockkStatic("com.tangem.domain.wallets.legacy.UserWalletsListManagerExtensionsKt") + } + + @Test + fun `GIVEN manager is not lockable WHEN invoke THEN return empty list`() = runTest { + // GIVEN + every { userWalletsListManager.isLockable } returns false + every { userWalletsListManager.userWallets } returns flowOf(emptyList()) + every { userWalletsListManager.isLockedSync } returns false + every { userWalletsListManager.asLockable() } returns null + + // WHEN + val result = useCase().firstOrNull() + + // THEN + assertThat(result).isNull() + } + + @Test + fun `GIVEN manager is locked WHEN invoke THEN return empty list`() = runTest { + // GIVEN + val mockLockable = mockk() + every { userWalletsListManager.isLockable } returns true + every { userWalletsListManager.userWallets } returns flowOf(emptyList()) + every { userWalletsListManager.isLockedSync } returns true + every { userWalletsListManager.asLockable() } returns mockLockable + + // WHEN + val result = useCase().firstOrNull() + + // THEN + assertThat(result).isNull() + } + + @Test + fun `GIVEN manager is not locked and has wallets WHEN invoke THEN return wallets list`() = runTest { + // GIVEN + val mockLockable = mockk() + val wallets = listOf(mockk(), mockk()) + every { userWalletsListManager.isLockable } returns true + every { userWalletsListManager.userWallets } returns flowOf(wallets) + every { userWalletsListManager.isLockedSync } returns false + every { userWalletsListManager.asLockable() } returns mockLockable + + // WHEN + val result = useCase().firstOrNull() + + // THEN + assertThat(result).isEqualTo(wallets) + } +} \ No newline at end of file From c613890b9a7646cd63ea75ef490201b11de674c8 Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 19 May 2025 14:26:21 +0500 Subject: [PATCH 052/165] Updated on 2026-08-14 --- .../main/java/com/tangem/tap/MainActivity.kt | 35 +++-- .../component/impl/DefaultRoutingComponent.kt | 5 + .../tap/routing/utils/DeepLinkFactory.kt | 123 ++++++++++++++++++ .../tangem/common/routing/DeepLinkRoute.kt | 15 +++ .../onramp/deeplink/OnrampDeepLink.kt | 8 ++ .../deeplink/DefaultOnrampDeepLinkHandler.kt | 58 +++++++++ .../deeplink/di/OnrampDeeplinkModule.kt | 6 +- .../model/OnrampSuccessComponentModel.kt | 4 +- 8 files changed, 241 insertions(+), 13 deletions(-) create mode 100644 app/src/main/java/com/tangem/tap/routing/utils/DeepLinkFactory.kt create mode 100644 common/routing/src/main/kotlin/com/tangem/common/routing/DeepLinkRoute.kt create mode 100644 features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/deeplink/DefaultOnrampDeepLinkHandler.kt diff --git a/app/src/main/java/com/tangem/tap/MainActivity.kt b/app/src/main/java/com/tangem/tap/MainActivity.kt index b3ad882b44..dfd119816f 100644 --- a/app/src/main/java/com/tangem/tap/MainActivity.kt +++ b/app/src/main/java/com/tangem/tap/MainActivity.kt @@ -19,6 +19,7 @@ import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.toArgb +import androidx.core.net.toUri import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen import androidx.lifecycle.Lifecycle import androidx.lifecycle.flowWithLifecycle @@ -30,6 +31,7 @@ import com.tangem.common.routing.entity.SerializableIntent import com.tangem.core.analytics.api.AnalyticsEventHandler import com.tangem.core.decompose.context.AppComponentContext import com.tangem.core.decompose.di.RootAppComponentContext +import com.tangem.core.deeplink.DEEPLINK_KEY import com.tangem.core.deeplink.DeepLinksRegistry import com.tangem.core.navigation.email.EmailSender import com.tangem.core.ui.UiDependencies @@ -68,6 +70,7 @@ import com.tangem.tap.proxy.AppStateHolder import com.tangem.tap.proxy.redux.DaggerGraphAction import com.tangem.tap.routing.component.RoutingComponent import com.tangem.tap.routing.configurator.AppRouterConfig +import com.tangem.tap.routing.utils.DeepLinkFactory import com.tangem.utils.coroutines.CoroutineDispatcherProvider import com.tangem.utils.coroutines.FeatureCoroutineExceptionHandler import dagger.hilt.android.AndroidEntryPoint @@ -175,6 +178,9 @@ class MainActivity : AppCompatActivity(), ActivityResultCallbackHolder { @Inject internal lateinit var routingFeatureToggle: RoutingFeatureToggle + @Inject + internal lateinit var deeplinkFactory: DeepLinkFactory + internal val viewModel: MainViewModel by viewModels() private lateinit var appThemeModeFlow: SharedFlow @@ -227,13 +233,9 @@ class MainActivity : AppCompatActivity(), ActivityResultCallbackHolder { sendStakingUnsubmittedHashes() checkGoogleServicesAvailability() - if (intent != null && savedInstanceState == null) { + if (routingFeatureToggle.isDeepLinkNavigationEnabled.not() && intent != null && savedInstanceState == null) { // handle intent only on start, not on recreate - if (routingFeatureToggle.isDeepLinkNavigationEnabled) { - // todo [REDACTED_TASK_KEY] - } else { - deepLinksRegistry.launch(intent) - } + handleDeepLink(intent) } lifecycle.addObserver(WindowObscurationObserver) @@ -382,11 +384,7 @@ class MainActivity : AppCompatActivity(), ActivityResultCallbackHolder { } if (intent != null) { - if (routingFeatureToggle.isDeepLinkNavigationEnabled) { - // todo [REDACTED_TASK_KEY] - } else { - deepLinksRegistry.launch(intent) - } + handleDeepLink(intent) } } @@ -467,9 +465,24 @@ class MainActivity : AppCompatActivity(), ActivityResultCallbackHolder { } } + if (routingFeatureToggle.isDeepLinkNavigationEnabled && intent != null) { + handleDeepLink(intent) + } + viewModel.checkForUnfinishedBackup() } + private fun handleDeepLink(intent: Intent) { + if (routingFeatureToggle.isDeepLinkNavigationEnabled) { + val deepLinkExtras = intent.getStringExtra(DEEPLINK_KEY)?.toUri() + val receivedDeepLink = intent.data ?: deepLinkExtras ?: return + + deeplinkFactory.handleDeeplink(deeplinkUri = receivedDeepLink, coroutineScope = lifecycleScope) + } else { + deepLinksRegistry.launch(intent) + } + } + private fun observePolkadotAccountHealthCheck() { lifecycleScope.launch { getPolkadotCheckHasResetUseCase() diff --git a/app/src/main/java/com/tangem/tap/routing/component/impl/DefaultRoutingComponent.kt b/app/src/main/java/com/tangem/tap/routing/component/impl/DefaultRoutingComponent.kt index 92ac286e88..64d10c0193 100644 --- a/app/src/main/java/com/tangem/tap/routing/component/impl/DefaultRoutingComponent.kt +++ b/app/src/main/java/com/tangem/tap/routing/component/impl/DefaultRoutingComponent.kt @@ -22,6 +22,7 @@ import com.tangem.tap.routing.component.RoutingComponent import com.tangem.tap.routing.component.RoutingComponent.Child import com.tangem.tap.routing.configurator.AppRouterConfig import com.tangem.tap.routing.utils.ChildFactory +import com.tangem.tap.routing.utils.DeepLinkFactory import dagger.assisted.Assisted import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject @@ -34,6 +35,7 @@ internal class DefaultRoutingComponent @AssistedInject constructor( private val appRouterConfig: AppRouterConfig, private val uiDependencies: UiDependencies, private val wcRoutingComponentFactory: WcRoutingComponent.Factory, + private val deeplinkFactory: DeepLinkFactory, ) : RoutingComponent, AppComponentContext by context, SnackbarHandler { @@ -60,7 +62,10 @@ internal class DefaultRoutingComponent @AssistedInject constructor( stack.subscribe(lifecycle) { stack -> val stackItems = stack.items.map { it.configuration } + wcRoutingComponent.onAppRouteChange(stack.active.configuration) + deeplinkFactory.checkRoutingReadiness(stack.active.configuration) + if (appRouterConfig.stack != stackItems) { appRouterConfig.stack = stackItems } diff --git a/app/src/main/java/com/tangem/tap/routing/utils/DeepLinkFactory.kt b/app/src/main/java/com/tangem/tap/routing/utils/DeepLinkFactory.kt new file mode 100644 index 0000000000..ea0040261e --- /dev/null +++ b/app/src/main/java/com/tangem/tap/routing/utils/DeepLinkFactory.kt @@ -0,0 +1,123 @@ +package com.tangem.tap.routing.utils + +import android.net.Uri +import com.tangem.common.routing.AppRoute +import com.tangem.common.routing.DeepLinkRoute +import com.tangem.common.routing.DeepLinkScheme +import com.tangem.features.onramp.deeplink.OnrampDeepLinkHandler +import com.tangem.utils.coroutines.JobHolder +import com.tangem.utils.coroutines.saveIn +import dagger.hilt.android.scopes.ActivityScoped +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.transformLatest +import timber.log.Timber +import javax.inject.Inject + +@ActivityScoped +internal class DeepLinkFactory @Inject constructor( + private val onrampDeepLink: OnrampDeepLinkHandler.Factory, +) { + private val permittedAppRoute = MutableStateFlow(false) + + private var lastDeepLink: Uri? = null + private val deepLinkHandlerJobHolder = JobHolder() + + @OptIn(ExperimentalCoroutinesApi::class) + fun handleDeeplink(deeplinkUri: Uri, coroutineScope: CoroutineScope) { + lastDeepLink = deeplinkUri + + Timber.i( + """ + Received deep link intent + |- Received URI: $deeplinkUri + """.trimIndent(), + ) + permittedAppRoute + .transformLatest { isPermitted -> + if (isPermitted) { + lastDeepLink?.let { + launchDeepLink(it, coroutineScope) + } + lastDeepLink = null + } + } + .launchIn(coroutineScope) + .saveIn(deepLinkHandlerJobHolder) + } + + /** + * Check if app is ready to handle deeplink + */ + fun checkRoutingReadiness(appRoute: AppRoute) { + permittedAppRoute.value = when (appRoute) { + AppRoute.Initial, + AppRoute.Home, + is AppRoute.Welcome, + is AppRoute.Disclaimer, + is AppRoute.Stories, + is AppRoute.Onboarding, + -> false + else -> true + } + } + + private fun launchDeepLink(deeplinkUri: Uri, coroutineScope: CoroutineScope) { + when (deeplinkUri.scheme) { + DeepLinkScheme.Tangem.scheme -> handleTangemDeepLinks(deeplinkUri, coroutineScope) + else -> { + Timber.i( + """ + No match found for deep link + |- Received URI: $deeplinkUri + """.trimIndent(), + ) + } + } + } + + private fun handleTangemDeepLinks(deeplinkUri: Uri, coroutineScope: CoroutineScope) { + val params = getParams(deeplinkUri) + when (deeplinkUri.host) { + DeepLinkRoute.Onramp.host -> onrampDeepLink.create(coroutineScope, params) + else -> { + Timber.i( + """ + No match found for deep link + |- Received URI: $deeplinkUri + |- With params: $params + """.trimIndent(), + ) + } + } + } + + private fun getParams(uri: Uri): Map { + val params = mutableMapOf() + + uri.queryParameterNames.forEach { paramName -> + val paramValue = uri.getQueryParameter(paramName) + + if (paramName.validate() && paramValue?.validate() == true) { + params[paramName] = paramValue + } + } + + return params + } + + /** + * Check for malicious symbol in uri part + */ + private fun String.validate(): Boolean { + val regex = DEEPLINK_VALIDATION_REGEX.toRegex() + + return !regex.containsMatchIn(this) + } + + private companion object { + const val DEEPLINK_VALIDATION_REGEX = "['\";<>()+\\\\]" + } +} \ No newline at end of file diff --git a/common/routing/src/main/kotlin/com/tangem/common/routing/DeepLinkRoute.kt b/common/routing/src/main/kotlin/com/tangem/common/routing/DeepLinkRoute.kt new file mode 100644 index 0000000000..85bf976648 --- /dev/null +++ b/common/routing/src/main/kotlin/com/tangem/common/routing/DeepLinkRoute.kt @@ -0,0 +1,15 @@ +package com.tangem.common.routing + +sealed class DeepLinkRoute { + + abstract val host: String + + data object Onramp : DeepLinkRoute() { + override val host: String = "onramp" + } +} + +enum class DeepLinkScheme(val scheme: String) { + Tangem(scheme = "tangem"), + WalletConnect(scheme = "wc"), +} \ No newline at end of file diff --git a/features/onramp/api/src/main/kotlin/com/tangem/features/onramp/deeplink/OnrampDeepLink.kt b/features/onramp/api/src/main/kotlin/com/tangem/features/onramp/deeplink/OnrampDeepLink.kt index 1b2ae8c801..e590b31392 100644 --- a/features/onramp/api/src/main/kotlin/com/tangem/features/onramp/deeplink/OnrampDeepLink.kt +++ b/features/onramp/api/src/main/kotlin/com/tangem/features/onramp/deeplink/OnrampDeepLink.kt @@ -3,10 +3,18 @@ package com.tangem.features.onramp.deeplink import com.tangem.core.deeplink.DeepLink import kotlinx.coroutines.CoroutineScope +@Deprecated("Use OnrampDeepLinkHandler") abstract class OnrampDeepLink : DeepLink() { override val uri = "tangem://onramp" interface Factory { fun create(coroutineScope: CoroutineScope): OnrampDeepLink } +} + +interface OnrampDeepLinkHandler { + + interface Factory { + fun create(coroutineScope: CoroutineScope, params: Map): OnrampDeepLinkHandler + } } \ No newline at end of file diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/deeplink/DefaultOnrampDeepLinkHandler.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/deeplink/DefaultOnrampDeepLinkHandler.kt new file mode 100644 index 0000000000..c1ef286e30 --- /dev/null +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/deeplink/DefaultOnrampDeepLinkHandler.kt @@ -0,0 +1,58 @@ +package com.tangem.features.onramp.deeplink + +import com.tangem.common.routing.AppRoute +import com.tangem.common.routing.AppRouter +import com.tangem.features.onramp.success.OnrampSuccessScreenTrigger +import dagger.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.launch +import timber.log.Timber + +class DefaultOnrampDeepLinkHandler @AssistedInject constructor( + appRouter: AppRouter, + private val onrampSuccessScreenTrigger: OnrampSuccessScreenTrigger, + @Assisted private val scope: CoroutineScope, + @Assisted params: Map, +) : OnrampDeepLinkHandler { + + init { + val txId = params[TX_ID_KEY] + val result = OnrampRedirectResult.getResult(params[RESULT_KEY]) + + when { + !txId.isNullOrEmpty() -> { + // finish current onramp flow and show onramp success screen + val replaceOnrampScreens = appRouter.stack + .filterNot { it is AppRoute.Onramp || it is AppRoute.OnrampSuccess } + .toMutableList() + AppRoute.OnrampSuccess(txId) + + appRouter.replaceAll(*replaceOnrampScreens.toTypedArray()) + } + result != OnrampRedirectResult.Unknown -> { + scope.launch { + onrampSuccessScreenTrigger.triggerOnrampSuccess(result == OnrampRedirectResult.Success) + } + } + else -> { + Timber.e( + """ + Invalid parameters for ONRAMP deeplink + |- Params: $params + """.trimIndent(), + ) + } + } + } + + @AssistedFactory + interface Factory : OnrampDeepLinkHandler.Factory { + override fun create(coroutineScope: CoroutineScope, params: Map): DefaultOnrampDeepLinkHandler + } + + private companion object { + const val TX_ID_KEY = "tx_id" + const val RESULT_KEY = "result" + } +} \ No newline at end of file diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/deeplink/di/OnrampDeeplinkModule.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/deeplink/di/OnrampDeeplinkModule.kt index 6663ec80d4..fc55367fe3 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/deeplink/di/OnrampDeeplinkModule.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/deeplink/di/OnrampDeeplinkModule.kt @@ -1,7 +1,7 @@ package com.tangem.features.onramp.deeplink.di +import com.tangem.features.onramp.deeplink.* import com.tangem.features.onramp.deeplink.DefaultOnrampDeepLink -import com.tangem.features.onramp.deeplink.OnrampDeepLink import dagger.Binds import dagger.Module import dagger.hilt.InstallIn @@ -15,4 +15,8 @@ internal interface OnrampDeeplinkModule { @Binds @Singleton fun bindFactory(impl: DefaultOnrampDeepLink.Factory): OnrampDeepLink.Factory + + @Binds + @Singleton + fun bindFactoryV2(impl: DefaultOnrampDeepLinkHandler.Factory): OnrampDeepLinkHandler.Factory } \ No newline at end of file diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/success/model/OnrampSuccessComponentModel.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/success/model/OnrampSuccessComponentModel.kt index 436bcd0791..006073a17a 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/success/model/OnrampSuccessComponentModel.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/success/model/OnrampSuccessComponentModel.kt @@ -153,7 +153,9 @@ internal class OnrampSuccessComponentModel @Inject constructor( } else { resourceReference(R.string.express_error_code, wrappedList(errorCode)) }, - onDismissRequest = router::pop, + firstActionBuilder = { + okAction(router::pop) + }, ) messageSender.send(message) From e96b780d21af801ba6bfb71540658f57716f2ee0 Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 19 May 2025 11:07:06 +0400 Subject: [PATCH 053/165] Updated on 2026-08-14 --- .../local/network/entity/NetworkStatusDM.kt | 33 ++ .../multi/DefaultMultiNetworkStatusFetcher.kt | 88 +-- .../DefaultSingleNetworkStatusFetcher.kt | 58 +- .../DefaultMultiNetworkStatusFetcherTest.kt | 518 ++++++++++++++---- .../DefaultSingleNetworkStatusFetcherTest.kt | 122 +++-- .../multi/MultiNetworkStatusFetcher.kt | 2 +- .../single/SingleNetworkStatusFetcher.kt | 22 +- .../tokens/FetchCurrencyStatusUseCase.kt | 2 +- .../UpdateDelayedNetworkStatusUseCase.kt | 2 +- .../usecase/SendTransactionUseCase.kt | 2 +- 10 files changed, 577 insertions(+), 272 deletions(-) diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/network/entity/NetworkStatusDM.kt b/core/datasource/src/main/java/com/tangem/datasource/local/network/entity/NetworkStatusDM.kt index 40f26e9e08..de66806e7f 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/local/network/entity/NetworkStatusDM.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/local/network/entity/NetworkStatusDM.kt @@ -2,17 +2,40 @@ package com.tangem.datasource.local.network.entity import com.squareup.moshi.Json import com.squareup.moshi.JsonClass +import com.tangem.datasource.local.network.entity.NetworkStatusDM.NoAccount +import com.tangem.datasource.local.network.entity.NetworkStatusDM.Verified import com.tangem.domain.tokens.model.Network import dev.onenowy.moshipolymorphicadapter.annotations.NameLabel import java.math.BigDecimal +/** + * Network status for storage in the local cache. Supports two types - the [Verified] and [NoAccount]. + * + * @see [com.tangem.domain.tokens.model.NetworkStatus] + */ sealed interface NetworkStatusDM { + /** Network id */ val networkId: Network.ID + + /** Derivation path */ val derivationPath: DerivationPath + + /** Selected address */ val selectedAddress: String + + /** Available address */ val availableAddresses: Set
+ /** + * Verified + * + * @property networkId network id + * @property derivationPath derivation path + * @property selectedAddress selected address + * @property availableAddresses available addresses + * @property amounts amounts + */ @NameLabel("amounts") data class Verified( @Json(name = "network_id") override val networkId: Network.ID, @@ -22,6 +45,16 @@ sealed interface NetworkStatusDM { @Json(name = "amounts") val amounts: Map, ) : NetworkStatusDM + /** + * No account + * + * @property networkId network id + * @property derivationPath derivation path + * @property selectedAddress selected address + * @property availableAddresses available addresses + * @property amountToCreateAccount amount to create account + * @property errorMessage error message + */ @NameLabel("amount_to_create_account") data class NoAccount( @Json(name = "network_id") override val networkId: Network.ID, diff --git a/data/networks/src/main/java/com/tangem/data/networks/multi/DefaultMultiNetworkStatusFetcher.kt b/data/networks/src/main/java/com/tangem/data/networks/multi/DefaultMultiNetworkStatusFetcher.kt index 67a3ba3d44..d696addbdb 100644 --- a/data/networks/src/main/java/com/tangem/data/networks/multi/DefaultMultiNetworkStatusFetcher.kt +++ b/data/networks/src/main/java/com/tangem/data/networks/multi/DefaultMultiNetworkStatusFetcher.kt @@ -2,26 +2,15 @@ package com.tangem.data.networks.multi import arrow.core.raise.catch import arrow.core.raise.ensure -import com.tangem.blockchain.common.Blockchain -import com.tangem.blockchainsdk.utils.ExcludedBlockchains -import com.tangem.blockchainsdk.utils.fromNetworkId import com.tangem.data.common.currency.CardCryptoCurrencyFactory -import com.tangem.data.common.currency.ResponseCryptoCurrenciesFactory +import com.tangem.data.networks.fetcher.CommonNetworkStatusFetcher import com.tangem.data.networks.store.NetworksStatusesStoreV2 import com.tangem.data.networks.store.setSourceAsCache import com.tangem.data.networks.store.setSourceAsOnlyCache -import com.tangem.datasource.api.tangemTech.models.UserTokensResponse -import com.tangem.datasource.local.preferences.AppPreferencesStore -import com.tangem.datasource.local.preferences.PreferencesKeys -import com.tangem.datasource.local.preferences.utils.getObjectSyncOrNull import com.tangem.datasource.local.userwallet.UserWalletsStore import com.tangem.domain.common.util.cardTypesResolver import com.tangem.domain.core.utils.eitherOn import com.tangem.domain.networks.multi.MultiNetworkStatusFetcher -import com.tangem.domain.networks.single.SingleNetworkStatusFetcher -import com.tangem.domain.tokens.model.CryptoCurrency -import com.tangem.domain.tokens.model.Network -import com.tangem.domain.wallets.models.UserWallet import com.tangem.utils.coroutines.CoroutineDispatcherProvider import kotlinx.coroutines.async import kotlinx.coroutines.awaitAll @@ -31,28 +20,24 @@ import javax.inject.Inject /** * Default implementation of [MultiNetworkStatusFetcher] * - * @property singleNetworkStatusFetcher single network status fetcher * @property networksStatusesStore networks statuses store + * @property userWalletsStore user wallets store + * @property cardCryptoCurrencyFactory card crypto currency factory + * @property commonNetworkStatusFetcher common network status fetcher + * @property dispatchers dispatchers * [REDACTED_AUTHOR] */ @Suppress("LongParameterList") internal class DefaultMultiNetworkStatusFetcher @Inject constructor( - excludedBlockchains: ExcludedBlockchains, private val networksStatusesStore: NetworksStatusesStoreV2, private val userWalletsStore: UserWalletsStore, - private val appPreferencesStore: AppPreferencesStore, - private val singleNetworkStatusFetcher: SingleNetworkStatusFetcher, - private val dispatchers: CoroutineDispatcherProvider, private val cardCryptoCurrencyFactory: CardCryptoCurrencyFactory, + private val commonNetworkStatusFetcher: CommonNetworkStatusFetcher, + private val dispatchers: CoroutineDispatcherProvider, ) : MultiNetworkStatusFetcher { - private val responseCurrenciesFactory by lazy { ResponseCryptoCurrenciesFactory(excludedBlockchains) } - override suspend fun invoke(params: MultiNetworkStatusFetcher.Params) = eitherOn(dispatchers.default) { - // Optimization! - // Every singleNetworkStatusFetcher with applyRefresh as true will refresh every network in the store. - // So if we update all networks at once, it will be more efficient. networksStatusesStore.setSourceAsCache(userWalletId = params.userWalletId, networks = params.networks) val userWallet = catch( @@ -67,27 +52,39 @@ internal class DefaultMultiNetworkStatusFetcher @Inject constructor( }, ) - val isNotSingleWallet = with(userWallet.scanResponse.cardTypesResolver) { + val cardTypesResolver = userWallet.cardTypesResolver + val isWalletSupported = with(cardTypesResolver) { isMultiwalletAllowed() || isSingleWalletWithToken() } - ensure(isNotSingleWallet) { - networksStatusesStore.setSourceAsOnlyCache(userWalletId = params.userWalletId, networks = params.networks) + ensure(isWalletSupported) { + networksStatusesStore.setSourceAsOnlyCache( + userWalletId = params.userWalletId, + networks = params.networks, + ) IllegalStateException("User wallet is not multi-currency") } - val networksCurrencies = createCurrencies(userWallet = userWallet, networks = params.networks) + val networksCurrencies = if (cardTypesResolver.isMultiwalletAllowed()) { + cardCryptoCurrencyFactory.createCurrenciesForMultiCurrencyCard( + userWallet = userWallet, + networks = params.networks, + ) + } else { + cardCryptoCurrencyFactory.createCurrenciesForSingleCurrencyCardWithToken( + scanResponse = userWallet.scanResponse, + ) + .groupBy { it.network } + } val result = coroutineScope { params.networks .map { network -> async { - singleNetworkStatusFetcher( - params = SingleNetworkStatusFetcher.Params.Prepared( - userWalletId = params.userWalletId, - network = network, - addedNetworkCurrencies = networksCurrencies.filter { it.network == network }.toSet(), - ), + commonNetworkStatusFetcher.fetch( + userWalletId = params.userWalletId, + network = network, + networkCurrencies = networksCurrencies[network].orEmpty().toSet(), ) } } @@ -100,31 +97,4 @@ internal class DefaultMultiNetworkStatusFetcher @Inject constructor( IllegalStateException("Failed to fetch network statuses") } } - - private suspend fun createCurrencies(userWallet: UserWallet, networks: Set): Set { - val blockchains = networks.map { Blockchain.fromNetworkId(networkId = it.backendId) } - - // multi-currency wallet - if (userWallet.isMultiCurrency) return getMultiWalletCurrencies(userWallet = userWallet, networks = networks) - - // check if the blockchain of single-currency wallet is the same as network - val cardBlockchain = userWallet.scanResponse.cardTypesResolver.getBlockchain() - if (!blockchains.contains(cardBlockchain)) return emptySet() - - return cardCryptoCurrencyFactory.createCurrenciesForSingleCurrencyCardWithToken(userWallet.scanResponse).toSet() - } - - private suspend fun getMultiWalletCurrencies(userWallet: UserWallet, networks: Set): Set { - val response = appPreferencesStore.getObjectSyncOrNull( - key = PreferencesKeys.getUserTokensKey(userWallet.walletId.stringValue), - ) ?: return emptySet() - - return responseCurrenciesFactory.createCurrencies( - tokens = response.tokens.filter { token -> - networks.any { it.backendId == token.networkId && it.derivationPath.value == token.derivationPath } - }, - scanResponse = userWallet.scanResponse, - ) - .toSet() - } } \ No newline at end of file diff --git a/data/networks/src/main/java/com/tangem/data/networks/single/DefaultSingleNetworkStatusFetcher.kt b/data/networks/src/main/java/com/tangem/data/networks/single/DefaultSingleNetworkStatusFetcher.kt index ef23a8c4fc..c3a38ceb1b 100644 --- a/data/networks/src/main/java/com/tangem/data/networks/single/DefaultSingleNetworkStatusFetcher.kt +++ b/data/networks/src/main/java/com/tangem/data/networks/single/DefaultSingleNetworkStatusFetcher.kt @@ -2,70 +2,46 @@ package com.tangem.data.networks.single import arrow.core.Either import com.tangem.data.common.currency.CardCryptoCurrencyFactory +import com.tangem.data.networks.fetcher.CommonNetworkStatusFetcher import com.tangem.data.networks.store.NetworksStatusesStoreV2 import com.tangem.data.networks.store.setSourceAsCache -import com.tangem.data.networks.store.setSourceAsOnlyCache -import com.tangem.data.networks.store.storeStatus -import com.tangem.data.networks.utils.NetworkStatusFactory import com.tangem.domain.core.utils.catchOn import com.tangem.domain.networks.single.SingleNetworkStatusFetcher -import com.tangem.domain.tokens.model.CryptoCurrency -import com.tangem.domain.walletmanager.WalletManagersFacade import com.tangem.utils.coroutines.CoroutineDispatcherProvider -import kotlinx.coroutines.withContext -import timber.log.Timber import javax.inject.Inject /** * Default implementation of [SingleNetworkStatusFetcher] * - * @property walletManagersFacade wallet managers facade - * @property networksStatusesStore networks statuses store - * @property cardCryptoCurrencyFactory card crypto currency factory - * @property dispatchers dispatchers + * @property commonNetworkStatusFetcher common network status fetcher + * @property networksStatusesStore networks statuses store + * @property cardCryptoCurrencyFactory card crypto currency factory + * @property dispatchers dispatchers * [REDACTED_AUTHOR] */ internal class DefaultSingleNetworkStatusFetcher @Inject constructor( - private val walletManagersFacade: WalletManagersFacade, + private val commonNetworkStatusFetcher: CommonNetworkStatusFetcher, private val networksStatusesStore: NetworksStatusesStoreV2, private val cardCryptoCurrencyFactory: CardCryptoCurrencyFactory, private val dispatchers: CoroutineDispatcherProvider, ) : SingleNetworkStatusFetcher { - override suspend fun invoke(params: SingleNetworkStatusFetcher.Params) = Either.catchOn(dispatchers.default) { - val networkCurrencies = when (params) { - is SingleNetworkStatusFetcher.Params.Prepared -> params.addedNetworkCurrencies - is SingleNetworkStatusFetcher.Params.Simple -> { - networksStatusesStore.setSourceAsCache(userWalletId = params.userWalletId, network = params.network) + override suspend fun invoke(params: SingleNetworkStatusFetcher.Params): Either { + return Either.catchOn(dispatchers.default) { + networksStatusesStore.setSourceAsCache(userWalletId = params.userWalletId, network = params.network) - cardCryptoCurrencyFactory.create( - userWalletId = params.userWalletId, - network = params.network, - ) - } - } - - val result = withContext(dispatchers.io) { - walletManagersFacade.update( + val networkCurrencies = cardCryptoCurrencyFactory.create( userWalletId = params.userWalletId, network = params.network, - extraTokens = networkCurrencies - .filterIsInstance() - .toSet(), ) + + commonNetworkStatusFetcher.fetch( + userWalletId = params.userWalletId, + network = params.network, + networkCurrencies = networkCurrencies.toSet(), + ) + .onLeft { throw it } } - - val status = NetworkStatusFactory.create( - network = params.network, - updatingResult = result, - addedCurrencies = networkCurrencies.toSet(), - ) - - networksStatusesStore.storeStatus(userWalletId = params.userWalletId, status = status) } - .onLeft { - Timber.e("Failed to fetch network status for $params: $it") - networksStatusesStore.setSourceAsOnlyCache(userWalletId = params.userWalletId, network = params.network) - } } \ No newline at end of file diff --git a/data/networks/src/test/java/com/tangem/data/networks/multi/DefaultMultiNetworkStatusFetcherTest.kt b/data/networks/src/test/java/com/tangem/data/networks/multi/DefaultMultiNetworkStatusFetcherTest.kt index 18ea008ac3..069df070d9 100644 --- a/data/networks/src/test/java/com/tangem/data/networks/multi/DefaultMultiNetworkStatusFetcherTest.kt +++ b/data/networks/src/test/java/com/tangem/data/networks/multi/DefaultMultiNetworkStatusFetcherTest.kt @@ -1,119 +1,409 @@ package com.tangem.data.networks.multi +import arrow.core.Either +import com.google.common.truth.Truth +import com.tangem.common.test.domain.token.MockCryptoCurrencyFactory +import com.tangem.common.test.domain.wallet.MockUserWalletFactory +import com.tangem.data.common.currency.CardCryptoCurrencyFactory +import com.tangem.data.networks.fetcher.CommonNetworkStatusFetcher +import com.tangem.data.networks.store.NetworksStatusesStoreV2 +import com.tangem.data.networks.store.setSourceAsCache +import com.tangem.data.networks.store.setSourceAsOnlyCache +import com.tangem.datasource.local.userwallet.UserWalletsStore +import com.tangem.domain.common.CardTypesResolver +import com.tangem.domain.common.util.cardTypesResolver +import com.tangem.domain.networks.multi.MultiNetworkStatusFetcher +import com.tangem.domain.wallets.models.UserWallet +import com.tangem.domain.wallets.models.UserWalletId +import com.tangem.utils.coroutines.TestingCoroutineDispatcherProvider +import io.mockk.* +import kotlinx.coroutines.test.runTest +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.TestInstance + /** [REDACTED_AUTHOR] */ -// internal class DefaultMultiNetworkStatusFetcherTest { -// -// private val singleNetworkStatusFetcher: SingleNetworkStatusFetcher = mockk() -// private val networksStatusesStore: NetworksStatusesStoreV2 = mockk(relaxed = true) -// -// private val fetcher = DefaultMultiNetworkStatusFetcher( -// singleNetworkStatusFetcher = singleNetworkStatusFetcher, -// networksStatusesStore = networksStatusesStore, -// ) -// -// @Test -// fun `fetch networks statuses successfully`() = runTest { -// val params = MultiNetworkStatusFetcher.Params(userWalletId = userWalletId, networks = ethereumAndStellar) -// -// val ethParams = SingleNetworkStatusFetcher.Params( -// userWalletId = userWalletId, -// network = ethereumAndStellar.first(), -// applyRefresh = false, -// ) -// -// val stellarParams = SingleNetworkStatusFetcher.Params( -// userWalletId = userWalletId, -// network = ethereumAndStellar.last(), -// applyRefresh = false, -// ) -// -// coEvery { singleNetworkStatusFetcher(ethParams) } returns Unit.right() -// coEvery { singleNetworkStatusFetcher(stellarParams) } returns Unit.right() -// -// val actual = fetcher(params) -// -// coVerify { -// networksStatusesStore.refresh(userWalletId = userWalletId, networks = ethereumAndStellar) -// singleNetworkStatusFetcher(ethParams) -// singleNetworkStatusFetcher(stellarParams) -// } -// -// Truth.assertThat(actual.isRight()).isTrue() -// } -// -// @Test -// fun `fetch networks statuses failure if one of them fails`() = runTest { -// val params = MultiNetworkStatusFetcher.Params(userWalletId = userWalletId, networks = ethereumAndStellar) -// -// val ethParams = SingleNetworkStatusFetcher.Params( -// userWalletId = userWalletId, -// network = ethereumAndStellar.first(), -// applyRefresh = false, -// ) -// -// val stellarParams = SingleNetworkStatusFetcher.Params( -// userWalletId = userWalletId, -// network = ethereumAndStellar.last(), -// applyRefresh = false, -// ) -// -// val ethException = IllegalStateException("eth") -// coEvery { singleNetworkStatusFetcher(ethParams) } returns ethException.left() -// coEvery { singleNetworkStatusFetcher(stellarParams) } returns Unit.right() -// -// val actual = fetcher(params) -// -// coVerify { -// networksStatusesStore.refresh(userWalletId = userWalletId, networks = ethereumAndStellar) -// singleNetworkStatusFetcher(ethParams) -// singleNetworkStatusFetcher(stellarParams) -// } -// -// val expected = IllegalStateException("Failed to fetch network statuses") -// -// Truth.assertThat(actual.isLeft()).isTrue() -// Truth.assertThat(actual.leftOrNull()).isInstanceOf(expected::class.java) -// Truth.assertThat(actual.leftOrNull()).hasMessageThat().isEqualTo(expected.message) -// } -// -// @Test -// fun `fetch networks statuses failure if all of them fails`() = runTest { -// val params = MultiNetworkStatusFetcher.Params(userWalletId = userWalletId, networks = ethereumAndStellar) -// -// val ethParams = SingleNetworkStatusFetcher.Params( -// userWalletId = userWalletId, -// network = ethereumAndStellar.first(), -// applyRefresh = false, -// ) -// -// val stellarParams = SingleNetworkStatusFetcher.Params( -// userWalletId = userWalletId, -// network = ethereumAndStellar.last(), -// applyRefresh = false, -// ) -// -// coEvery { singleNetworkStatusFetcher(ethParams) } returns IllegalStateException("eth").left() -// coEvery { singleNetworkStatusFetcher(stellarParams) } returns IllegalStateException("stellar").left() -// -// val actual = fetcher(params) -// -// coVerify { -// networksStatusesStore.refresh(userWalletId = userWalletId, networks = ethereumAndStellar) -// singleNetworkStatusFetcher(ethParams) -// singleNetworkStatusFetcher(stellarParams) -// } -// -// val expected = IllegalStateException("Failed to fetch network statuses") -// -// Truth.assertThat(actual.isLeft()).isTrue() -// Truth.assertThat(actual.leftOrNull()).isInstanceOf(expected::class.java) -// Truth.assertThat(actual.leftOrNull()).hasMessageThat().isEqualTo(expected.message) -// } -// -// private companion object { -// val userWalletId = UserWalletId("011") -// val ethereumAndStellar = MockCryptoCurrencyFactory().ethereumAndStellar.map { it.network }.toSet() -// } -// } \ No newline at end of file +@TestInstance(TestInstance.Lifecycle.PER_CLASS) +internal class DefaultMultiNetworkStatusFetcherTest { + + private val networksStatusesStore: NetworksStatusesStoreV2 = mockk(relaxUnitFun = true) + private val userWalletsStore: UserWalletsStore = mockk() + private val cardCryptoCurrencyFactory: CardCryptoCurrencyFactory = mockk() + private val commonNetworkStatusFetcher: CommonNetworkStatusFetcher = mockk() + + private val fetcher = DefaultMultiNetworkStatusFetcher( + networksStatusesStore = networksStatusesStore, + userWalletsStore = userWalletsStore, + cardCryptoCurrencyFactory = cardCryptoCurrencyFactory, + commonNetworkStatusFetcher = commonNetworkStatusFetcher, + dispatchers = TestingCoroutineDispatcherProvider(), + ) + + @BeforeEach + fun resetMocks() { + clearMocks(networksStatusesStore, userWalletsStore, cardCryptoCurrencyFactory, commonNetworkStatusFetcher) + } + + @Test + fun `fetch successfully for multi-currency card`() = runTest { + // Arrange + val networks = setOf(ethereum.network, cardano.network) + val params = MultiNetworkStatusFetcher.Params(userWalletId = userWalletId, networks = networks) + val userWallet = MockUserWalletFactory.create() + val cardTypesResolver = mockk() + val networksCurrencies = mapOf( + ethereum.network to listOf(ethereum), + cardano.network to listOf(cardano), + ) + val ethereumFetcherResult = Either.Right(Unit) + val cardanoFetcherResult = Either.Right(Unit) + + coEvery { networksStatusesStore.setSourceAsCache(params.userWalletId, params.networks) } returns Unit + every { userWalletsStore.getSyncStrict(key = userWalletId) } returns userWallet + + mockkStatic(UserWallet::cardTypesResolver) + every { userWallet.cardTypesResolver } returns cardTypesResolver + coEvery { cardTypesResolver.isMultiwalletAllowed() } returns true + + coEvery { + cardCryptoCurrencyFactory.createCurrenciesForMultiCurrencyCard(userWallet, params.networks) + } returns networksCurrencies + + coEvery { + commonNetworkStatusFetcher.fetch( + userWalletId = params.userWalletId, + network = ethereum.network, + networkCurrencies = setOf(ethereum), + ) + } returns ethereumFetcherResult + + coEvery { + commonNetworkStatusFetcher.fetch( + userWalletId = params.userWalletId, + network = cardano.network, + networkCurrencies = setOf(cardano), + ) + } returns cardanoFetcherResult + + // Act + val actual = fetcher(params) + + // Assert + val expected = Either.Right(Unit) + + Truth.assertThat(actual).isEqualTo(expected) + + coVerifyOrder { + networksStatusesStore.setSourceAsCache(params.userWalletId, params.networks) + userWalletsStore.getSyncStrict(key = userWalletId) + cardTypesResolver.isMultiwalletAllowed() + cardCryptoCurrencyFactory.createCurrenciesForMultiCurrencyCard(userWallet, params.networks) + commonNetworkStatusFetcher.fetch( + userWalletId = params.userWalletId, + network = ethereum.network, + networkCurrencies = setOf(ethereum), + ) + commonNetworkStatusFetcher.fetch( + userWalletId = params.userWalletId, + network = cardano.network, + networkCurrencies = setOf(cardano), + ) + } + + coVerify(inverse = true) { + networksStatusesStore.setSourceAsOnlyCache(userWalletId = any(), networks = any()) + cardTypesResolver.isSingleWalletWithToken() + cardCryptoCurrencyFactory.createCurrenciesForSingleCurrencyCardWithToken(scanResponse = any()) + } + } + + @Test + fun `fetch successfully for single-currency card with token`() = runTest { + // Arrange + val networks = setOf(ethereum.network) + val params = MultiNetworkStatusFetcher.Params(userWalletId = userWalletId, networks = networks) + val userWallet = MockUserWalletFactory.create() + val cardTypesResolver = mockk() + val networksCurrencies = listOf(ethereum) + val ethereumFetcherResult = Either.Right(Unit) + + coEvery { networksStatusesStore.setSourceAsCache(params.userWalletId, params.networks) } returns Unit + every { userWalletsStore.getSyncStrict(key = userWalletId) } returns userWallet + + mockkStatic(UserWallet::cardTypesResolver) + every { userWallet.cardTypesResolver } returns cardTypesResolver + coEvery { cardTypesResolver.isMultiwalletAllowed() } returns false + coEvery { cardTypesResolver.isSingleWalletWithToken() } returns true + + coEvery { + cardCryptoCurrencyFactory.createCurrenciesForSingleCurrencyCardWithToken(userWallet.scanResponse) + } returns networksCurrencies + + coEvery { + commonNetworkStatusFetcher.fetch( + userWalletId = params.userWalletId, + network = ethereum.network, + networkCurrencies = setOf(ethereum), + ) + } returns ethereumFetcherResult + + // Act + val actual = fetcher(params) + + // Assert + val expected = Either.Right(Unit) + + Truth.assertThat(actual).isEqualTo(expected) + + coVerifyOrder { + networksStatusesStore.setSourceAsCache(params.userWalletId, params.networks) + userWalletsStore.getSyncStrict(key = userWalletId) + cardTypesResolver.isMultiwalletAllowed() + cardTypesResolver.isSingleWalletWithToken() + cardCryptoCurrencyFactory.createCurrenciesForSingleCurrencyCardWithToken(userWallet.scanResponse) + commonNetworkStatusFetcher.fetch( + userWalletId = params.userWalletId, + network = ethereum.network, + networkCurrencies = setOf(ethereum), + ) + } + + coVerify(inverse = true) { + networksStatusesStore.setSourceAsOnlyCache(userWalletId = any(), networks = any()) + cardCryptoCurrencyFactory.createCurrenciesForMultiCurrencyCard(any(), any()) + } + } + + @Test + fun `fetch failure if one of them fails`() = runTest { + // Arrange + val networks = setOf(ethereum.network, cardano.network) + val params = MultiNetworkStatusFetcher.Params(userWalletId = userWalletId, networks = networks) + val userWallet = MockUserWalletFactory.create() + val cardTypesResolver = mockk() + val networksCurrencies = mapOf( + ethereum.network to listOf(ethereum), + cardano.network to listOf(cardano), + ) + val ethereumFetcherResult = Either.Left(IllegalStateException()) + val cardanoFetcherResult = Either.Right(Unit) + + coEvery { networksStatusesStore.setSourceAsCache(params.userWalletId, params.networks) } returns Unit + every { userWalletsStore.getSyncStrict(key = userWalletId) } returns userWallet + + mockkStatic(UserWallet::cardTypesResolver) + every { userWallet.cardTypesResolver } returns cardTypesResolver + coEvery { cardTypesResolver.isMultiwalletAllowed() } returns true + + coEvery { + cardCryptoCurrencyFactory.createCurrenciesForMultiCurrencyCard(userWallet, params.networks) + } returns networksCurrencies + + coEvery { + commonNetworkStatusFetcher.fetch( + userWalletId = params.userWalletId, + network = ethereum.network, + networkCurrencies = setOf(ethereum), + ) + } returns ethereumFetcherResult + + coEvery { + commonNetworkStatusFetcher.fetch( + userWalletId = params.userWalletId, + network = cardano.network, + networkCurrencies = setOf(cardano), + ) + } returns cardanoFetcherResult + + // Act + val actual = fetcher(params) + + // Assert + val expected = Either.Left(IllegalStateException("Failed to fetch network statuses")) + Truth.assertThat(actual.isLeft()).isTrue() + Truth.assertThat(actual.leftOrNull()).isInstanceOf(expected.leftOrNull()!!::class.java) + Truth.assertThat(actual.leftOrNull()).hasMessageThat().isEqualTo(expected.leftOrNull()!!.message) + + coVerifyOrder { + networksStatusesStore.setSourceAsCache(params.userWalletId, params.networks) + userWalletsStore.getSyncStrict(key = userWalletId) + cardTypesResolver.isMultiwalletAllowed() + cardCryptoCurrencyFactory.createCurrenciesForMultiCurrencyCard(userWallet, params.networks) + commonNetworkStatusFetcher.fetch( + userWalletId = params.userWalletId, + network = ethereum.network, + networkCurrencies = setOf(ethereum), + ) + commonNetworkStatusFetcher.fetch( + userWalletId = params.userWalletId, + network = cardano.network, + networkCurrencies = setOf(cardano), + ) + } + + coVerify(inverse = true) { + networksStatusesStore.setSourceAsOnlyCache(userWalletId = any(), networks = any()) + cardTypesResolver.isSingleWalletWithToken() + cardCryptoCurrencyFactory.createCurrenciesForSingleCurrencyCardWithToken(scanResponse = any()) + } + } + + @Test + fun `fetch failure if all of them fails`() = runTest { + // Arrange + val networks = setOf(ethereum.network, cardano.network) + val params = MultiNetworkStatusFetcher.Params(userWalletId = userWalletId, networks = networks) + val userWallet = MockUserWalletFactory.create() + val cardTypesResolver = mockk() + val networksCurrencies = mapOf( + ethereum.network to listOf(ethereum), + cardano.network to listOf(cardano), + ) + val ethereumFetcherResult = Either.Left(IllegalStateException("ethereum")) + val cardanoFetcherResult = Either.Left(IllegalStateException("cardano")) + + coEvery { networksStatusesStore.setSourceAsCache(params.userWalletId, params.networks) } returns Unit + every { userWalletsStore.getSyncStrict(key = userWalletId) } returns userWallet + + mockkStatic(UserWallet::cardTypesResolver) + every { userWallet.cardTypesResolver } returns cardTypesResolver + coEvery { cardTypesResolver.isMultiwalletAllowed() } returns true + + coEvery { + cardCryptoCurrencyFactory.createCurrenciesForMultiCurrencyCard(userWallet, params.networks) + } returns networksCurrencies + + coEvery { + commonNetworkStatusFetcher.fetch( + userWalletId = params.userWalletId, + network = ethereum.network, + networkCurrencies = setOf(ethereum), + ) + } returns ethereumFetcherResult + + coEvery { + commonNetworkStatusFetcher.fetch( + userWalletId = params.userWalletId, + network = cardano.network, + networkCurrencies = setOf(cardano), + ) + } returns cardanoFetcherResult + + // Act + val actual = fetcher(params) + + // Assert + val expected = Either.Left(IllegalStateException("Failed to fetch network statuses")) + Truth.assertThat(actual.isLeft()).isTrue() + Truth.assertThat(actual.leftOrNull()).isInstanceOf(expected.leftOrNull()!!::class.java) + Truth.assertThat(actual.leftOrNull()).hasMessageThat().isEqualTo(expected.leftOrNull()!!.message) + + coVerifyOrder { + networksStatusesStore.setSourceAsCache(params.userWalletId, params.networks) + userWalletsStore.getSyncStrict(key = userWalletId) + cardTypesResolver.isMultiwalletAllowed() + cardCryptoCurrencyFactory.createCurrenciesForMultiCurrencyCard(userWallet, params.networks) + commonNetworkStatusFetcher.fetch( + userWalletId = params.userWalletId, + network = ethereum.network, + networkCurrencies = setOf(ethereum), + ) + commonNetworkStatusFetcher.fetch( + userWalletId = params.userWalletId, + network = cardano.network, + networkCurrencies = setOf(cardano), + ) + } + + coVerify(inverse = true) { + networksStatusesStore.setSourceAsOnlyCache(userWalletId = any(), networks = any()) + cardTypesResolver.isSingleWalletWithToken() + cardCryptoCurrencyFactory.createCurrenciesForSingleCurrencyCardWithToken(scanResponse = any()) + } + } + + @Test + fun `fetch failure if userWalletsStore throws exception`() = runTest { + // Arrange + val networks = setOf(ethereum.network, cardano.network) + val params = MultiNetworkStatusFetcher.Params(userWalletId = userWalletId, networks = networks) + val userWalletStoreException = IllegalStateException() + + coEvery { networksStatusesStore.setSourceAsCache(params.userWalletId, params.networks) } returns Unit + every { userWalletsStore.getSyncStrict(key = userWalletId) } throws userWalletStoreException + coEvery { networksStatusesStore.setSourceAsOnlyCache(params.userWalletId, params.networks) } returns Unit + + // Act + val actual = fetcher(params) + + // Assert + val expected = Either.Left(userWalletStoreException) + + Truth.assertThat(actual).isEqualTo(expected) + + coVerifyOrder { + networksStatusesStore.setSourceAsCache(params.userWalletId, params.networks) + userWalletsStore.getSyncStrict(key = userWalletId) + networksStatusesStore.setSourceAsOnlyCache(params.userWalletId, params.networks) + } + + coVerify(inverse = true) { + cardCryptoCurrencyFactory.createCurrenciesForMultiCurrencyCard(any(), any()) + cardCryptoCurrencyFactory.createCurrenciesForSingleCurrencyCardWithToken(scanResponse = any()) + commonNetworkStatusFetcher.fetch(any(), any(), any()) + } + } + + @Test + fun `fetch failure if card is single-currency`() = runTest { + // Arrange + val networks = setOf(ethereum.network, cardano.network) + val params = MultiNetworkStatusFetcher.Params(userWalletId = userWalletId, networks = networks) + val userWallet = MockUserWalletFactory.create() + val cardTypesResolver = mockk() + + coEvery { networksStatusesStore.setSourceAsCache(params.userWalletId, params.networks) } returns Unit + every { userWalletsStore.getSyncStrict(key = userWalletId) } returns userWallet + + mockkStatic(UserWallet::cardTypesResolver) + every { userWallet.cardTypesResolver } returns cardTypesResolver + coEvery { cardTypesResolver.isMultiwalletAllowed() } returns false + coEvery { cardTypesResolver.isSingleWalletWithToken() } returns false + + coEvery { networksStatusesStore.setSourceAsOnlyCache(params.userWalletId, params.networks) } returns Unit + + // Act + val actual = fetcher(params) + + // Assert + val expected = Either.Left(IllegalStateException("User wallet is not multi-currency")) + + Truth.assertThat(actual.isLeft()).isTrue() + Truth.assertThat(actual.leftOrNull()).isInstanceOf(expected.leftOrNull()!!::class.java) + Truth.assertThat(actual.leftOrNull()).hasMessageThat().isEqualTo(expected.leftOrNull()!!.message) + + coVerifyOrder { + networksStatusesStore.setSourceAsCache(params.userWalletId, params.networks) + userWalletsStore.getSyncStrict(key = userWalletId) + cardTypesResolver.isMultiwalletAllowed() + cardTypesResolver.isSingleWalletWithToken() + networksStatusesStore.setSourceAsOnlyCache(params.userWalletId, params.networks) + } + + coVerify(inverse = true) { + networksStatusesStore.setSourceAsOnlyCache(userWalletId = any(), networks = any()) + cardCryptoCurrencyFactory.createCurrenciesForMultiCurrencyCard(any(), any()) + cardCryptoCurrencyFactory.createCurrenciesForSingleCurrencyCardWithToken(scanResponse = any()) + commonNetworkStatusFetcher.fetch(any(), any(), any()) + } + } + + private companion object { + val userWalletId = UserWalletId("011") + val cryptoCurrencyFactory = MockCryptoCurrencyFactory() + val ethereum = cryptoCurrencyFactory.ethereum + val cardano = cryptoCurrencyFactory.cardano + } +} \ No newline at end of file diff --git a/data/networks/src/test/java/com/tangem/data/networks/single/DefaultSingleNetworkStatusFetcherTest.kt b/data/networks/src/test/java/com/tangem/data/networks/single/DefaultSingleNetworkStatusFetcherTest.kt index 813f2d20c1..21b5a1df8d 100644 --- a/data/networks/src/test/java/com/tangem/data/networks/single/DefaultSingleNetworkStatusFetcherTest.kt +++ b/data/networks/src/test/java/com/tangem/data/networks/single/DefaultSingleNetworkStatusFetcherTest.kt @@ -1,97 +1,141 @@ package com.tangem.data.networks.single +import arrow.core.Either +import arrow.core.left import com.google.common.truth.Truth import com.tangem.common.test.domain.token.MockCryptoCurrencyFactory import com.tangem.data.common.currency.CardCryptoCurrencyFactory +import com.tangem.data.networks.fetcher.CommonNetworkStatusFetcher import com.tangem.data.networks.store.NetworksStatusesStoreV2 import com.tangem.data.networks.store.setSourceAsCache -import com.tangem.data.networks.store.storeSuccess import com.tangem.domain.networks.single.SingleNetworkStatusFetcher -import com.tangem.domain.tokens.model.NetworkStatus -import com.tangem.domain.walletmanager.WalletManagersFacade -import com.tangem.domain.walletmanager.model.UpdateWalletManagerResult import com.tangem.domain.wallets.models.UserWalletId import com.tangem.utils.coroutines.TestingCoroutineDispatcherProvider -import io.mockk.coEvery -import io.mockk.coVerify -import io.mockk.coVerifyOrder -import io.mockk.mockk +import io.mockk.* import kotlinx.coroutines.test.runTest -import org.junit.Test +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.TestInstance /** [REDACTED_AUTHOR] */ +@TestInstance(TestInstance.Lifecycle.PER_CLASS) internal class DefaultSingleNetworkStatusFetcherTest { - private val walletManagersFacade: WalletManagersFacade = mockk(relaxUnitFun = true) + private val commonNetworkStatusFetcher: CommonNetworkStatusFetcher = mockk() private val networksStatusesStore: NetworksStatusesStoreV2 = mockk(relaxUnitFun = true) private val cardCryptoCurrencyFactory: CardCryptoCurrencyFactory = mockk() private val fetcher = DefaultSingleNetworkStatusFetcher( - walletManagersFacade = walletManagersFacade, + commonNetworkStatusFetcher = commonNetworkStatusFetcher, networksStatusesStore = networksStatusesStore, cardCryptoCurrencyFactory = cardCryptoCurrencyFactory, dispatchers = TestingCoroutineDispatcherProvider(), ) + @BeforeEach + fun resetMocks() { + clearMocks(commonNetworkStatusFetcher, networksStatusesStore, cardCryptoCurrencyFactory) + } + @Test - fun `fetch network status successfully`() = runTest { - val params = createParams() + fun `fetch successfully`() = runTest { + // Arrange + val params = SingleNetworkStatusFetcher.Params(userWalletId = userWalletId, network = ethereum.network) + val networkCurrencies = listOf(ethereum) + val commonFetcherResult = Either.Right(Unit) - coEvery { cardCryptoCurrencyFactory.create(params.userWalletId, params.network) } returns listOf(ethereum) - - val result = UpdateWalletManagerResult.MissedDerivation - coEvery { walletManagersFacade.update(params.userWalletId, params.network, emptySet()) } returns result + coEvery { networksStatusesStore.setSourceAsCache(params.userWalletId, params.network) } returns Unit + coEvery { cardCryptoCurrencyFactory.create(params.userWalletId, params.network) } returns networkCurrencies + coEvery { + commonNetworkStatusFetcher.fetch( + userWalletId = params.userWalletId, + network = params.network, + networkCurrencies = networkCurrencies.toSet(), + ) + } returns commonFetcherResult + // Act val actual = fetcher(params) + // Assert + val expected = commonFetcherResult + + Truth.assertThat(actual).isEqualTo(expected) + coVerifyOrder { networksStatusesStore.setSourceAsCache(params.userWalletId, params.network) cardCryptoCurrencyFactory.create(params.userWalletId, params.network) - walletManagersFacade.update(params.userWalletId, params.network, emptySet()) - networksStatusesStore.storeSuccess( - userWalletId = params.userWalletId, - status = NetworkStatus(params.network, NetworkStatus.MissedDerivation), - ) + commonNetworkStatusFetcher.fetch(params.userWalletId, params.network, setOf(ethereum)) } - - Truth.assertThat(actual.isRight()).isTrue() } @Test - fun `fetch network status failure`() = runTest { - val params = createParams() + fun `fetch failure if cardCryptoCurrencyFactory throws exception`() = runTest { + // Arrange + val params = SingleNetworkStatusFetcher.Params(userWalletId = userWalletId, network = ethereum.network) + val factoryException = IllegalStateException() - val exception = IllegalStateException() - coEvery { cardCryptoCurrencyFactory.create(params.userWalletId, params.network) } throws exception + coEvery { networksStatusesStore.setSourceAsCache(params.userWalletId, params.network) } returns Unit + coEvery { cardCryptoCurrencyFactory.create(params.userWalletId, params.network) } throws factoryException + // Act val actual = fetcher(params) + // Arrange + val expected = Either.Left(factoryException) + Truth.assertThat(actual).isEqualTo(expected) + coVerifyOrder { networksStatusesStore.setSourceAsCache(userWalletId = params.userWalletId, network = params.network) cardCryptoCurrencyFactory.create(userWalletId = params.userWalletId, network = params.network) - // networksStatusesStore.setSourceAsOnlyCache(userWalletId = params.userWalletId, network = params.network) } coVerify(inverse = true) { - walletManagersFacade.update(userWalletId = any(), network = any(), extraTokens = any()) - // networksStatusesStore.storeSuccess(userWalletId = any(), status = any()) + commonNetworkStatusFetcher.fetch(userWalletId = any(), network = any(), networkCurrencies = any()) } - - Truth.assertThat(actual.isLeft()).isTrue() - Truth.assertThat(actual.leftOrNull()).isEqualTo(exception) } - private fun createParams(): SingleNetworkStatusFetcher.Params { - return SingleNetworkStatusFetcher.Params.Simple( - userWalletId = UserWalletId("011"), - network = ethereum.network, - ) + @Test + fun `fetch failure if commonNetworkStatusFetcher returns exception`() = runTest { + // Arrange + val params = SingleNetworkStatusFetcher.Params(userWalletId = userWalletId, network = ethereum.network) + val networkCurrencies = listOf(ethereum) + val commonFetcherResult = IllegalStateException().left() + + coEvery { networksStatusesStore.setSourceAsCache(params.userWalletId, params.network) } returns Unit + coEvery { cardCryptoCurrencyFactory.create(params.userWalletId, params.network) } returns networkCurrencies + coEvery { + commonNetworkStatusFetcher.fetch( + userWalletId = params.userWalletId, + network = params.network, + networkCurrencies = networkCurrencies.toSet(), + ) + } returns commonFetcherResult + + // Act + val actual = fetcher(params) + + // Arrange + val expected = commonFetcherResult + + Truth.assertThat(actual).isEqualTo(expected) + + coVerifyOrder { + networksStatusesStore.setSourceAsCache(userWalletId = params.userWalletId, network = params.network) + cardCryptoCurrencyFactory.create(userWalletId = params.userWalletId, network = params.network) + commonNetworkStatusFetcher.fetch( + userWalletId = params.userWalletId, + network = params.network, + networkCurrencies = setOf(ethereum), + ) + } } private companion object { + val userWalletId = UserWalletId("011") val ethereum = MockCryptoCurrencyFactory().ethereum } } \ No newline at end of file diff --git a/domain/networks/src/main/java/com/tangem/domain/networks/multi/MultiNetworkStatusFetcher.kt b/domain/networks/src/main/java/com/tangem/domain/networks/multi/MultiNetworkStatusFetcher.kt index 0710aa80d3..c1afcf6834 100644 --- a/domain/networks/src/main/java/com/tangem/domain/networks/multi/MultiNetworkStatusFetcher.kt +++ b/domain/networks/src/main/java/com/tangem/domain/networks/multi/MultiNetworkStatusFetcher.kt @@ -5,7 +5,7 @@ import com.tangem.domain.tokens.model.Network import com.tangem.domain.wallets.models.UserWalletId /** - * Fetcher of network status [Network] for wallet with [UserWalletId] + * Fetcher of network status [Network] for multi-currency wallet with [UserWalletId] * [REDACTED_AUTHOR] */ diff --git a/domain/networks/src/main/java/com/tangem/domain/networks/single/SingleNetworkStatusFetcher.kt b/domain/networks/src/main/java/com/tangem/domain/networks/single/SingleNetworkStatusFetcher.kt index 30836055f1..3ad9457bea 100644 --- a/domain/networks/src/main/java/com/tangem/domain/networks/single/SingleNetworkStatusFetcher.kt +++ b/domain/networks/src/main/java/com/tangem/domain/networks/single/SingleNetworkStatusFetcher.kt @@ -1,7 +1,6 @@ package com.tangem.domain.networks.single import com.tangem.domain.core.flow.FlowFetcher -import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.domain.tokens.model.Network import com.tangem.domain.wallets.models.UserWalletId @@ -12,18 +11,11 @@ import com.tangem.domain.wallets.models.UserWalletId */ interface SingleNetworkStatusFetcher : FlowFetcher { - /** Params */ - sealed interface Params { - - val userWalletId: UserWalletId - val network: Network - - data class Simple(override val userWalletId: UserWalletId, override val network: Network) : Params - - data class Prepared( - override val userWalletId: UserWalletId, - override val network: Network, - val addedNetworkCurrencies: Set, - ) : Params - } + /** + * Params + * + * @property userWalletId user wallet id + * @property network network + */ + data class Params(val userWalletId: UserWalletId, val network: Network) } \ No newline at end of file diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/FetchCurrencyStatusUseCase.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/FetchCurrencyStatusUseCase.kt index 303758d938..dee7078730 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/FetchCurrencyStatusUseCase.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/FetchCurrencyStatusUseCase.kt @@ -119,7 +119,7 @@ class FetchCurrencyStatusUseCase( private suspend fun Raise.fetchNetworkStatus(userWalletId: UserWalletId, network: Network) { singleNetworkStatusFetcher( - params = SingleNetworkStatusFetcher.Params.Simple(userWalletId = userWalletId, network = network), + params = SingleNetworkStatusFetcher.Params(userWalletId = userWalletId, network = network), ) .mapLeft { CurrencyStatusError.DataError(it) } .bind() diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/UpdateDelayedNetworkStatusUseCase.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/UpdateDelayedNetworkStatusUseCase.kt index ada4cab157..632b102018 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/UpdateDelayedNetworkStatusUseCase.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/UpdateDelayedNetworkStatusUseCase.kt @@ -38,7 +38,7 @@ class UpdateDelayedNetworkStatusUseCase( private suspend fun Raise.fetchNetworkStatus(userWalletId: UserWalletId, network: Network) { singleNetworkStatusFetcher( - params = SingleNetworkStatusFetcher.Params.Simple(userWalletId = userWalletId, network = network), + params = SingleNetworkStatusFetcher.Params(userWalletId = userWalletId, network = network), ) .mapLeft(CurrencyStatusError::DataError) .bind() diff --git a/domain/transaction/src/main/java/com/tangem/domain/transaction/usecase/SendTransactionUseCase.kt b/domain/transaction/src/main/java/com/tangem/domain/transaction/usecase/SendTransactionUseCase.kt index 0bf8d57527..e519d17ac5 100644 --- a/domain/transaction/src/main/java/com/tangem/domain/transaction/usecase/SendTransactionUseCase.kt +++ b/domain/transaction/src/main/java/com/tangem/domain/transaction/usecase/SendTransactionUseCase.kt @@ -82,7 +82,7 @@ class SendTransactionUseCase( return sendResult .onRight { singleNetworkStatusFetcher( - params = SingleNetworkStatusFetcher.Params.Simple( + params = SingleNetworkStatusFetcher.Params( userWalletId = userWallet.walletId, network = network, ), From 438b8ab22bb88ad281ecdc4cda4bdacbbdca3b4d Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 19 May 2025 16:03:27 +0400 Subject: [PATCH 054/165] Updated on 2026-08-14 --- ...cieve_new_24.xml => ic_receive_new_24.xml} | 0 .../blockaid/TransactionCheckResultsUM.kt | 6 ++ .../blockaid/TransactionCheckResultsItem.kt | 75 +++++++++++++++++++ .../blockaid/WcEstimatedWalletChangesItem.kt | 2 +- .../blockaid/WcTransactionCheckErrorItem.kt | 49 ++++++++++++ 5 files changed, 131 insertions(+), 1 deletion(-) rename core/ui/src/main/res/drawable/{ic_recieve_new_24.xml => ic_receive_new_24.xml} (100%) create mode 100644 features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/entity/blockaid/TransactionCheckResultsUM.kt create mode 100644 features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/ui/blockaid/TransactionCheckResultsItem.kt create mode 100644 features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/ui/blockaid/WcTransactionCheckErrorItem.kt diff --git a/core/ui/src/main/res/drawable/ic_recieve_new_24.xml b/core/ui/src/main/res/drawable/ic_receive_new_24.xml similarity index 100% rename from core/ui/src/main/res/drawable/ic_recieve_new_24.xml rename to core/ui/src/main/res/drawable/ic_receive_new_24.xml diff --git a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/entity/blockaid/TransactionCheckResultsUM.kt b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/entity/blockaid/TransactionCheckResultsUM.kt new file mode 100644 index 0000000000..6bb884bc3c --- /dev/null +++ b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/entity/blockaid/TransactionCheckResultsUM.kt @@ -0,0 +1,6 @@ +package com.tangem.features.walletconnect.transaction.entity.blockaid + +internal data class TransactionCheckResultsUM( + val estimatedWalletChanges: WcEstimatedWalletChangesUM, + val notificationText: String? = null, +) \ No newline at end of file diff --git a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/ui/blockaid/TransactionCheckResultsItem.kt b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/ui/blockaid/TransactionCheckResultsItem.kt new file mode 100644 index 0000000000..a4cbbcc8cc --- /dev/null +++ b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/ui/blockaid/TransactionCheckResultsItem.kt @@ -0,0 +1,75 @@ +package com.tangem.features.walletconnect.transaction.ui.blockaid + +import android.content.res.Configuration +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.* +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.tooling.preview.Devices +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.tooling.preview.PreviewParameter +import androidx.compose.ui.tooling.preview.PreviewParameterProvider +import androidx.compose.ui.unit.dp +import com.tangem.core.ui.extensions.resourceReference +import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.res.TangemThemePreview +import com.tangem.features.walletconnect.impl.R +import com.tangem.features.walletconnect.transaction.entity.blockaid.WcEstimatedWalletChangeUM +import com.tangem.features.walletconnect.transaction.entity.blockaid.WcEstimatedWalletChangesUM +import com.tangem.features.walletconnect.transaction.entity.blockaid.TransactionCheckResultsUM +import kotlinx.collections.immutable.persistentListOf + +@Composable +internal fun TransactionCheckResultsItem(item: TransactionCheckResultsUM, modifier: Modifier = Modifier) { + Column( + modifier = modifier + .padding(12.dp) + .fillMaxWidth(), + verticalArrangement = Arrangement.spacedBy(12.dp), + ) { + if (item.notificationText != null) { + WcTransactionCheckErrorItem(item.notificationText) + } + WcEstimatedWalletChangesItem(item.estimatedWalletChanges) + } +} + +@Composable +@Preview(showBackground = true, device = Devices.PIXEL_7_PRO) +@Preview(showBackground = true, device = Devices.PIXEL_7_PRO, uiMode = Configuration.UI_MODE_NIGHT_YES) +private fun TransactionCheckResultsItemPreview( + @PreviewParameter(TransactionCheckResultsItemProvider::class) item: TransactionCheckResultsUM, +) { + TangemThemePreview { + Box( + modifier = Modifier + .background(TangemTheme.colors.background.tertiary), + ) { + TransactionCheckResultsItem(item = item) + } + } +} + +private class TransactionCheckResultsItemProvider : PreviewParameterProvider { + override val values = sequenceOf( + TransactionCheckResultsUM( + notificationText = "The transaction approves erc20 tokens to a known malicious address", + estimatedWalletChanges = WcEstimatedWalletChangesUM( + items = persistentListOf( + WcEstimatedWalletChangeUM( + iconRes = R.drawable.ic_send_new_24, + title = resourceReference(R.string.common_send), + description = "- 42 USDT", + tokenIconUrl = "https://tangem.com", + ), + WcEstimatedWalletChangeUM( + iconRes = R.drawable.ic_receive_new_24, + title = resourceReference(R.string.common_receive), + description = "+ 1,131.46 MATIC", + tokenIconUrl = "https://tangem.com", + ), + ), + ), + ), + ) +} \ No newline at end of file diff --git a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/ui/blockaid/WcEstimatedWalletChangesItem.kt b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/ui/blockaid/WcEstimatedWalletChangesItem.kt index b0b37f31ac..cbe8e3f59a 100644 --- a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/ui/blockaid/WcEstimatedWalletChangesItem.kt +++ b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/ui/blockaid/WcEstimatedWalletChangesItem.kt @@ -138,7 +138,7 @@ private class EstimatedWalletChangesPreviewProviderTwoItems : tokenIconUrl = "https://tangem.com", ), WcEstimatedWalletChangeUM( - iconRes = R.drawable.ic_recieve_new_24, + iconRes = R.drawable.ic_receive_new_24, title = resourceReference(R.string.common_receive), description = "+ 1,131.46 MATIC", tokenIconUrl = "https://tangem.com", diff --git a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/ui/blockaid/WcTransactionCheckErrorItem.kt b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/ui/blockaid/WcTransactionCheckErrorItem.kt new file mode 100644 index 0000000000..a8431340b3 --- /dev/null +++ b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/ui/blockaid/WcTransactionCheckErrorItem.kt @@ -0,0 +1,49 @@ +package com.tangem.features.walletconnect.transaction.ui.blockaid + +import android.content.res.Configuration +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.tooling.preview.Devices +import androidx.compose.ui.tooling.preview.Preview +import com.tangem.core.ui.components.notifications.Notification +import com.tangem.core.ui.components.notifications.NotificationConfig +import com.tangem.core.ui.extensions.TextReference +import com.tangem.core.ui.extensions.resourceReference +import com.tangem.core.ui.res.TangemColorPalette +import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.res.TangemThemePreview +import com.tangem.features.walletconnect.impl.R + +@Composable +internal fun WcTransactionCheckErrorItem(notificationText: String, modifier: Modifier = Modifier) { + Notification( + modifier = modifier + .fillMaxWidth(), + config = NotificationConfig( + title = resourceReference(R.string.wc_malicious_transaction), + subtitle = TextReference.Str(notificationText), + iconResId = R.drawable.ic_alert_circle_24, + ), + containerColor = TangemColorPalette.Amaranth.copy(alpha = 0.1f), + titleColor = TangemTheme.colors.text.warning, + subtitleColor = TangemTheme.colors.text.primary1, + iconTint = TangemTheme.colors.icon.warning, + ) +} + +@Composable +@Preview(showBackground = true, device = Devices.PIXEL_7_PRO) +@Preview(showBackground = true, device = Devices.PIXEL_7_PRO, uiMode = Configuration.UI_MODE_NIGHT_YES) +private fun WcTransactionCheckErrorItemPreview() { + TangemThemePreview { + Box( + modifier = Modifier + .background(TangemTheme.colors.background.tertiary), + ) { + WcTransactionCheckErrorItem("The transaction approves erc20 tokens to a known malicious address") + } + } +} \ No newline at end of file From c38fa7ad19bdb20d8761a69faaec80c37d4ba2e0 Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 19 May 2025 15:10:32 +0300 Subject: [PATCH 055/165] Updated on 2026-08-14 --- .../tasks/visa/VisaCardActivationTask.kt | 59 ++---------------- .../visa/VisaCustomerWalletApproveTask.kt | 9 +-- .../tap/domain/visa/VisaCardScanHandler.kt | 62 ++----------------- .../com/tangem/domain/visa/error/VisaError.kt | 2 + 4 files changed, 15 insertions(+), 117 deletions(-) diff --git a/app/src/main/java/com/tangem/tap/domain/tasks/visa/VisaCardActivationTask.kt b/app/src/main/java/com/tangem/tap/domain/tasks/visa/VisaCardActivationTask.kt index dd4e607371..3b07d873a0 100644 --- a/app/src/main/java/com/tangem/tap/domain/tasks/visa/VisaCardActivationTask.kt +++ b/app/src/main/java/com/tangem/tap/domain/tasks/visa/VisaCardActivationTask.kt @@ -14,12 +14,10 @@ import com.tangem.common.extensions.toHexString import com.tangem.common.map import com.tangem.common.timemeasure.RealtimeMonotonicTimeSource import com.tangem.core.error.ext.tangemError -import com.tangem.crypto.hdWallet.bip32.ExtendedPublicKey import com.tangem.datasource.local.visa.VisaAuthTokenStorage import com.tangem.datasource.local.visa.VisaOTPStorage import com.tangem.datasource.local.visa.VisaOtpData import com.tangem.datasource.local.visa.hasSavedOTP -import com.tangem.domain.common.visa.VisaUtilities import com.tangem.domain.common.visa.VisaWalletPublicKeyUtility import com.tangem.domain.models.scan.CardDTO import com.tangem.domain.visa.error.VisaActivationError @@ -29,7 +27,6 @@ import com.tangem.domain.visa.repository.VisaActivationRepository import com.tangem.domain.visa.repository.VisaAuthRepository import com.tangem.operations.GenerateOTPCommand import com.tangem.operations.attestation.AttestCardKeyCommand -import com.tangem.operations.derivation.DeriveWalletPublicKeyTask import com.tangem.operations.pins.SetUserCodeCommand import com.tangem.operations.sign.SignHashCommand import com.tangem.operations.sign.SignHashResponse @@ -95,23 +92,7 @@ class VisaCardActivationTask @AssistedInject constructor( context.signAuthorizationChallenge(mode.authorizationChallenge) } is VisaCardActivationTaskMode.SignOnly -> { - val wallet = - card.wallets.firstOrNull { it.curve == EllipticCurve.Secp256k1 } - ?: return CompletionResult.Failure(TangemSdkError.MissingPreflightRead()) - - val derivedPublicKey = when (val deriveKeyResult = context.deriveKey(wallet.publicKey)) { - is CompletionResult.Failure -> { - return CompletionResult.Failure(deriveKeyResult.error) - } - is CompletionResult.Success -> { - deriveKeyResult.data - } - } - - context.signData( - mode.dataToSignByCardWallet, - derivedPublicKey, - ) + context.signData(mode.dataToSignByCardWallet) } } } @@ -164,16 +145,7 @@ class VisaCardActivationTask @AssistedInject constructor( card.wallets.firstOrNull { it.curve == EllipticCurve.Secp256k1 } ?: return CompletionResult.Failure(TangemSdkError.MissingPreflightRead()) - val derivedPublicKey = when (val deriveKeyResult = deriveKey(wallet.publicKey)) { - is CompletionResult.Failure -> { - return CompletionResult.Failure(deriveKeyResult.error) - } - is CompletionResult.Success -> { - deriveKeyResult.data - } - } - - val walletAddress = VisaWalletPublicKeyUtility.generateAddressOnSecp256k1(derivedPublicKey.publicKey) + val walletAddress = VisaWalletPublicKeyUtility.generateAddressOnSecp256k1(wallet.publicKey) .getOrElse { return CompletionResult.Failure(it.tangemError) } .value @@ -190,10 +162,7 @@ class VisaCardActivationTask @AssistedInject constructor( otpTaskDeferred.await() - signData( - dataToSign = dataToSign, - derivedPublicKey = derivedPublicKey, - ) + signData(dataToSign = dataToSign) } } @@ -294,7 +263,6 @@ class VisaCardActivationTask @AssistedInject constructor( private suspend fun SessionContext.signData( dataToSign: VisaDataToSignByCardWallet, - derivedPublicKey: ExtendedPublicKey, ): CompletionResult { val card = session.environment.card ?: return CompletionResult.Failure(TangemSdkError.MissingPreflightRead()) @@ -306,7 +274,6 @@ class VisaCardActivationTask @AssistedInject constructor( val task = SignHashCommand( hash = dataToSign.hashToSign.hexToBytes(), walletPublicKey = wallet.publicKey, - derivationPath = VisaUtilities.visaDefaultDerivationPath, ) val timedResult = RealtimeMonotonicTimeSource.measureTimedValue { @@ -325,7 +292,7 @@ class VisaCardActivationTask @AssistedInject constructor( handleSignedData( dataToSign = dataToSign, response = result.data, - derivedPublicKey = derivedPublicKey, + walletPublicKey = wallet.publicKey, ) } is CompletionResult.Failure -> { @@ -335,23 +302,9 @@ class VisaCardActivationTask @AssistedInject constructor( } } - private suspend fun SessionContext.deriveKey(publicKey: ByteArray): CompletionResult { - val derivationPath = VisaUtilities.visaDefaultDerivationPath - ?: return CompletionResult.Failure(VisaActivationError.FailedToCreateAddress.tangemError) - - val derivationTask = DeriveWalletPublicKeyTask(publicKey, derivationPath) - val derivationTaskResult = suspendCancellableCoroutine { continuation -> - derivationTask.run(session) { result -> - continuation.resume(result) - } - } - - return derivationTaskResult - } - private suspend fun SessionContext.handleSignedData( dataToSign: VisaDataToSignByCardWallet, - derivedPublicKey: ExtendedPublicKey, + walletPublicKey: ByteArray, response: SignHashResponse, ): CompletionResult { val otp = otpStorage.getOTP(cardId) ?: run { @@ -362,7 +315,7 @@ class VisaCardActivationTask @AssistedInject constructor( val rsvSignature = UnmarshalHelper.unmarshalSignatureExtended( signature = response.signature, hash = dataToSign.hashToSign.hexToBytes(), - publicKey = derivedPublicKey.publicKey.toDecompressedPublicKey(), + publicKey = walletPublicKey.toDecompressedPublicKey(), ).asRSVLegacyEVM().toHexString() val signedActivationData = dataToSign.sign( diff --git a/app/src/main/java/com/tangem/tap/domain/tasks/visa/VisaCustomerWalletApproveTask.kt b/app/src/main/java/com/tangem/tap/domain/tasks/visa/VisaCustomerWalletApproveTask.kt index 55dd835a3e..232724142a 100644 --- a/app/src/main/java/com/tangem/tap/domain/tasks/visa/VisaCustomerWalletApproveTask.kt +++ b/app/src/main/java/com/tangem/tap/domain/tasks/visa/VisaCustomerWalletApproveTask.kt @@ -40,17 +40,12 @@ class VisaCustomerWalletApproveTask( } if (VisaUtilities.isVisaCard(card.firmwareVersion.doubleValue, card.batchId)) { - // TODO TVF-21 - callback(CompletionResult.Failure(TangemSdkError.Underlying("Can't use Visa card for approve"))) + callback(CompletionResult.Failure(VisaActivationError.VisaCardForApproval.tangemError)) return } if (visaDataForApprove.customerWalletCardId != null && card.cardId != visaDataForApprove.customerWalletCardId) { - callback( - CompletionResult.Failure( - TangemSdkError.Underlying("Use tangem wallet specified during visa registration"), // TODO TVF-21 - ), - ) + callback(CompletionResult.Failure(VisaActivationError.CardIdNotMatched.tangemError)) return } diff --git a/app/src/main/java/com/tangem/tap/domain/visa/VisaCardScanHandler.kt b/app/src/main/java/com/tangem/tap/domain/visa/VisaCardScanHandler.kt index b83d23f8d9..e31948a49c 100644 --- a/app/src/main/java/com/tangem/tap/domain/visa/VisaCardScanHandler.kt +++ b/app/src/main/java/com/tangem/tap/domain/visa/VisaCardScanHandler.kt @@ -2,17 +2,13 @@ package com.tangem.tap.domain.visa import arrow.core.getOrElse import com.tangem.common.CompletionResult -import com.tangem.common.card.CardWallet import com.tangem.common.card.EllipticCurve import com.tangem.common.core.CardSession import com.tangem.common.core.TangemSdkError import com.tangem.common.extensions.hexToBytes import com.tangem.common.extensions.toHexString import com.tangem.core.error.ext.tangemError -import com.tangem.crypto.hdWallet.DerivationPath -import com.tangem.crypto.hdWallet.bip32.ExtendedPublicKey import com.tangem.datasource.local.visa.VisaAuthTokenStorage -import com.tangem.domain.common.visa.VisaUtilities import com.tangem.domain.common.visa.VisaWalletPublicKeyUtility import com.tangem.domain.visa.error.VisaActivationError import com.tangem.domain.visa.error.VisaAuthorizationAPIError @@ -22,7 +18,6 @@ import com.tangem.domain.visa.repository.VisaActivationRepository import com.tangem.domain.visa.repository.VisaAuthRepository import com.tangem.operations.attestation.AttestCardKeyCommand import com.tangem.operations.attestation.AttestCardKeyResponse -import com.tangem.operations.derivation.DeriveWalletPublicKeyTask import com.tangem.operations.sign.SignHashCommand import com.tangem.operations.sign.SignHashResponse import kotlinx.coroutines.suspendCancellableCoroutine @@ -61,54 +56,19 @@ internal class VisaCardScanHandler @Inject constructor( session = session, ) - val wallet = card.wallets.firstOrNull { it.curve == EllipticCurve.Secp256k1 } ?: run { + card.wallets.firstOrNull { it.curve == EllipticCurve.Secp256k1 } ?: run { val activationInput = VisaActivationInput(card.cardId, card.cardPublicKey.toHexString(), card.isAccessCodeSet) val activationStatus = VisaCardActivationStatus.NotStartedActivation(activationInput) return CompletionResult.Success(activationStatus) } - return context.deriveKey(wallet) - } - - private suspend fun SessionContext.deriveKey(wallet: CardWallet): CompletionResult { - val derivationPath = VisaUtilities.visaDefaultDerivationPath ?: run { - Timber.e("Failed to create derivation path while first scan") - - return CompletionResult.Failure(VisaCardScanError.FailedToCreateDerivationPath.tangemError) - } - - val derivationTask = DeriveWalletPublicKeyTask(wallet.publicKey, derivationPath) - val derivationTaskResult = suspendCancellableCoroutine { continuation -> - derivationTask.run(session) { result -> - continuation.resume(result) - } - } - return handleDerivationResponse(derivationTaskResult) - } - - private suspend fun SessionContext.handleDerivationResponse( - result: CompletionResult, - ): CompletionResult { - return when (result) { - is CompletionResult.Success -> { - Timber.i("Start task for loading challenge for Visa wallet") - handleWalletAuthorization() - } - is CompletionResult.Failure -> { - CompletionResult.Failure(result.error) - } - } + return context.handleWalletAuthorization() } private suspend fun SessionContext.handleWalletAuthorization(): CompletionResult { Timber.i("Started handling authorization using Visa wallet") - val derivationPath = VisaUtilities.visaDefaultDerivationPath ?: run { - Timber.e("Failed to create derivation path while handling wallet authorization") - return CompletionResult.Failure(VisaCardScanError.FailedToCreateDerivationPath.tangemError) - } - val card = session.environment.card ?: return CompletionResult.Failure(TangemSdkError.MissingPreflightRead()) val wallet = card.wallets.firstOrNull { it.curve == EllipticCurve.Secp256k1 } ?: run { @@ -116,12 +76,7 @@ internal class VisaCardScanHandler @Inject constructor( return CompletionResult.Failure(VisaCardScanError.FailedToFindWallet.tangemError) } - val extendedPublicKey = wallet.derivedKeys[derivationPath] ?: run { - Timber.e("Failed to find extended public key while handling wallet authorization") - return CompletionResult.Failure(VisaCardScanError.FailedToFindDerivedWalletKey.tangemError) - } - - val walletAddress = VisaWalletPublicKeyUtility.generateAddressOnSecp256k1(extendedPublicKey.publicKey) + val walletAddress = VisaWalletPublicKeyUtility.generateAddressOnSecp256k1(wallet.publicKey) .getOrElse { return CompletionResult.Failure(it.tangemError) } @@ -132,9 +87,7 @@ internal class VisaCardScanHandler @Inject constructor( error("sign and get specific error to switch to card_id flow") visaAuthRepository.getCardWalletAuthChallenge(cardWalletAddress = walletAddress.value) }.getOrElse { - Timber.i( - "Failed to get Access token for Wallet public key authoziation. Authorizing using Card Pub key", - ) + Timber.i("Failed to get Access token for Wallet public key authorization. Authorizing using Card Pub key") return handleCardAuthorization( cardWalletAddress = walletAddress.value, ) @@ -142,7 +95,6 @@ internal class VisaCardScanHandler @Inject constructor( val signChallengeResult = signChallengeWithWallet( publicKey = wallet.publicKey, - derivationPath = derivationPath, nonce = challengeResponse.challenge, ) @@ -169,9 +121,7 @@ internal class VisaCardScanHandler @Inject constructor( val authorizationTokensResponse = runCatching { visaAuthRepository.getAccessTokens(signedChallenge = signedChallenge) }.getOrElse { - Timber.i( - "Failed to get Access token for Wallet public key authoziation. Authorizing using Card Pub key", - ) + Timber.i("Failed to get Access token for Wallet public key authorization. Authorizing using Card Pub key") return handleCardAuthorization( cardWalletAddress = cardWalletAddress, ) @@ -271,13 +221,11 @@ internal class VisaCardScanHandler @Inject constructor( private suspend fun SessionContext.signChallengeWithWallet( publicKey: ByteArray, - derivationPath: DerivationPath, nonce: String, ): CompletionResult { val signHashCommand = SignHashCommand( hash = nonce.hexToBytes(), walletPublicKey = publicKey, - derivationPath = derivationPath, ) return suspendCancellableCoroutine { signHashCommand.run(session) { result -> diff --git a/domain/visa/models/src/main/kotlin/com/tangem/domain/visa/error/VisaError.kt b/domain/visa/models/src/main/kotlin/com/tangem/domain/visa/error/VisaError.kt index bf9bd888dd..6ddfd6c0d7 100644 --- a/domain/visa/models/src/main/kotlin/com/tangem/domain/visa/error/VisaError.kt +++ b/domain/visa/models/src/main/kotlin/com/tangem/domain/visa/error/VisaError.kt @@ -45,6 +45,8 @@ enum class VisaActivationError( AddressNotMatched(104003008), InconsistentRemoteState(104003009), FailedRemoteState(104003010), + VisaCardForApproval(104003011), + CardIdNotMatched(104003011), } object VisaAuthorizationAPIError : UniversalError { From 2689850df00af6f947fae022f2042fe182894093 Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 19 May 2025 17:16:17 +0500 Subject: [PATCH 056/165] Updated on 2026-08-14 --- .../tangem/features/nft/collections/ui/NFTCollectionsEmpty.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/features/nft/impl/src/main/kotlin/com/tangem/features/nft/collections/ui/NFTCollectionsEmpty.kt b/features/nft/impl/src/main/kotlin/com/tangem/features/nft/collections/ui/NFTCollectionsEmpty.kt index 1566f4d125..a5941e042d 100644 --- a/features/nft/impl/src/main/kotlin/com/tangem/features/nft/collections/ui/NFTCollectionsEmpty.kt +++ b/features/nft/impl/src/main/kotlin/com/tangem/features/nft/collections/ui/NFTCollectionsEmpty.kt @@ -55,7 +55,7 @@ internal fun NFTCollectionsEmpty(state: NFTCollectionsUM.Empty, modifier: Modifi PrimaryButton( modifier = Modifier .padding(top = TangemTheme.dimens.spacing48) - .wrapContentWidth(), + .widthIn(min = TangemTheme.dimens.size158), text = stringResourceSafe(R.string.nft_collections_receive), onClick = state.onReceiveClick, ) From c8e19f4cd6cdf3aa32921d7b316e767b2736f5f8 Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 19 May 2025 17:16:47 +0500 Subject: [PATCH 057/165] Updated on 2026-08-14 --- .../tangem/tap/di/domain/NFTDomainModule.kt | 9 +++++ .../ObserveAndClearNFTCacheIfNeedUseCase.kt | 38 +++++++++++++++++++ .../wallet/child/wallet/model/WalletModel.kt | 11 ++++++ .../implementors/MultiWalletContentLoader.kt | 3 ++ .../MultiWalletContentLoaderFactory.kt | 3 ++ .../subscribers/WalletNFTListSubscriber.kt | 15 +++++--- 6 files changed, 74 insertions(+), 5 deletions(-) create mode 100644 domain/nft/src/main/kotlin/com/tangem/domain/nft/ObserveAndClearNFTCacheIfNeedUseCase.kt diff --git a/app/src/main/java/com/tangem/tap/di/domain/NFTDomainModule.kt b/app/src/main/java/com/tangem/tap/di/domain/NFTDomainModule.kt index 26161f3d76..07c16b3c3f 100644 --- a/app/src/main/java/com/tangem/tap/di/domain/NFTDomainModule.kt +++ b/app/src/main/java/com/tangem/tap/di/domain/NFTDomainModule.kt @@ -127,4 +127,13 @@ internal object NFTDomainModule { fun provideGetWalletNFTEnabledUseCase(walletsRepository: WalletsRepository): GetWalletNFTEnabledUseCase { return GetWalletNFTEnabledUseCase(walletsRepository) } + + @Provides + @Singleton + fun provideClearNFTCacheUseCase( + nftRepository: NFTRepository, + currenciesRepository: CurrenciesRepository, + ): ObserveAndClearNFTCacheIfNeedUseCase { + return ObserveAndClearNFTCacheIfNeedUseCase(nftRepository, currenciesRepository) + } } \ No newline at end of file diff --git a/domain/nft/src/main/kotlin/com/tangem/domain/nft/ObserveAndClearNFTCacheIfNeedUseCase.kt b/domain/nft/src/main/kotlin/com/tangem/domain/nft/ObserveAndClearNFTCacheIfNeedUseCase.kt new file mode 100644 index 0000000000..dec9562a2a --- /dev/null +++ b/domain/nft/src/main/kotlin/com/tangem/domain/nft/ObserveAndClearNFTCacheIfNeedUseCase.kt @@ -0,0 +1,38 @@ +package com.tangem.domain.nft + +import com.tangem.domain.nft.repository.NFTRepository +import com.tangem.domain.tokens.model.CryptoCurrency +import com.tangem.domain.tokens.model.Network +import com.tangem.domain.tokens.repository.CurrenciesRepository +import com.tangem.domain.wallets.models.UserWalletId +import kotlinx.coroutines.flow.* + +class ObserveAndClearNFTCacheIfNeedUseCase( + private val nftRepository: NFTRepository, + private val currenciesRepository: CurrenciesRepository, +) { + operator fun invoke(userWalletId: UserWalletId): Flow> = currenciesRepository + .getWalletCurrenciesUpdates(userWalletId) + .map { it.map(CryptoCurrency::network) } + .mapDiff { old, new -> + // calculate networks sets difference to determine which networks were removed + old.toSet() - new.toSet() + } + .distinctUntilChanged() + .onEach { removedNetworks -> + if (removedNetworks.isNotEmpty()) { + nftRepository.clearCache(userWalletId, removedNetworks.toList()) + } + } + + private fun Flow.mapDiff(diff: (old: T, new: T) -> R): Flow = flow { + var previous: T? = null + collect { current -> + val prev = previous + if (prev != null) { + emit(diff(prev, current)) + } + previous = current + } + } +} \ No newline at end of file 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 1fe0628b59..2360c86709 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 @@ -16,6 +16,7 @@ import com.tangem.core.deeplink.DeepLinksRegistry import com.tangem.core.deeplink.global.ReferralDeepLink import com.tangem.domain.balancehiding.GetBalanceHidingSettingsUseCase import com.tangem.domain.common.util.cardTypesResolver +import com.tangem.domain.nft.ObserveAndClearNFTCacheIfNeedUseCase import com.tangem.domain.settings.* import com.tangem.domain.tokens.FetchCurrencyStatusUseCase import com.tangem.domain.tokens.RefreshMultiCurrencyWalletQuotesUseCase @@ -84,6 +85,7 @@ internal class WalletModel @Inject constructor( private val fetchCurrencyStatusUseCase: FetchCurrencyStatusUseCase, private val appRouter: AppRouter, private val routingFeatureToggle: RoutingFeatureToggle, + private val observeAndClearNFTCacheIfNeedUseCase: ObserveAndClearNFTCacheIfNeedUseCase, val screenLifecycleProvider: ScreenLifecycleProvider, val innerWalletRouter: InnerWalletRouter, ) : Model() { @@ -94,6 +96,7 @@ internal class WalletModel @Inject constructor( private val walletsUpdateJobHolder = JobHolder() private val refreshWalletJobHolder = JobHolder() private val expressStatusJobHolder = JobHolder() + private val clearNFTCacheJobHolder = JobHolder() private var needToRefreshWallet = false private var expressTxStatusTaskScheduler = SingleTaskScheduler() @@ -224,6 +227,7 @@ internal class WalletModel @Inject constructor( } subscribeOnExpressTransactionsUpdates(selectedWallet) subscribeToScreenBackgroundState(selectedWallet) + observeAndClearNFTCacheIfNeedUseCase(selectedWallet) } .flowOn(dispatchers.main) .launchIn(modelScope) @@ -283,6 +287,13 @@ internal class WalletModel @Inject constructor( ) } + private fun observeAndClearNFTCacheIfNeedUseCase(selectedWallet: UserWallet) { + observeAndClearNFTCacheIfNeedUseCase + .invoke(selectedWallet.walletId) + .launchIn(modelScope) + .saveIn(clearNFTCacheJobHolder) + } + private fun needToRefreshTimer() { modelScope.launch { delay(REFRESH_WALLET_BACKGROUND_TIMER_MILLIS) diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/loaders/implementors/MultiWalletContentLoader.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/loaders/implementors/MultiWalletContentLoader.kt index 62325f8bfe..007c74fcc6 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/loaders/implementors/MultiWalletContentLoader.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/loaders/implementors/MultiWalletContentLoader.kt @@ -8,6 +8,7 @@ import com.tangem.domain.nft.GetNFTCollectionsUseCase import com.tangem.domain.promo.GetStoryContentUseCase import com.tangem.domain.tokens.ApplyTokenListSortingUseCase import com.tangem.domain.tokens.RunPolkadotAccountHealthCheckUseCase +import com.tangem.domain.tokens.repository.CurrenciesRepository import com.tangem.domain.wallets.models.UserWallet import com.tangem.domain.wallets.repository.WalletsRepository import com.tangem.domain.wallets.usecase.ShouldSaveUserWalletsUseCase @@ -47,6 +48,7 @@ internal class MultiWalletContentLoader( private val deepLinksRegistry: DeepLinksRegistry, private val nftFeatureToggles: NFTFeatureToggles, private val walletsRepository: WalletsRepository, + private val currenciesRepository: CurrenciesRepository, private val routingFeatureToggle: RoutingFeatureToggle, ) : WalletContentLoader(id = userWallet.walletId) { @@ -72,6 +74,7 @@ internal class MultiWalletContentLoader( stateHolder = stateHolder, walletsRepository = walletsRepository, clickIntents = clickIntents, + currenciesRepository = currenciesRepository, ).let(::add) } MultiWalletWarningsSubscriber( diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/loaders/implementors/MultiWalletContentLoaderFactory.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/loaders/implementors/MultiWalletContentLoaderFactory.kt index 44aa146b20..9cdb3f5767 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/loaders/implementors/MultiWalletContentLoaderFactory.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/loaders/implementors/MultiWalletContentLoaderFactory.kt @@ -8,6 +8,7 @@ import com.tangem.domain.nft.GetNFTCollectionsUseCase import com.tangem.domain.promo.GetStoryContentUseCase import com.tangem.domain.tokens.ApplyTokenListSortingUseCase import com.tangem.domain.tokens.RunPolkadotAccountHealthCheckUseCase +import com.tangem.domain.tokens.repository.CurrenciesRepository import com.tangem.domain.wallets.models.UserWallet import com.tangem.domain.wallets.repository.WalletsRepository import com.tangem.domain.wallets.usecase.ShouldSaveUserWalletsUseCase @@ -42,6 +43,7 @@ internal class MultiWalletContentLoaderFactory @Inject constructor( private val walletsRepository: WalletsRepository, private val getNFTCollectionsUseCase: GetNFTCollectionsUseCase, private val routingFeatureToggle: RoutingFeatureToggle, + private val currenciesRepository: CurrenciesRepository, ) { fun create(userWallet: UserWallet, clickIntents: WalletClickIntents): WalletContentLoader { @@ -65,6 +67,7 @@ internal class MultiWalletContentLoaderFactory @Inject constructor( walletsRepository = walletsRepository, getNFTCollectionsUseCase = getNFTCollectionsUseCase, routingFeatureToggle = routingFeatureToggle, + currenciesRepository = currenciesRepository, ) } } \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/WalletNFTListSubscriber.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/WalletNFTListSubscriber.kt index 57ed2a8b6d..329dc0268c 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/WalletNFTListSubscriber.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/WalletNFTListSubscriber.kt @@ -1,6 +1,7 @@ package com.tangem.feature.wallet.presentation.wallet.subscribers import com.tangem.domain.nft.GetNFTCollectionsUseCase +import com.tangem.domain.tokens.repository.CurrenciesRepository import com.tangem.domain.wallets.models.UserWallet import com.tangem.domain.wallets.repository.WalletsRepository import com.tangem.feature.wallet.child.wallet.model.intents.WalletClickIntents @@ -15,17 +16,21 @@ internal class WalletNFTListSubscriber( private val userWallet: UserWallet, private val stateHolder: WalletStateController, private val walletsRepository: WalletsRepository, + private val currenciesRepository: CurrenciesRepository, private val getNFTCollectionsUseCase: GetNFTCollectionsUseCase, private val clickIntents: WalletClickIntents, ) : WalletSubscriber() { @OptIn(ExperimentalCoroutinesApi::class) - override fun create(coroutineScope: CoroutineScope): Flow<*> = walletsRepository - .nftEnabledStatus(userWallet.walletId) + override fun create(coroutineScope: CoroutineScope): Flow<*> = combine( + walletsRepository.nftEnabledStatus(userWallet.walletId), + currenciesRepository.getWalletCurrenciesUpdates(userWallet.walletId), + ) { nftEnabled, currencies -> nftEnabled to currencies } .distinctUntilChanged() - .flatMapLatest { nftEnabled -> - // if NFT is enabled for this wallet, then start observing changes from store and apply transformer if need - if (nftEnabled) { + .flatMapLatest { (nftEnabled, currencies) -> + // 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(userWallet.walletId) .shareIn( scope = coroutineScope, From d1d3e11361ff18bf393bd1df6888c52f0b28314b Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 19 May 2025 16:51:09 +0300 Subject: [PATCH 058/165] Updated on 2026-08-14 --- .gitignore | 3 +++ app/src/main/assets/tangem-app-config | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index 74ed0fd34b..6106311a37 100644 --- a/.gitignore +++ b/.gitignore @@ -9,6 +9,9 @@ local.properties # Google services +app/src/debug/google-services.json +app/src/internal/google-services.json +app/src/external/google-services.json # Gradle generated files .gradle diff --git a/app/src/main/assets/tangem-app-config b/app/src/main/assets/tangem-app-config index 0828475441..4ec5de6632 160000 --- a/app/src/main/assets/tangem-app-config +++ b/app/src/main/assets/tangem-app-config @@ -1 +1 @@ -Subproject commit 08284754412397ae4ad9eb5b0073d7d7db1e1059 +Subproject commit 4ec5de66321afa82c04104746015ea9e6bc9fe64 From c2c536f66903c33fcc2d4d3586ac2dd832dad8ab Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 19 May 2025 18:33:53 +0300 Subject: [PATCH 059/165] Updated on 2026-08-14 --- fastlane/Fastfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/fastlane/Fastfile b/fastlane/Fastfile index 574898644a..ae8a2a0445 100644 --- a/fastlane/Fastfile +++ b/fastlane/Fastfile @@ -18,7 +18,7 @@ default_platform(:android) platform :android do before_all do |lane, options| - FileUtils.cp("../app/src/main/assets/tangem-app-config/android/google-services.json", "../app") + FileUtils.cp("../app/src/main/assets/tangem-app-config/android/google-services/release/google-services.json", "../app") end desc "Run detekt" From 37ad3e68468f89b8f08f48bf92e006866ceb0763 Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 19 May 2025 18:49:03 +0300 Subject: [PATCH 060/165] Updated on 2026-08-14 --- fastlane/Fastfile | 57 +++-------------------------------------------- 1 file changed, 3 insertions(+), 54 deletions(-) diff --git a/fastlane/Fastfile b/fastlane/Fastfile index ae8a2a0445..950482041e 100644 --- a/fastlane/Fastfile +++ b/fastlane/Fastfile @@ -17,10 +17,6 @@ default_platform(:android) platform :android do - before_all do |lane, options| - FileUtils.cp("../app/src/main/assets/tangem-app-config/android/google-services/release/google-services.json", "../app") - end - desc "Run detekt" lane :detekt do FileUtils.cp("../ci_resources/tests_8gb_ram_ci_gradle.properties", "../gradle.properties") @@ -30,56 +26,16 @@ platform :android do desc "Run tests" lane :test do + FileUtils.cp("../app/src/main/assets/tangem-app-config/android/google-services/dev/google-services.json", "../app") FileUtils.cp("../ci_resources/tests_8gb_ram_ci_gradle.properties", "../gradle.properties") puts File.read("../gradle.properties") gradle(task: "testDebugUnitTest") end - desc "Build a signed release APK" - lane :release do |options| # TODO: remove after fully moving from old CI - gradle( - task: "clean assemble", - build_type: "Release", - properties: { - "android.injected.signing.store.file" => options[:keystore], - "android.injected.signing.store.password" => options[:store_password], - "android.injected.signing.key.alias" => options[:key_alias], - "android.injected.signing.key.password" => options[:key_password], - }) - end - - desc "Build external and release APKs" - lane :build do |options| # TODO: remove after fully moving from old CI - gradle(task: 'clean') - - gradle( - task: "bundle", - build_type: "Release", - properties: { - 'versionCode' => options[:versionCode], - 'versionName' => options[:versionName], - "android.injected.signing.store.file" => options[:keystore], - "android.injected.signing.store.password" => options[:store_password], - "android.injected.signing.key.alias" => options[:key_alias], - "android.injected.signing.key.password" => options[:key_password], - } - ) - gradle( - task: "assemble", - build_type: "Release", - properties: { - 'versionCode' => options[:versionCode], - 'versionName' => options[:versionName], - "android.injected.signing.store.file" => options[:keystore], - "android.injected.signing.store.password" => options[:store_password], - "android.injected.signing.key.alias" => options[:key_alias], - "android.injected.signing.key.password" => options[:key_password], - } - ) - end desc "Build release AAB and APK" lane :buildRelease do |options| + FileUtils.cp("../app/src/main/assets/tangem-app-config/android/google-services/release/google-services.json", "../app") FileUtils.cp("../ci_resources/build_ci_gradle.properties", "../gradle.properties") puts File.read("../gradle.properties") @@ -109,17 +65,10 @@ platform :android do ) end - desc "Submit a new Beta Build to Firebase App Distribution" - lane :beta do |options| # TODO: remove after fully moving from old CI - firebase_app_distribution( - app: options[:app_id], - apk_path: options[:apk_path], - groups: options[:groups]) - end - desc "Build internal APK Firebase App Distribution" lane :buildInternal do |options| + FileUtils.cp("../app/src/main/assets/tangem-app-config/android/google-services/dev/google-services.json", "../app") FileUtils.cp("../ci_resources/build_ci_gradle.properties", "../gradle.properties") puts File.read("../gradle.properties") From db1d205bae0383490baebbaa49c511e48ea7328d Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 19 May 2025 22:49:30 +0700 Subject: [PATCH 061/165] Updated on 2026-08-14 --- .../di/WalletConnectDataModule.kt | 2 + .../ethereum/WcEthAddNetworkUseCase.kt | 42 +++++++++++++++++++ .../network/ethereum/WcEthNetwork.kt | 19 ++++++++- .../walletconnect/model/WcEthAddChain.kt | 10 +++++ .../domain/walletconnect/model/WcEthMethod.kt | 7 ++++ .../walletconnect/model/WcMethodName.kt | 1 + .../usecase/method/WcAddNetworkUseCase.kt | 11 +++++ .../connections/routing/WcRoutingModel.kt | 1 + 8 files changed, 91 insertions(+), 2 deletions(-) create mode 100644 data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/network/ethereum/WcEthAddNetworkUseCase.kt create mode 100644 domain/wallet-connect/models/src/main/java/com/tangem/domain/walletconnect/model/WcEthAddChain.kt create mode 100644 domain/wallet-connect/src/main/kotlin/com/tangem/domain/walletconnect/usecase/method/WcAddNetworkUseCase.kt diff --git a/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/di/WalletConnectDataModule.kt b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/di/WalletConnectDataModule.kt index f1fe67b26a..2eb8bb69d2 100644 --- a/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/di/WalletConnectDataModule.kt +++ b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/di/WalletConnectDataModule.kt @@ -128,11 +128,13 @@ internal object WalletConnectDataModule { sessionsManager: WcSessionsManager, factories: WcEthNetwork.Factories, namespaceConverter: WcEthNetwork.NamespaceConverter, + walletManagersFacade: WalletManagersFacade, ): WcEthNetwork = WcEthNetwork( moshi = moshi, namespaceConverter = namespaceConverter, sessionsManager = sessionsManager, factories = factories, + walletManagersFacade = walletManagersFacade, ) @Provides diff --git a/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/network/ethereum/WcEthAddNetworkUseCase.kt b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/network/ethereum/WcEthAddNetworkUseCase.kt new file mode 100644 index 0000000000..d94d4eba8f --- /dev/null +++ b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/network/ethereum/WcEthAddNetworkUseCase.kt @@ -0,0 +1,42 @@ +package com.tangem.data.walletconnect.network.ethereum + +import arrow.core.Either +import com.tangem.data.walletconnect.respond.WcRespondService +import com.tangem.data.walletconnect.sign.WcMethodUseCaseContext +import com.tangem.domain.tokens.model.Network +import com.tangem.domain.walletconnect.model.WcEthMethod +import com.tangem.domain.walletconnect.model.WcSession +import com.tangem.domain.walletconnect.model.sdkcopy.WcSdkSessionRequest +import com.tangem.domain.walletconnect.usecase.method.WcAddNetworkUseCase +import dagger.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject + +internal class WcEthAddNetworkUseCase @AssistedInject constructor( + private val respondService: WcRespondService, + @Assisted val context: WcMethodUseCaseContext, + @Assisted override val method: WcEthMethod.AddEthereumChain, +) : WcAddNetworkUseCase { + + override val session: WcSession + get() = context.session + override val rawSdkRequest: WcSdkSessionRequest + get() = context.rawSdkRequest + override val network: Network + get() = context.network + override val walletAddress: String + get() = context.accountAddress + + override suspend fun approve(): Either { + return respondService.respond(rawSdkRequest, "") + } + + override fun reject() { + respondService.rejectRequestNonBlock(rawSdkRequest) + } + + @AssistedFactory + interface Factory { + fun create(context: WcMethodUseCaseContext, method: WcEthMethod.AddEthereumChain): WcEthAddNetworkUseCase + } +} \ No newline at end of file diff --git a/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/network/ethereum/WcEthNetwork.kt b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/network/ethereum/WcEthNetwork.kt index 5dda26b898..2ae96265ea 100644 --- a/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/network/ethereum/WcEthNetwork.kt +++ b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/network/ethereum/WcEthNetwork.kt @@ -10,6 +10,7 @@ import com.tangem.data.walletconnect.request.WcRequestToUseCaseConverter.Compani import com.tangem.data.walletconnect.sign.WcMethodUseCaseContext import com.tangem.data.walletconnect.utils.WcNamespaceConverter import com.tangem.domain.tokens.model.Network +import com.tangem.domain.walletconnect.model.WcEthAddChain import com.tangem.domain.walletconnect.model.WcEthMethod import com.tangem.domain.walletconnect.model.WcEthMethodName import com.tangem.domain.walletconnect.model.WcEthSignTypedDataParams @@ -17,6 +18,7 @@ import com.tangem.domain.walletconnect.model.WcEthTransactionParams import com.tangem.domain.walletconnect.model.sdkcopy.WcSdkSessionRequest import com.tangem.domain.walletconnect.repository.WcSessionsManager import com.tangem.domain.walletconnect.usecase.method.WcMethodUseCase +import com.tangem.domain.walletmanager.WalletManagersFacade import com.tangem.domain.wallets.models.UserWallet import jakarta.inject.Inject @@ -25,6 +27,7 @@ internal class WcEthNetwork( private val sessionsManager: WcSessionsManager, private val factories: Factories, private val namespaceConverter: NamespaceConverter, + private val walletManagersFacade: WalletManagersFacade, ) : WcRequestToUseCaseConverter { override fun toWcMethodName(request: WcSdkSessionRequest): WcEthMethodName? { @@ -33,16 +36,19 @@ internal class WcEthNetwork( return name } + @Suppress("CyclomaticComplexMethod") override suspend fun toUseCase(request: WcSdkSessionRequest): WcMethodUseCase? { val name = toWcMethodName(request) ?: return null - val method: WcEthMethod = name.toMethod(request) ?: return null val session = sessionsManager.findSessionByTopic(request.topic) ?: return null + val method: WcEthMethod = name.toMethod(request, session.wallet) ?: return null val network = namespaceConverter.toNetwork(request.chainId.orEmpty(), session.wallet) ?: return null + val walletManagerAddress = walletManagersFacade.getDefaultAddress(session.wallet.walletId, network).orEmpty() val accountAddress = when (method) { is WcEthMethod.MessageSign -> method.account is WcEthMethod.SendTransaction -> method.transaction.from is WcEthMethod.SignTransaction -> method.transaction.from is WcEthMethod.SignTypedData -> method.account + is WcEthMethod.AddEthereumChain -> walletManagerAddress } val context = WcMethodUseCaseContext( session = session, @@ -55,10 +61,11 @@ internal class WcEthNetwork( is WcEthMethod.SendTransaction -> factories.sendTransaction.create(context, method) is WcEthMethod.SignTransaction -> factories.signTransaction.create(context, method) is WcEthMethod.SignTypedData -> factories.signTypedData.create(context, method) + is WcEthMethod.AddEthereumChain -> factories.addNetwork.create(context, method) } } - private fun WcEthMethodName.toMethod(request: WcSdkSessionRequest): WcEthMethod? { + private fun WcEthMethodName.toMethod(request: WcSdkSessionRequest, wallet: UserWallet): WcEthMethod? { val rawParams = request.request.params return when (this) { WcEthMethodName.EthSign, @@ -78,6 +85,13 @@ internal class WcEthNetwork( WcEthMethod.SendTransaction(transaction = it) } } + WcEthMethodName.AddEthereumChain -> moshi.fromJson>(rawParams) + ?.firstOrNull() + ?.let { + val newNetwork = namespaceConverter + .toNetwork(it.chainId, wallet) ?: return null + WcEthMethod.AddEthereumChain(rawChain = it, network = newNetwork) + } } } @@ -130,5 +144,6 @@ internal class WcEthNetwork( val signTypedData: WcEthSignTypedDataUseCase.Factory, val sendTransaction: WcEthSendTransactionUseCase.Factory, val signTransaction: WcEthSignTransactionUseCase.Factory, + val addNetwork: WcEthAddNetworkUseCase.Factory, ) } \ No newline at end of file diff --git a/domain/wallet-connect/models/src/main/java/com/tangem/domain/walletconnect/model/WcEthAddChain.kt b/domain/wallet-connect/models/src/main/java/com/tangem/domain/walletconnect/model/WcEthAddChain.kt new file mode 100644 index 0000000000..9daf8154a1 --- /dev/null +++ b/domain/wallet-connect/models/src/main/java/com/tangem/domain/walletconnect/model/WcEthAddChain.kt @@ -0,0 +1,10 @@ +package com.tangem.domain.walletconnect.model + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +@JsonClass(generateAdapter = true) +data class WcEthAddChain( + @Json(name = "chainId") + val chainId: String, +) \ No newline at end of file diff --git a/domain/wallet-connect/models/src/main/java/com/tangem/domain/walletconnect/model/WcEthMethod.kt b/domain/wallet-connect/models/src/main/java/com/tangem/domain/walletconnect/model/WcEthMethod.kt index 8f85b4063f..e72a35c68c 100644 --- a/domain/wallet-connect/models/src/main/java/com/tangem/domain/walletconnect/model/WcEthMethod.kt +++ b/domain/wallet-connect/models/src/main/java/com/tangem/domain/walletconnect/model/WcEthMethod.kt @@ -1,5 +1,7 @@ package com.tangem.domain.walletconnect.model +import com.tangem.domain.tokens.model.Network + sealed interface WcEthMethod : WcMethod { data class MessageSign( @@ -23,4 +25,9 @@ sealed interface WcEthMethod : WcMethod { data class SignTransaction( val transaction: WcEthTransactionParams, ) : WcEthMethod + + data class AddEthereumChain( + val rawChain: WcEthAddChain, + val network: Network, + ) : WcEthMethod } \ No newline at end of file diff --git a/domain/wallet-connect/models/src/main/java/com/tangem/domain/walletconnect/model/WcMethodName.kt b/domain/wallet-connect/models/src/main/java/com/tangem/domain/walletconnect/model/WcMethodName.kt index 7b557f0223..6ae4b431ac 100644 --- a/domain/wallet-connect/models/src/main/java/com/tangem/domain/walletconnect/model/WcMethodName.kt +++ b/domain/wallet-connect/models/src/main/java/com/tangem/domain/walletconnect/model/WcMethodName.kt @@ -13,6 +13,7 @@ enum class WcEthMethodName(override val raw: String) : WcMethodName { SignTypeDataV4("eth_signTypedData_v4"), SignTransaction("eth_signTransaction"), SendTransaction("eth_sendTransaction"), + AddEthereumChain("wallet_addEthereumChain"), } enum class WcSolanaMethodName(override val raw: String) : WcMethodName { diff --git a/domain/wallet-connect/src/main/kotlin/com/tangem/domain/walletconnect/usecase/method/WcAddNetworkUseCase.kt b/domain/wallet-connect/src/main/kotlin/com/tangem/domain/walletconnect/usecase/method/WcAddNetworkUseCase.kt new file mode 100644 index 0000000000..b7794de457 --- /dev/null +++ b/domain/wallet-connect/src/main/kotlin/com/tangem/domain/walletconnect/usecase/method/WcAddNetworkUseCase.kt @@ -0,0 +1,11 @@ +package com.tangem.domain.walletconnect.usecase.method + +import arrow.core.Either + +interface WcAddNetworkUseCase : + WcMethodUseCase, + WcMethodContext { + + suspend fun approve(): Either + fun reject() +} \ No newline at end of file diff --git a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/routing/WcRoutingModel.kt b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/routing/WcRoutingModel.kt index ed054eb3dc..99eaa067eb 100644 --- a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/routing/WcRoutingModel.kt +++ b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/routing/WcRoutingModel.kt @@ -53,6 +53,7 @@ internal class WcRoutingModel @Inject constructor( WcEthMethodName.SendTransaction -> TODO() WcSolanaMethodName.SignTransaction -> TODO() WcSolanaMethodName.SendAllTransaction -> TODO() + WcEthMethodName.AddEthereumChain -> TODO() is WcMethodName.Unsupported -> TODO() } } From cb1ede0531cf759b9510248b652299f60edc08fb Mon Sep 17 00:00:00 2001 From: Tangem Date: Fri, 16 May 2025 12:25:37 +0500 Subject: [PATCH 062/165] Updated on 2026-08-14 --- .../deeplink/global/SellCurrencyDeepLink.kt | 1 + features/send-v2/api/build.gradle.kts | 3 + .../v2/api/deeplink/SellDeepLinkHandler.kt | 10 +++ .../v2/deeplink/DefaultSellDeepLinkHandler.kt | 84 +++++++++++++++++++ .../send/v2/deeplink/di/SendDeepLinkModule.kt | 18 ++++ 5 files changed, 116 insertions(+) create mode 100644 features/send-v2/api/src/main/java/com/tangem/features/send/v2/api/deeplink/SellDeepLinkHandler.kt create mode 100644 features/send-v2/impl/src/main/java/com/tangem/features/send/v2/deeplink/DefaultSellDeepLinkHandler.kt create mode 100644 features/send-v2/impl/src/main/java/com/tangem/features/send/v2/deeplink/di/SendDeepLinkModule.kt diff --git a/core/deep-links/global/src/main/kotlin/com/tangem/core/deeplink/global/SellCurrencyDeepLink.kt b/core/deep-links/global/src/main/kotlin/com/tangem/core/deeplink/global/SellCurrencyDeepLink.kt index fe8b56d0cd..c3ac0e7faa 100644 --- a/core/deep-links/global/src/main/kotlin/com/tangem/core/deeplink/global/SellCurrencyDeepLink.kt +++ b/core/deep-links/global/src/main/kotlin/com/tangem/core/deeplink/global/SellCurrencyDeepLink.kt @@ -2,6 +2,7 @@ package com.tangem.core.deeplink.global import com.tangem.core.deeplink.DeepLink +@Deprecated("Use SellDeepLinkHandler") class SellCurrencyDeepLink( val onReceive: (data: Data) -> Unit, shouldHandleDelayed: Boolean, diff --git a/features/send-v2/api/build.gradle.kts b/features/send-v2/api/build.gradle.kts index fce40fb61c..e6a4fc3942 100644 --- a/features/send-v2/api/build.gradle.kts +++ b/features/send-v2/api/build.gradle.kts @@ -17,4 +17,7 @@ dependencies { implementation(projects.domain.wallets.models) implementation(projects.domain.tokens.models) implementation(projects.domain.nft.models) + + /* Compose */ + implementation(deps.compose.runtime) } \ No newline at end of file diff --git a/features/send-v2/api/src/main/java/com/tangem/features/send/v2/api/deeplink/SellDeepLinkHandler.kt b/features/send-v2/api/src/main/java/com/tangem/features/send/v2/api/deeplink/SellDeepLinkHandler.kt new file mode 100644 index 0000000000..d645fff429 --- /dev/null +++ b/features/send-v2/api/src/main/java/com/tangem/features/send/v2/api/deeplink/SellDeepLinkHandler.kt @@ -0,0 +1,10 @@ +package com.tangem.features.send.v2.api.deeplink + +import kotlinx.coroutines.CoroutineScope + +interface SellDeepLinkHandler { + + interface Factory { + fun create(coroutineScope: CoroutineScope, params: Map): SellDeepLinkHandler + } +} \ No newline at end of file diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/deeplink/DefaultSellDeepLinkHandler.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/deeplink/DefaultSellDeepLinkHandler.kt new file mode 100644 index 0000000000..e86282c818 --- /dev/null +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/deeplink/DefaultSellDeepLinkHandler.kt @@ -0,0 +1,84 @@ +package com.tangem.features.send.v2.deeplink + +import arrow.core.getOrElse +import com.tangem.common.routing.AppRoute +import com.tangem.common.routing.AppRouter +import com.tangem.domain.tokens.GetCryptoCurrencyUseCase +import com.tangem.domain.wallets.usecase.GetSelectedWalletSyncUseCase +import com.tangem.features.send.v2.api.deeplink.SellDeepLinkHandler +import dagger.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.launch +import timber.log.Timber + +@Suppress("ComplexCondition") +internal class DefaultSellDeepLinkHandler @AssistedInject constructor( + @Assisted scope: CoroutineScope, + @Assisted params: Map, + appRouter: AppRouter, + getSelectedWalletSyncUseCase: GetSelectedWalletSyncUseCase, + getCryptoCurrencyUseCase: GetCryptoCurrencyUseCase, +) : SellDeepLinkHandler { + + init { + val currencyId = params[CURRENCY_ID_KEY] + val transactionId = params[TRANSACTION_ID_KEY] + val amount = params[AMOUNT_KEY] + val destinationAddress = params[DESTINATION_ADDRESS_KEY] + val memo = params[MEMO_KEY] + + // It is okay here, we are navigating from outside, and there is no other way to getting UserWallet + getSelectedWalletSyncUseCase() + .fold( + ifLeft = { + Timber.e("Error on getting user wallet: $it") + }, + ifRight = { userWallet -> + if (currencyId.isNullOrEmpty() || transactionId.isNullOrEmpty() || + amount.isNullOrEmpty() || destinationAddress.isNullOrEmpty() + ) { + Timber.e( + """ + Invalid parameters for SELL deeplink + |- Params: $params + """.trimIndent(), + ) + return@fold + } + + scope.launch { + val cryptoCurrency = getCryptoCurrencyUseCase(userWallet, currencyId).getOrElse { + Timber.e("Error on getting cryptoCurrency: $it") + return@launch + } + + appRouter.push( + AppRoute.Send( + currency = cryptoCurrency, + userWalletId = userWallet.walletId, + transactionId = transactionId, + destinationAddress = destinationAddress, + amount = amount, + tag = memo, + ), + ) + } + }, + ) + } + + @AssistedFactory + interface Factory : SellDeepLinkHandler.Factory { + override fun create(coroutineScope: CoroutineScope, params: Map): DefaultSellDeepLinkHandler + } + + private companion object { + const val TRANSACTION_ID_KEY = "transactionId" + const val CURRENCY_ID_KEY = "currency_id" + const val AMOUNT_KEY = "baseCurrencyAmount" + const val DESTINATION_ADDRESS_KEY = "depositWalletAddress" + const val MEMO_KEY = "depositWalletAddressTag" + } +} \ No newline at end of file diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/deeplink/di/SendDeepLinkModule.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/deeplink/di/SendDeepLinkModule.kt new file mode 100644 index 0000000000..6cd2bca80d --- /dev/null +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/deeplink/di/SendDeepLinkModule.kt @@ -0,0 +1,18 @@ +package com.tangem.features.send.v2.deeplink.di + +import com.tangem.features.send.v2.api.deeplink.SellDeepLinkHandler +import com.tangem.features.send.v2.deeplink.DefaultSellDeepLinkHandler +import dagger.Binds +import dagger.Module +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent +import javax.inject.Singleton + +@Module +@InstallIn(SingletonComponent::class) +internal interface SendDeepLinkModule { + + @Binds + @Singleton + fun bindFactory(impl: DefaultSellDeepLinkHandler.Factory): SellDeepLinkHandler.Factory +} \ No newline at end of file From e70595047c06f7412a8c5738331f113024164992 Mon Sep 17 00:00:00 2001 From: Tangem Date: Fri, 16 May 2025 12:25:41 +0500 Subject: [PATCH 063/165] Updated on 2026-08-14 --- .../deeplink/global/BuyCurrencyDeepLink.kt | 1 + .../onramp/deeplink/BuyDeepLinkHandler.kt | 10 ++++ .../deeplink/DefaultBuyDeepLinkHandler.kt | 48 +++++++++++++++++++ .../deeplink/di/OnrampDeeplinkModule.kt | 7 ++- 4 files changed, 64 insertions(+), 2 deletions(-) create mode 100644 features/onramp/api/src/main/kotlin/com/tangem/features/onramp/deeplink/BuyDeepLinkHandler.kt create mode 100644 features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/deeplink/DefaultBuyDeepLinkHandler.kt diff --git a/core/deep-links/global/src/main/kotlin/com/tangem/core/deeplink/global/BuyCurrencyDeepLink.kt b/core/deep-links/global/src/main/kotlin/com/tangem/core/deeplink/global/BuyCurrencyDeepLink.kt index 90f79f358c..ff3cdaf90f 100644 --- a/core/deep-links/global/src/main/kotlin/com/tangem/core/deeplink/global/BuyCurrencyDeepLink.kt +++ b/core/deep-links/global/src/main/kotlin/com/tangem/core/deeplink/global/BuyCurrencyDeepLink.kt @@ -2,6 +2,7 @@ package com.tangem.core.deeplink.global import com.tangem.core.deeplink.DeepLink +@Deprecated("Use BuyDeepLinkHandler") class BuyCurrencyDeepLink( val onReceive: () -> Unit, ) : DeepLink() { diff --git a/features/onramp/api/src/main/kotlin/com/tangem/features/onramp/deeplink/BuyDeepLinkHandler.kt b/features/onramp/api/src/main/kotlin/com/tangem/features/onramp/deeplink/BuyDeepLinkHandler.kt new file mode 100644 index 0000000000..75c2d61f29 --- /dev/null +++ b/features/onramp/api/src/main/kotlin/com/tangem/features/onramp/deeplink/BuyDeepLinkHandler.kt @@ -0,0 +1,10 @@ +package com.tangem.features.onramp.deeplink + +import kotlinx.coroutines.CoroutineScope + +interface BuyDeepLinkHandler { + + interface Factory { + fun create(coroutineScope: CoroutineScope): BuyDeepLinkHandler + } +} \ No newline at end of file diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/deeplink/DefaultBuyDeepLinkHandler.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/deeplink/DefaultBuyDeepLinkHandler.kt new file mode 100644 index 0000000000..07c655873a --- /dev/null +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/deeplink/DefaultBuyDeepLinkHandler.kt @@ -0,0 +1,48 @@ +package com.tangem.features.onramp.deeplink + +import arrow.core.getOrElse +import com.tangem.core.analytics.api.AnalyticsEventHandler +import com.tangem.domain.tokens.GetCryptoCurrencyUseCase +import com.tangem.domain.tokens.model.analytics.TokenScreenAnalyticsEvent +import com.tangem.domain.wallets.usecase.GetSelectedWalletSyncUseCase +import com.tangem.features.onramp.OnrampFeatureToggles +import dagger.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.launch +import timber.log.Timber + +internal class DefaultBuyDeepLinkHandler @AssistedInject constructor( + @Assisted scope: CoroutineScope, + onrampFeatureToggles: OnrampFeatureToggles, + getSelectedWalletSyncUseCase: GetSelectedWalletSyncUseCase, + getCryptoCurrencyUseCase: GetCryptoCurrencyUseCase, + analyticsEventHandler: AnalyticsEventHandler, +) : BuyDeepLinkHandler { + + init { + // It is okay here, we are navigating from outside, and there is no other way to getting UserWallet + getSelectedWalletSyncUseCase().fold( + ifLeft = { + Timber.e("Error on getting user wallet: $it") + }, + ifRight = { userWallet -> + if (!onrampFeatureToggles.isFeatureEnabled && !userWallet.isMultiCurrency) { + scope.launch { + val cryptoCurrency = getCryptoCurrencyUseCase(userWallet.walletId).getOrElse { + Timber.e("Error on getting cryptoCurrency: $it") + return@launch + } + analyticsEventHandler.send(TokenScreenAnalyticsEvent.Bought(cryptoCurrency.symbol)) + } + } + }, + ) + } + + @AssistedFactory + interface Factory : BuyDeepLinkHandler.Factory { + override fun create(coroutineScope: CoroutineScope): DefaultBuyDeepLinkHandler + } +} \ No newline at end of file diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/deeplink/di/OnrampDeeplinkModule.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/deeplink/di/OnrampDeeplinkModule.kt index fc55367fe3..418baeccc3 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/deeplink/di/OnrampDeeplinkModule.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/deeplink/di/OnrampDeeplinkModule.kt @@ -1,7 +1,6 @@ package com.tangem.features.onramp.deeplink.di import com.tangem.features.onramp.deeplink.* -import com.tangem.features.onramp.deeplink.DefaultOnrampDeepLink import dagger.Binds import dagger.Module import dagger.hilt.InstallIn @@ -18,5 +17,9 @@ internal interface OnrampDeeplinkModule { @Binds @Singleton - fun bindFactoryV2(impl: DefaultOnrampDeepLinkHandler.Factory): OnrampDeepLinkHandler.Factory + fun bindOnrampDeepLinkHandlerFactory(impl: DefaultOnrampDeepLinkHandler.Factory): OnrampDeepLinkHandler.Factory + + @Binds + @Singleton + fun bindBuyDeepLinkHandler(impl: DefaultBuyDeepLinkHandler.Factory): BuyDeepLinkHandler.Factory } \ No newline at end of file From e56c34de0b63a08dd8c8163e2e3d0174a36b8b75 Mon Sep 17 00:00:00 2001 From: Tangem Date: Fri, 16 May 2025 12:26:58 +0500 Subject: [PATCH 064/165] Updated on 2026-08-14 --- .../core/deeplink/global/ReferralDeepLink.kt | 1 + features/referral/api/build.gradle.kts | 3 ++ .../api/deeplink/ReferralDeepLinkHandler.kt | 8 ++++ features/referral/impl/build.gradle.kts | 2 + .../DefaultReferralDeepLinkHandler.kt | 37 +++++++++++++++++++ .../deeplink/di/ReferralDeepLinkModule.kt | 18 +++++++++ 6 files changed, 69 insertions(+) create mode 100644 features/referral/api/src/main/kotlin/com/tangem/feature/referral/api/deeplink/ReferralDeepLinkHandler.kt create mode 100644 features/referral/impl/src/main/java/com/tangem/feature/referral/deeplink/DefaultReferralDeepLinkHandler.kt create mode 100644 features/referral/impl/src/main/java/com/tangem/feature/referral/deeplink/di/ReferralDeepLinkModule.kt diff --git a/core/deep-links/global/src/main/kotlin/com/tangem/core/deeplink/global/ReferralDeepLink.kt b/core/deep-links/global/src/main/kotlin/com/tangem/core/deeplink/global/ReferralDeepLink.kt index 763a6d80f7..5dcf6211d5 100644 --- a/core/deep-links/global/src/main/kotlin/com/tangem/core/deeplink/global/ReferralDeepLink.kt +++ b/core/deep-links/global/src/main/kotlin/com/tangem/core/deeplink/global/ReferralDeepLink.kt @@ -2,6 +2,7 @@ package com.tangem.core.deeplink.global import com.tangem.core.deeplink.DeepLink +@Deprecated("Use ReferralDeepLinkHandler") class ReferralDeepLink( val onReceive: () -> Unit, ) : DeepLink(shouldHandleDelayed = true) { diff --git a/features/referral/api/build.gradle.kts b/features/referral/api/build.gradle.kts index 22a3ba6fa3..da68554518 100644 --- a/features/referral/api/build.gradle.kts +++ b/features/referral/api/build.gradle.kts @@ -17,4 +17,7 @@ dependencies { /* Project - Domain */ implementation(projects.domain.wallets.models) + + /* Compose */ + implementation(deps.compose.runtime) } \ No newline at end of file diff --git a/features/referral/api/src/main/kotlin/com/tangem/feature/referral/api/deeplink/ReferralDeepLinkHandler.kt b/features/referral/api/src/main/kotlin/com/tangem/feature/referral/api/deeplink/ReferralDeepLinkHandler.kt new file mode 100644 index 0000000000..b405e43147 --- /dev/null +++ b/features/referral/api/src/main/kotlin/com/tangem/feature/referral/api/deeplink/ReferralDeepLinkHandler.kt @@ -0,0 +1,8 @@ +package com.tangem.feature.referral.api.deeplink + +interface ReferralDeepLinkHandler { + + interface Factory { + fun create(): ReferralDeepLinkHandler + } +} \ No newline at end of file diff --git a/features/referral/impl/build.gradle.kts b/features/referral/impl/build.gradle.kts index cf2272bee5..89e4f4f57c 100644 --- a/features/referral/impl/build.gradle.kts +++ b/features/referral/impl/build.gradle.kts @@ -39,12 +39,14 @@ dependencies { /** Domain */ implementation(projects.domain.demo) implementation(projects.domain.wallets) + implementation(projects.domain.legacy) implementation(projects.domain.wallets.models) implementation(projects.features.referral.domain) /** Other libraries */ implementation(deps.compose.shimmer) implementation(deps.compose.accompanist.systemUiController) + implementation(deps.timber) /** DI */ implementation(deps.hilt.android) diff --git a/features/referral/impl/src/main/java/com/tangem/feature/referral/deeplink/DefaultReferralDeepLinkHandler.kt b/features/referral/impl/src/main/java/com/tangem/feature/referral/deeplink/DefaultReferralDeepLinkHandler.kt new file mode 100644 index 0000000000..a4f80621ef --- /dev/null +++ b/features/referral/impl/src/main/java/com/tangem/feature/referral/deeplink/DefaultReferralDeepLinkHandler.kt @@ -0,0 +1,37 @@ +package com.tangem.feature.referral.deeplink + +import com.tangem.common.routing.AppRoute +import com.tangem.common.routing.AppRouter +import com.tangem.domain.common.util.cardTypesResolver +import com.tangem.domain.wallets.usecase.GetSelectedWalletSyncUseCase +import com.tangem.feature.referral.api.deeplink.ReferralDeepLinkHandler +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject +import timber.log.Timber + +internal class DefaultReferralDeepLinkHandler @AssistedInject constructor( + appRouter: AppRouter, + getSelectedWalletSyncUseCase: GetSelectedWalletSyncUseCase, +) : ReferralDeepLinkHandler { + + init { + // It is okay here, we are navigating from outside, and there is no other way to getting UserWallet + getSelectedWalletSyncUseCase().fold( + ifLeft = { + Timber.e("Error on getting user wallet: $it") + }, + ifRight = { userWallet -> + if (userWallet.cardTypesResolver.isTangemWallet()) { + appRouter.push( + AppRoute.ReferralProgram(userWalletId = userWallet.walletId), + ) + } + }, + ) + } + + @AssistedFactory + interface Factory : ReferralDeepLinkHandler.Factory { + override fun create(): DefaultReferralDeepLinkHandler + } +} \ No newline at end of file diff --git a/features/referral/impl/src/main/java/com/tangem/feature/referral/deeplink/di/ReferralDeepLinkModule.kt b/features/referral/impl/src/main/java/com/tangem/feature/referral/deeplink/di/ReferralDeepLinkModule.kt new file mode 100644 index 0000000000..6ba16abf7e --- /dev/null +++ b/features/referral/impl/src/main/java/com/tangem/feature/referral/deeplink/di/ReferralDeepLinkModule.kt @@ -0,0 +1,18 @@ +package com.tangem.feature.referral.deeplink.di + +import com.tangem.feature.referral.api.deeplink.ReferralDeepLinkHandler +import com.tangem.feature.referral.deeplink.DefaultReferralDeepLinkHandler +import dagger.Binds +import dagger.Module +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent +import javax.inject.Singleton + +@Module +@InstallIn(SingletonComponent::class) +internal interface ReferralDeepLinkModule { + + @Binds + @Singleton + fun bindFactory(impl: DefaultReferralDeepLinkHandler.Factory): ReferralDeepLinkHandler.Factory +} \ No newline at end of file From 717ab01782a9bef15e0348aaeec944f94c9c51e8 Mon Sep 17 00:00:00 2001 From: Tangem Date: Fri, 16 May 2025 12:27:04 +0500 Subject: [PATCH 065/165] Updated on 2026-08-14 --- .../com/tangem/tap/routing/utils/DeepLinkFactory.kt | 9 +++++++++ .../com/tangem/common/routing/DeepLinkRoute.kt | 12 ++++++++++++ .../onramp/deeplink/DefaultOnrampDeepLinkHandler.kt | 8 ++++---- 3 files changed, 25 insertions(+), 4 deletions(-) diff --git a/app/src/main/java/com/tangem/tap/routing/utils/DeepLinkFactory.kt b/app/src/main/java/com/tangem/tap/routing/utils/DeepLinkFactory.kt index ea0040261e..699c1e184c 100644 --- a/app/src/main/java/com/tangem/tap/routing/utils/DeepLinkFactory.kt +++ b/app/src/main/java/com/tangem/tap/routing/utils/DeepLinkFactory.kt @@ -4,7 +4,10 @@ import android.net.Uri import com.tangem.common.routing.AppRoute import com.tangem.common.routing.DeepLinkRoute import com.tangem.common.routing.DeepLinkScheme +import com.tangem.feature.referral.api.deeplink.ReferralDeepLinkHandler +import com.tangem.features.onramp.deeplink.BuyDeepLinkHandler import com.tangem.features.onramp.deeplink.OnrampDeepLinkHandler +import com.tangem.features.send.v2.api.deeplink.SellDeepLinkHandler import com.tangem.utils.coroutines.JobHolder import com.tangem.utils.coroutines.saveIn import dagger.hilt.android.scopes.ActivityScoped @@ -19,6 +22,9 @@ import javax.inject.Inject @ActivityScoped internal class DeepLinkFactory @Inject constructor( private val onrampDeepLink: OnrampDeepLinkHandler.Factory, + private val sellDeepLink: SellDeepLinkHandler.Factory, + private val buyDeepLink: BuyDeepLinkHandler.Factory, + private val referralDeepLink: ReferralDeepLinkHandler.Factory, ) { private val permittedAppRoute = MutableStateFlow(false) @@ -82,6 +88,9 @@ internal class DeepLinkFactory @Inject constructor( val params = getParams(deeplinkUri) when (deeplinkUri.host) { DeepLinkRoute.Onramp.host -> onrampDeepLink.create(coroutineScope, params) + DeepLinkRoute.Sell.host -> sellDeepLink.create(coroutineScope, params) + DeepLinkRoute.Buy.host -> buyDeepLink.create(coroutineScope) + DeepLinkRoute.Referral.host -> referralDeepLink.create() else -> { Timber.i( """ diff --git a/common/routing/src/main/kotlin/com/tangem/common/routing/DeepLinkRoute.kt b/common/routing/src/main/kotlin/com/tangem/common/routing/DeepLinkRoute.kt index 85bf976648..5eb9bafa87 100644 --- a/common/routing/src/main/kotlin/com/tangem/common/routing/DeepLinkRoute.kt +++ b/common/routing/src/main/kotlin/com/tangem/common/routing/DeepLinkRoute.kt @@ -7,6 +7,18 @@ sealed class DeepLinkRoute { data object Onramp : DeepLinkRoute() { override val host: String = "onramp" } + + data object Sell : DeepLinkRoute() { + override val host: String = "redirect_sell" + } + + data object Buy : DeepLinkRoute() { + override val host: String = "redirect" + } + + data object Referral : DeepLinkRoute() { + override val host: String = "referral" + } } enum class DeepLinkScheme(val scheme: String) { diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/deeplink/DefaultOnrampDeepLinkHandler.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/deeplink/DefaultOnrampDeepLinkHandler.kt index c1ef286e30..47b4d78b20 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/deeplink/DefaultOnrampDeepLinkHandler.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/deeplink/DefaultOnrampDeepLinkHandler.kt @@ -10,11 +10,11 @@ import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.launch import timber.log.Timber -class DefaultOnrampDeepLinkHandler @AssistedInject constructor( - appRouter: AppRouter, - private val onrampSuccessScreenTrigger: OnrampSuccessScreenTrigger, - @Assisted private val scope: CoroutineScope, +internal class DefaultOnrampDeepLinkHandler @AssistedInject constructor( + @Assisted scope: CoroutineScope, @Assisted params: Map, + appRouter: AppRouter, + onrampSuccessScreenTrigger: OnrampSuccessScreenTrigger, ) : OnrampDeepLinkHandler { init { From 8a8ef9825cdd940da7e20c3594916a3f381bdf78 Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 19 May 2025 18:25:00 +0500 Subject: [PATCH 066/165] Updated on 2026-08-14 --- .../tap/routing/utils/DeepLinkFactoryTest.kt | 276 ++++++++++++++++++ 1 file changed, 276 insertions(+) create mode 100644 app/src/test/kotlin/com/tangem/tap/routing/utils/DeepLinkFactoryTest.kt diff --git a/app/src/test/kotlin/com/tangem/tap/routing/utils/DeepLinkFactoryTest.kt b/app/src/test/kotlin/com/tangem/tap/routing/utils/DeepLinkFactoryTest.kt new file mode 100644 index 0000000000..6bc9b58431 --- /dev/null +++ b/app/src/test/kotlin/com/tangem/tap/routing/utils/DeepLinkFactoryTest.kt @@ -0,0 +1,276 @@ +package com.tangem.tap.routing.utils + +import android.net.Uri +import com.tangem.common.routing.AppRoute +import com.tangem.feature.referral.api.deeplink.ReferralDeepLinkHandler +import com.tangem.features.onramp.deeplink.BuyDeepLinkHandler +import com.tangem.features.onramp.deeplink.OnrampDeepLinkHandler +import com.tangem.features.send.v2.api.deeplink.SellDeepLinkHandler +import io.mockk.every +import io.mockk.mockk +import io.mockk.verify +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.cancel +import kotlinx.coroutines.test.* +import org.junit.After +import org.junit.Before +import org.junit.Test +import timber.log.Timber + +@OptIn(ExperimentalCoroutinesApi::class) +class DeepLinkFactoryTest { + + private val onrampDeepLinkFactory = mockk(relaxed = true) { + every { create(any(), any()) } returns mockk() + } + private val sellDeepLinkFactory = mockk(relaxed = true) { + every { create(any(), any()) } returns mockk() + } + private val buyDeepLinkFactory = mockk(relaxed = true) { + every { create(any()) } returns mockk() + } + private val referralDeepLinkFactory = mockk(relaxed = true) { + every { create() } returns mockk() + } + + private val mockedUri = mockk(relaxed = true) + + private lateinit var testDispatcher: TestDispatcher + private lateinit var testScope: TestScope + + private val deepLinkFactory = DeepLinkFactory( + onrampDeepLinkFactory, + sellDeepLinkFactory, + buyDeepLinkFactory, + referralDeepLinkFactory, + ) + + @OptIn(ExperimentalCoroutinesApi::class) + @Before + fun setUp() { + testDispatcher = StandardTestDispatcher() + testScope = TestScope(testDispatcher) + + Dispatchers.setMain(testDispatcher) + + every { mockedUri.path } returns "/path" + every { mockedUri.toString() } returns "https://example.com/path?query=param" + every { mockedUri.authority } returns "example.com" + every { mockedUri.port } returns 443 // Default HTTPS port + every { mockedUri.fragment } returns null // No fragment in this URI + + Timber.uprootAll() // Disable Timber logging for tests + } + + @OptIn(ExperimentalCoroutinesApi::class) + @After + fun tearDown() { + // Reset the main dispatcher + Dispatchers.resetMain() + // Clean up test coroutines + testScope.cancel() + } + + @Test + fun `handleDeeplink stores uri and launches when permitted`() = runTest { + every { mockedUri.scheme } returns "tangem" + every { mockedUri.host } returns "onramp" + every { mockedUri.query } returns "param=value" + every { mockedUri.queryParameterNames } returns setOf("param") + every { mockedUri.getQueryParameter("param") } returns "value" + + // Set permittedAppRoute to true + deepLinkFactory.handleDeeplink(mockedUri, testScope) + deepLinkFactory.checkRoutingReadiness(AppRoute.Wallet) + + advanceUntilIdle() + + // Verify onramp handler was called + verify { + onrampDeepLinkFactory.create(eq(testScope), eq(mapOf("param" to "value"))) + } + } + + @Test + fun `handleDeeplink does not launch when not permitted`() = runTest { + every { mockedUri.scheme } returns "tangem" + every { mockedUri.host } returns "onramp" + every { mockedUri.query } returns "param=value" + every { mockedUri.queryParameterNames } returns setOf("param") + every { mockedUri.getQueryParameter("param") } returns "value" + + deepLinkFactory.handleDeeplink(mockedUri, testScope) + deepLinkFactory.checkRoutingReadiness(AppRoute.Initial) + + advanceUntilIdle() + + // Verify no handler was called + verify(inverse = true) { onrampDeepLinkFactory.create(any(), any()) } + } + + @Test + fun `launchDeepLink handles tangem scheme correctly`() = runTest { + every { mockedUri.scheme } returns "tangem" + every { mockedUri.host } returns "onramp" + every { mockedUri.query } returns "param=value" + every { mockedUri.queryParameterNames } returns setOf("param") + every { mockedUri.getQueryParameter("param") } returns "value" + + deepLinkFactory.checkRoutingReadiness(AppRoute.Wallet) + deepLinkFactory.handleDeeplink(mockedUri, testScope) + + advanceUntilIdle() + + verify { + onrampDeepLinkFactory.create(eq(testScope), eq(mapOf("param" to "value"))) + } + } + + @Test + fun `launchDeepLink ignores unknown scheme`() = runTest { + every { mockedUri.scheme } returns "https" + every { mockedUri.host } returns "example.com" + + deepLinkFactory.handleDeeplink(mockedUri, testScope) + + advanceUntilIdle() + + verify(inverse = true) { + onrampDeepLinkFactory.create(any(), any()) + sellDeepLinkFactory.create(any(), any()) + buyDeepLinkFactory.create(any()) + referralDeepLinkFactory.create() + } + } + + @Test + fun `handleTangemDeepLinks routes to correct handler`() = runTest { + every { mockedUri.scheme } returns "tangem" + every { mockedUri.query } returns "param=value" + every { mockedUri.queryParameterNames } returns setOf("param") + every { mockedUri.getQueryParameter("param") } returns "value" + + deepLinkFactory.checkRoutingReadiness(AppRoute.Wallet) + + // Test Onramp + every { mockedUri.host } returns "onramp" + deepLinkFactory.handleDeeplink(mockedUri, testScope) + advanceUntilIdle() + verify { onrampDeepLinkFactory.create(eq(testScope), eq(mapOf("param" to "value"))) } + + // Test Sell + every { mockedUri.host } returns "redirect_sell" + deepLinkFactory.handleDeeplink(mockedUri, testScope) + advanceUntilIdle() + verify { sellDeepLinkFactory.create(eq(testScope), eq(mapOf("param" to "value"))) } + + // Reset params + every { mockedUri.queryParameterNames } returns emptySet() + every { mockedUri.getQueryParameter(any()) } returns "" + + // Test Buy + every { mockedUri.host } returns "redirect" + deepLinkFactory.handleDeeplink(mockedUri, testScope) + advanceUntilIdle() + verify { buyDeepLinkFactory.create(eq(testScope)) } + + // Test Referral + every { mockedUri.host } returns "referral" + deepLinkFactory.handleDeeplink(mockedUri, testScope) + advanceUntilIdle() + verify { referralDeepLinkFactory.create() } + } + + @Test + fun `handleTangemDeepLinks incorrect host`() = runTest { + every { mockedUri.scheme } returns "tangem" + every { mockedUri.host } returns "unknown" + + deepLinkFactory.checkRoutingReadiness(AppRoute.Wallet) + deepLinkFactory.handleDeeplink(mockedUri, testScope) + + advanceUntilIdle() + + verify(inverse = true) { + onrampDeepLinkFactory.create(any(), any()) + sellDeepLinkFactory.create(any(), any()) + buyDeepLinkFactory.create(any()) + referralDeepLinkFactory.create() + } + } + + @Test + fun `getParams filters malicious parameters`() = runTest { + every { mockedUri.scheme } returns "tangem" + every { mockedUri.host } returns "onramp" + every { mockedUri.query } returns "safe=ok&malicious=%3Cscript%3E"e=O%27Brien" + every { mockedUri.queryParameterNames } returns setOf("safe", "malicious", "quote") + every { mockedUri.getQueryParameter("safe") } returns "ok" + every { mockedUri.getQueryParameter("malicious") } returns "