From e45d91b15e5b32efa7e98a1327987c12e4f73d35 Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 15 Jan 2026 11:56:24 +0300 Subject: [PATCH] Updated on 2026-08-14 --- .../converters/WalletIdBodyConverter.kt | 7 +- .../tangemTech/models/UserTokensResponse.kt | 2 + .../api/tangemTech/models/WalletIdBody.kt | 2 + .../converters/WalletIdBodyConverterTest.kt | 12 +- .../FetchWalletAccountsErrorHandler.kt | 23 +- .../FetchWalletAccountsErrorHandlerTest.kt | 44 +--- .../data/common/currency/UserTokensSaver.kt | 9 +- .../tangem/data/common/di/DataCommonModule.kt | 23 ++ .../wallet/DefaultWalletServerBinder.kt | 35 +++ .../data/common/wallet/WalletServerBinder.kt | 26 ++ .../common/currency/UserTokensSaverTest.kt | 15 +- .../wallet/DefaultWalletServerBinderTest.kt | 226 ++++++++++++++++++ .../data/wallets/DefaultWalletsRepository.kt | 14 +- .../data/wallets/di/WalletsDataModule.kt | 6 + .../wallets/DefaultWalletsRepositoryTest.kt | 12 + 15 files changed, 389 insertions(+), 67 deletions(-) create mode 100644 data/common/src/main/kotlin/com/tangem/data/common/wallet/DefaultWalletServerBinder.kt create mode 100644 data/common/src/main/kotlin/com/tangem/data/common/wallet/WalletServerBinder.kt create mode 100644 data/common/src/test/kotlin/com/tangem/data/common/wallet/DefaultWalletServerBinderTest.kt diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/converters/WalletIdBodyConverter.kt b/core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/converters/WalletIdBodyConverter.kt index dba595b6ff..8163c5ca35 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/converters/WalletIdBodyConverter.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/converters/WalletIdBodyConverter.kt @@ -4,10 +4,15 @@ import com.tangem.datasource.api.tangemTech.models.CardInfoBody import com.tangem.datasource.api.tangemTech.models.WalletIdBody import com.tangem.datasource.api.tangemTech.models.WalletType import com.tangem.domain.models.wallet.UserWallet +import com.tangem.domain.wallets.models.AppsFlyerConversionData object WalletIdBodyConverter { - fun convert(userWallet: UserWallet, publicKeys: Map? = null): WalletIdBody { + fun convert( + userWallet: UserWallet, + conversionData: AppsFlyerConversionData? = null, + publicKeys: Map? = null, + ): WalletIdBody { return WalletIdBody( walletId = userWallet.walletId.stringValue, name = userWallet.name, 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 c9602c0247..c0edc1becf 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 @@ -17,6 +17,8 @@ data class UserTokensResponse( @Json(name = "notifyStatus") val notifyStatus: Boolean? = null, @Json(name = "name") val walletName: String? = null, @Json(name = "type") val walletType: WalletType? = null, + @Json(name = "ref") val refcode: String? = null, + @Json(name = "campaign") val campaign: String? = null, ) { @JsonClass(generateAdapter = true) 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 cb4bd5f2df..597380b6f7 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 @@ -9,4 +9,6 @@ data class WalletIdBody( @Json(name = "name") val name: String, @Json(name = "type") val walletType: WalletType? = null, @Json(name = "cards") val cards: List? = null, + @Json(name = "ref") val refcode: String? = null, + @Json(name = "campaign") val campaign: String? = null, ) \ No newline at end of file diff --git a/core/datasource/src/test/kotlin/com/tangem/datasource/api/tangemTech/converters/WalletIdBodyConverterTest.kt b/core/datasource/src/test/kotlin/com/tangem/datasource/api/tangemTech/converters/WalletIdBodyConverterTest.kt index 299deb3a22..3ac09a3455 100644 --- a/core/datasource/src/test/kotlin/com/tangem/datasource/api/tangemTech/converters/WalletIdBodyConverterTest.kt +++ b/core/datasource/src/test/kotlin/com/tangem/datasource/api/tangemTech/converters/WalletIdBodyConverterTest.kt @@ -30,7 +30,11 @@ class WalletIdBodyConverterTest { ) // WHEN - val result = WalletIdBodyConverter.convert(userWallet, publicKeys) + val result = WalletIdBodyConverter.convert( + userWallet = userWallet, + conversionData = null, + publicKeys = publicKeys, + ) // THEN assertThat(result).isEqualTo( @@ -68,7 +72,11 @@ class WalletIdBodyConverterTest { val publicKeys = emptyMap() // WHEN - val result = WalletIdBodyConverter.convert(userWallet, publicKeys) + val result = WalletIdBodyConverter.convert( + userWallet = userWallet, + conversionData = null, + publicKeys = publicKeys, + ) // THEN assertThat(result).isEqualTo( diff --git a/data/account/src/main/kotlin/com/tangem/data/account/fetcher/FetchWalletAccountsErrorHandler.kt b/data/account/src/main/kotlin/com/tangem/data/account/fetcher/FetchWalletAccountsErrorHandler.kt index 772a1dce68..266d845b10 100644 --- a/data/account/src/main/kotlin/com/tangem/data/account/fetcher/FetchWalletAccountsErrorHandler.kt +++ b/data/account/src/main/kotlin/com/tangem/data/account/fetcher/FetchWalletAccountsErrorHandler.kt @@ -4,22 +4,18 @@ import com.tangem.data.account.fetcher.DefaultWalletAccountsFetcher.FetchResult import com.tangem.data.account.utils.DefaultWalletAccountsResponseFactory import com.tangem.data.common.currency.UserTokensResponseAccountIdEnricher import com.tangem.data.common.currency.UserTokensSaver +import com.tangem.data.common.wallet.WalletServerBinder import com.tangem.datasource.api.common.response.ApiResponse import com.tangem.datasource.api.common.response.ApiResponseError import com.tangem.datasource.api.common.response.ApiResponseError.HttpException.Code import com.tangem.datasource.api.common.response.ETAG_HEADER import com.tangem.datasource.api.common.response.isNetworkError -import com.tangem.datasource.api.tangemTech.TangemTechApi -import com.tangem.datasource.api.tangemTech.converters.WalletIdBodyConverter import com.tangem.datasource.api.tangemTech.models.UserTokensResponse import com.tangem.datasource.api.tangemTech.models.account.GetWalletAccountsResponse import com.tangem.datasource.api.tangemTech.models.account.WalletAccountDTO import com.tangem.datasource.api.tangemTech.models.account.toUserTokensResponse import com.tangem.datasource.local.token.UserTokensResponseStore -import com.tangem.datasource.local.userwallet.UserWalletsStore import com.tangem.domain.models.wallet.UserWalletId -import com.tangem.utils.coroutines.CoroutineDispatcherProvider -import kotlinx.coroutines.withContext import timber.log.Timber import javax.inject.Inject import javax.inject.Singleton @@ -27,26 +23,21 @@ import javax.inject.Singleton /** * Handles errors that occur during the fetching of wallet accounts * - * @property tangemTechApi API for network requests - * @property userWalletsStore provides access to user wallets storage * @property userTokensSaver saves user tokens to the storage * @property userTokensResponseStore provides access to user token responses. * @property defaultWalletAccountsResponseFactory creates [GetWalletAccountsResponse] from [UserTokensResponse] - * @property dispatchers dispatchers + * @property walletServerBinder binds user wallets on the server * * @see DefaultWalletAccountsFetcher * [REDACTED_AUTHOR] */ -@Suppress("LongParameterList") @Singleton internal class FetchWalletAccountsErrorHandler @Inject constructor( - private val tangemTechApi: TangemTechApi, - private val userWalletsStore: UserWalletsStore, private val userTokensSaver: UserTokensSaver, private val userTokensResponseStore: UserTokensResponseStore, private val defaultWalletAccountsResponseFactory: DefaultWalletAccountsResponseFactory, - private val dispatchers: CoroutineDispatcherProvider, + private val walletServerBinder: WalletServerBinder, ) { /** @@ -119,13 +110,7 @@ internal class FetchWalletAccountsErrorHandler @Inject constructor( */ private suspend fun createWallet(userWalletId: UserWalletId): String? { - val userWallet = userWalletsStore.getSyncOrNull(key = userWalletId) ?: return null - - val creationResponse = withContext(dispatchers.io) { - tangemTechApi.createWallet( - body = WalletIdBodyConverter.convert(userWallet), - ) - } + val creationResponse = walletServerBinder.bind(userWalletId) return if (creationResponse is ApiResponse.Success && creationResponse.code == Code.CREATED) { creationResponse.headers[ETAG_HEADER]?.firstOrNull() diff --git a/data/account/src/test/java/com/tangem/data/account/fetcher/FetchWalletAccountsErrorHandlerTest.kt b/data/account/src/test/java/com/tangem/data/account/fetcher/FetchWalletAccountsErrorHandlerTest.kt index 51d9c2ad3f..80d6803bf8 100644 --- a/data/account/src/test/java/com/tangem/data/account/fetcher/FetchWalletAccountsErrorHandlerTest.kt +++ b/data/account/src/test/java/com/tangem/data/account/fetcher/FetchWalletAccountsErrorHandlerTest.kt @@ -4,23 +4,22 @@ import com.tangem.data.account.converter.createGetWalletAccountsResponse import com.tangem.data.account.converter.createWalletAccountDTO import com.tangem.data.account.utils.DefaultWalletAccountsResponseFactory import com.tangem.data.common.currency.UserTokensSaver +import com.tangem.data.common.wallet.WalletServerBinder import com.tangem.datasource.api.common.response.ApiResponse import com.tangem.datasource.api.common.response.ApiResponseError import com.tangem.datasource.api.common.response.ApiResponseError.HttpException.Code import com.tangem.datasource.api.common.response.ETAG_HEADER import com.tangem.datasource.api.tangemTech.TangemTechApi import com.tangem.datasource.api.tangemTech.models.UserTokensResponse -import com.tangem.datasource.api.tangemTech.models.WalletIdBody -import com.tangem.datasource.api.tangemTech.models.WalletType import com.tangem.datasource.api.tangemTech.models.account.GetWalletAccountsResponse import com.tangem.datasource.api.tangemTech.models.account.WalletAccountDTO import com.tangem.datasource.api.tangemTech.models.account.toUserTokensResponse import com.tangem.datasource.local.token.UserTokensResponseStore -import com.tangem.datasource.local.userwallet.UserWalletsStore -import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.models.wallet.UserWalletId -import com.tangem.utils.coroutines.TestingCoroutineDispatcherProvider -import io.mockk.* +import io.mockk.clearMocks +import io.mockk.coEvery +import io.mockk.coVerify +import io.mockk.mockk import kotlinx.coroutines.test.runTest import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.Test @@ -33,18 +32,16 @@ import org.junit.jupiter.api.TestInstance class FetchWalletAccountsErrorHandlerTest { private val tangemTechApi: TangemTechApi = mockk() - private val userWalletsStore: UserWalletsStore = mockk() + private val walletServerBinder: WalletServerBinder = mockk() private val userTokensSaver: UserTokensSaver = mockk(relaxUnitFun = true) private val userTokensResponseStore: UserTokensResponseStore = mockk(relaxUnitFun = true) private val defaultWalletAccountsResponseFactory: DefaultWalletAccountsResponseFactory = mockk() private val handler = FetchWalletAccountsErrorHandler( - tangemTechApi = tangemTechApi, - userWalletsStore = userWalletsStore, + walletServerBinder = walletServerBinder, userTokensSaver = userTokensSaver, userTokensResponseStore = userTokensResponseStore, defaultWalletAccountsResponseFactory = defaultWalletAccountsResponseFactory, - dispatchers = TestingCoroutineDispatcherProvider(), ) private val pushWalletAccounts: suspend (List, String) -> GetWalletAccountsResponse = @@ -55,7 +52,7 @@ class FetchWalletAccountsErrorHandlerTest { fun setupEach() { clearMocks( tangemTechApi, - userWalletsStore, + walletServerBinder, userTokensSaver, userTokensResponseStore, defaultWalletAccountsResponseFactory, @@ -112,22 +109,7 @@ class FetchWalletAccountsErrorHandlerTest { code = Code.CREATED, ) - val walletName = "Wallet" - val userWallet = mockk { - every { this@mockk.walletId } returns userWalletId - every { this@mockk.name } returns walletName - } - coEvery { userWalletsStore.getSyncOrNull(userWalletId) } returns userWallet - - coEvery { - tangemTechApi.createWallet( - WalletIdBody( - walletId = userWalletId.stringValue, - name = walletName, - walletType = WalletType.COLD, - ), - ) - } returns apiResponse + coEvery { walletServerBinder.bind(userWalletId) } returns apiResponse coEvery { pushWalletAccounts(listOf(accountDTO), eTagValue) } returns savedAccountsResponse // Act @@ -142,13 +124,7 @@ class FetchWalletAccountsErrorHandlerTest { // Assert coVerify { userTokensSaver.pushWithRetryer(userWalletId, response = savedAccountsResponse.toUserTokensResponse()) - tangemTechApi.createWallet( - WalletIdBody( - walletId = userWalletId.stringValue, - name = walletName, - walletType = WalletType.COLD, - ), - ) + walletServerBinder.bind(userWalletId) pushWalletAccounts(listOf(accountDTO), eTagValue) storeWalletAccounts(userWalletId, savedAccountsResponse) } diff --git a/data/common/src/main/kotlin/com/tangem/data/common/currency/UserTokensSaver.kt b/data/common/src/main/kotlin/com/tangem/data/common/currency/UserTokensSaver.kt index 56153829bb..e0ae509ee6 100644 --- a/data/common/src/main/kotlin/com/tangem/data/common/currency/UserTokensSaver.kt +++ b/data/common/src/main/kotlin/com/tangem/data/common/currency/UserTokensSaver.kt @@ -2,13 +2,14 @@ package com.tangem.data.common.currency import com.tangem.data.common.api.safeApiCall import com.tangem.data.common.tokens.UserTokensBackwardCompatibility +import com.tangem.data.common.wallet.WalletServerBinder import com.tangem.datasource.api.common.response.ApiResponse import com.tangem.datasource.api.common.response.ApiResponseError import com.tangem.datasource.api.common.response.isNetworkError import com.tangem.datasource.api.tangemTech.TangemTechApi -import com.tangem.datasource.api.tangemTech.converters.WalletIdBodyConverter import com.tangem.datasource.api.tangemTech.models.UserTokensResponse import com.tangem.datasource.api.tangemTech.models.WalletType +import com.tangem.datasource.local.appsflyer.AppsFlyerConversionStore import com.tangem.datasource.local.token.UserTokensResponseStore import com.tangem.datasource.local.userwallet.UserWalletsStore import com.tangem.domain.account.featuretoggle.AccountsFeatureToggles @@ -27,6 +28,8 @@ class UserTokensSaver( private val userTokensResponseStore: UserTokensResponseStore, private val dispatchers: CoroutineDispatcherProvider, private val addressesEnricher: UserTokensResponseAddressesEnricher, + private val walletServerBinder: WalletServerBinder, + private val appsFlyerConversionStore: AppsFlyerConversionStore, private val accountsFeatureToggles: AccountsFeatureToggles, private val pushTokensRetryerPool: RetryerPool, ) { @@ -69,6 +72,8 @@ class UserTokensSaver( pushNew(userWallet = userWallet, response = enrichedResponse, onFailSend = onFailSend) } else { + val conversionData = appsFlyerConversionStore.get() + val enrichedResponse = response.enrichIf(userWalletId = userWalletId, condition = useEnricher).copy( walletName = userWallet.name, walletType = WalletType.from(userWallet), @@ -116,7 +121,7 @@ class UserTokensSaver( apiResponse.cause.isNetworkError(ApiResponseError.HttpException.Code.NOT_FOUND) if (isWalletNotFound) { - tangemTechApi.createWallet(body = WalletIdBodyConverter.convert(userWallet)).bind() + walletServerBinder.bind(userWallet).bind() tangemTechApi.saveTokens( userId = userWallet.walletId.stringValue, 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 index 48751fa834..9cc64b8160 100644 --- 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 @@ -7,7 +7,10 @@ import com.tangem.data.common.cache.etag.ETagsStore import com.tangem.data.common.currency.* import com.tangem.data.common.quote.DefaultQuotesFetcher import com.tangem.data.common.quote.QuotesFetcher +import com.tangem.data.common.wallet.DefaultWalletServerBinder +import com.tangem.data.common.wallet.WalletServerBinder import com.tangem.datasource.api.tangemTech.TangemTechApi +import com.tangem.datasource.local.appsflyer.AppsFlyerConversionStore import com.tangem.datasource.local.preferences.AppPreferencesStore import com.tangem.datasource.local.token.UserTokensResponseStore import com.tangem.datasource.local.userwallet.UserWalletsStore @@ -72,6 +75,8 @@ internal object DataCommonModule { userTokensResponseStore: UserTokensResponseStore, dispatchers: CoroutineDispatcherProvider, addressesEnricher: UserTokensResponseAddressesEnricher, + walletServerBinder: WalletServerBinder, + appsFlyerConversionStore: AppsFlyerConversionStore, accountsFeatureToggles: AccountsFeatureToggles, ): UserTokensSaver { return UserTokensSaver( @@ -84,6 +89,8 @@ internal object DataCommonModule { pushTokensRetryerPool = RetryerPool( coroutineScope = CoroutineScope(SupervisorJob() + dispatchers.default), ), + walletServerBinder = walletServerBinder, + appsFlyerConversionStore = appsFlyerConversionStore, ) } @@ -98,4 +105,20 @@ internal object DataCommonModule { fun provideETagsStore(appPreferencesStore: AppPreferencesStore): ETagsStore { return DefaultETagsStore(appPreferencesStore = appPreferencesStore) } + + @Provides + @Singleton + fun provideWalletServerBinder( + userWalletsStore: UserWalletsStore, + appsFlyerConversionStore: AppsFlyerConversionStore, + tangemTechApi: TangemTechApi, + dispatchers: CoroutineDispatcherProvider, + ): WalletServerBinder { + return DefaultWalletServerBinder( + userWalletsStore = userWalletsStore, + appsFlyerConversionStore = appsFlyerConversionStore, + tangemTechApi = tangemTechApi, + dispatchers = dispatchers, + ) + } } \ No newline at end of file diff --git a/data/common/src/main/kotlin/com/tangem/data/common/wallet/DefaultWalletServerBinder.kt b/data/common/src/main/kotlin/com/tangem/data/common/wallet/DefaultWalletServerBinder.kt new file mode 100644 index 0000000000..376f29fb84 --- /dev/null +++ b/data/common/src/main/kotlin/com/tangem/data/common/wallet/DefaultWalletServerBinder.kt @@ -0,0 +1,35 @@ +package com.tangem.data.common.wallet + +import com.tangem.datasource.api.common.response.ApiResponse +import com.tangem.datasource.api.tangemTech.TangemTechApi +import com.tangem.datasource.api.tangemTech.converters.WalletIdBodyConverter +import com.tangem.datasource.local.appsflyer.AppsFlyerConversionStore +import com.tangem.datasource.local.userwallet.UserWalletsStore +import com.tangem.domain.models.wallet.UserWallet +import com.tangem.domain.models.wallet.UserWalletId +import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import kotlinx.coroutines.withContext + +internal class DefaultWalletServerBinder( + private val userWalletsStore: UserWalletsStore, + private val appsFlyerConversionStore: AppsFlyerConversionStore, + private val tangemTechApi: TangemTechApi, + private val dispatchers: CoroutineDispatcherProvider, +) : WalletServerBinder { + + override suspend fun bind(userWalletId: UserWalletId): ApiResponse? { + val userWallet = userWalletsStore.getSyncOrNull(key = userWalletId) ?: return null + + return bind(userWallet) + } + + override suspend fun bind(userWallet: UserWallet): ApiResponse { + val conversionData = appsFlyerConversionStore.get() + + return withContext(dispatchers.io) { + tangemTechApi.createWallet( + body = WalletIdBodyConverter.convert(userWallet, conversionData), + ) + } + } +} \ No newline at end of file diff --git a/data/common/src/main/kotlin/com/tangem/data/common/wallet/WalletServerBinder.kt b/data/common/src/main/kotlin/com/tangem/data/common/wallet/WalletServerBinder.kt new file mode 100644 index 0000000000..6aeb5b8a1e --- /dev/null +++ b/data/common/src/main/kotlin/com/tangem/data/common/wallet/WalletServerBinder.kt @@ -0,0 +1,26 @@ +package com.tangem.data.common.wallet + +import com.tangem.datasource.api.common.response.ApiResponse +import com.tangem.domain.models.wallet.UserWallet +import com.tangem.domain.models.wallet.UserWalletId + +/** + + * +[REDACTED_AUTHOR] + */ +interface WalletServerBinder { + + /** + * Binds a wallet to the server using its [userWalletId]. + * Returns an [ApiResponse] indicating the result of the binding operation, + * or null if the binding was not performed. + */ + suspend fun bind(userWalletId: UserWalletId): ApiResponse? + + /** + * Binds a wallet to the server using the provided [userWallet]. + * Returns an [ApiResponse] indicating the result of the binding operation. + */ + suspend fun bind(userWallet: UserWallet): ApiResponse +} \ No newline at end of file diff --git a/data/common/src/test/kotlin/com/tangem/data/common/currency/UserTokensSaverTest.kt b/data/common/src/test/kotlin/com/tangem/data/common/currency/UserTokensSaverTest.kt index bc51068bff..64fe198777 100644 --- a/data/common/src/test/kotlin/com/tangem/data/common/currency/UserTokensSaverTest.kt +++ b/data/common/src/test/kotlin/com/tangem/data/common/currency/UserTokensSaverTest.kt @@ -1,10 +1,12 @@ package com.tangem.data.common.currency +import com.tangem.data.common.wallet.WalletServerBinder 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.UserTokensResponse import com.tangem.datasource.api.tangemTech.models.WalletType +import com.tangem.datasource.local.appsflyer.AppsFlyerConversionStore import com.tangem.datasource.local.token.UserTokensResponseStore import com.tangem.datasource.local.userwallet.UserWalletsStore import com.tangem.domain.account.featuretoggle.AccountsFeatureToggles @@ -27,6 +29,8 @@ class UserTokensSaverTest { private val accountsFeatureToggles = mockk { every { this@mockk.isFeatureEnabled } returns true } + private val walletServerBinder: WalletServerBinder = mockk() + private val appsFlyerConversionStore: AppsFlyerConversionStore = mockk() private val userTokensSaver: UserTokensSaver = UserTokensSaver( tangemTechApi = tangemTechApi, @@ -34,13 +38,21 @@ class UserTokensSaverTest { userTokensResponseStore = userTokensResponseStore, dispatchers = TestingCoroutineDispatcherProvider(), addressesEnricher = enricher, + walletServerBinder = walletServerBinder, + appsFlyerConversionStore = appsFlyerConversionStore, accountsFeatureToggles = accountsFeatureToggles, pushTokensRetryerPool = mockk(), ) @BeforeEach fun resetMocks() { - clearMocks(tangemTechApi, userWalletsStore, userTokensResponseStore, enricher) + clearMocks( + tangemTechApi, + userWalletsStore, + userTokensResponseStore, + enricher, + walletServerBinder, + ) } @Test @@ -109,7 +121,6 @@ class UserTokensSaverTest { coEvery { userWalletsStore.getSyncOrNull(userWalletId) } returns userWallet coEvery { enricher(userWalletId, response) } returns enrichedResponse coEvery { tangemTechApi.saveTokens(any(), any()) } returns ApiResponse.Error(error) as ApiResponse - coEvery { tangemTechApi.createWallet(body = any()) } returns ApiResponse.Error(error) as ApiResponse // WHEN userTokensSaver.push( diff --git a/data/common/src/test/kotlin/com/tangem/data/common/wallet/DefaultWalletServerBinderTest.kt b/data/common/src/test/kotlin/com/tangem/data/common/wallet/DefaultWalletServerBinderTest.kt new file mode 100644 index 0000000000..620f841b4a --- /dev/null +++ b/data/common/src/test/kotlin/com/tangem/data/common/wallet/DefaultWalletServerBinderTest.kt @@ -0,0 +1,226 @@ +package com.tangem.data.common.wallet + +import com.google.common.truth.Truth +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.WalletIdBody +import com.tangem.datasource.api.tangemTech.models.WalletType +import com.tangem.datasource.local.appsflyer.AppsFlyerConversionStore +import com.tangem.datasource.local.userwallet.UserWalletsStore +import com.tangem.domain.models.wallet.UserWallet +import com.tangem.domain.models.wallet.UserWalletId +import com.tangem.domain.wallets.models.AppsFlyerConversionData +import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import com.tangem.utils.coroutines.TestingCoroutineDispatcherProvider +import io.mockk.* +import kotlinx.coroutines.test.runTest +import org.junit.jupiter.api.AfterEach +import org.junit.jupiter.api.Nested +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.TestInstance + +/** +[REDACTED_AUTHOR] + */ +@TestInstance(TestInstance.Lifecycle.PER_CLASS) +internal class DefaultWalletServerBinderTest { + + private val userWalletsStore: UserWalletsStore = mockk() + private val appsFlyerConversionStore: AppsFlyerConversionStore = mockk() + private val tangemTechApi: TangemTechApi = mockk() + private val dispatchers: CoroutineDispatcherProvider = TestingCoroutineDispatcherProvider() + + private val binder = DefaultWalletServerBinder( + userWalletsStore = userWalletsStore, + appsFlyerConversionStore = appsFlyerConversionStore, + tangemTechApi = tangemTechApi, + dispatchers = dispatchers + ) + + private val userWalletId = UserWalletId("011") + private val userWallet = mockk { + every { this@mockk.walletId } returns userWalletId + every { this@mockk.name } returns "Wallet" + } + private val conversionData = AppsFlyerConversionData(refcode = "refcode", campaign = "campaign") + + @AfterEach + fun tearDown() { + clearMocks(userWalletsStore, appsFlyerConversionStore, tangemTechApi) + } + + @Nested + @TestInstance(TestInstance.Lifecycle.PER_CLASS) + inner class BindUserWalletId { + + @Test + fun `bind successfully`() = runTest { + val requestBody = WalletIdBody( + walletId = userWalletId.stringValue, + name = "Wallet", + walletType = WalletType.COLD, + refcode = "refcode", + campaign = "campaign", + ) + val apiResponse = ApiResponse.Success(Unit) + + coEvery { userWalletsStore.getSyncOrNull(userWalletId) } returns userWallet + coEvery { appsFlyerConversionStore.get() } returns conversionData + coEvery { tangemTechApi.createWallet(requestBody) } returns apiResponse + + val actual = binder.bind(userWalletId = userWalletId) + + Truth.assertThat(actual).isEqualTo(apiResponse) + + coVerifyOrder { + userWalletsStore.getSyncOrNull(userWalletId) + appsFlyerConversionStore.get() + tangemTechApi.createWallet(requestBody) + } + } + + @Test + fun `bind will skipped if userWalletsStore returns null`() = runTest { + coEvery { userWalletsStore.getSyncOrNull(userWalletId) } returns null + + val actual = binder.bind(userWalletId = userWalletId) + + Truth.assertThat(actual).isEqualTo(null) + + coVerifyOrder { + userWalletsStore.getSyncOrNull(userWalletId) + } + + coVerify(inverse = true) { + appsFlyerConversionStore.get() + tangemTechApi.createWallet(any()) + } + } + + @Test + fun `bind successfully if appsFlyerConversionStore returns null`() = runTest { + val requestBody = WalletIdBody( + walletId = userWalletId.stringValue, + name = "Wallet", + walletType = WalletType.COLD, + ) + val apiResponse = ApiResponse.Success(Unit) + + coEvery { userWalletsStore.getSyncOrNull(userWalletId) } returns userWallet + coEvery { appsFlyerConversionStore.get() } returns null + coEvery { tangemTechApi.createWallet(requestBody) } returns apiResponse + + val actual = binder.bind(userWalletId = userWalletId) + + Truth.assertThat(actual).isEqualTo(apiResponse) + + coVerifyOrder { + userWalletsStore.getSyncOrNull(userWalletId) + appsFlyerConversionStore.get() + tangemTechApi.createWallet(requestBody) + } + } + + @Test + fun `bind returns error if request is failed`() = runTest { + val requestBody = WalletIdBody( + walletId = userWalletId.stringValue, + name = "Wallet", + walletType = WalletType.COLD, + refcode = "refcode", + campaign = "campaign", + ) + val apiResponse = ApiResponse.Error(ApiResponseError.TimeoutException()) as ApiResponse + + coEvery { userWalletsStore.getSyncOrNull(userWalletId) } returns userWallet + coEvery { appsFlyerConversionStore.get() } returns conversionData + coEvery { tangemTechApi.createWallet(requestBody) } returns apiResponse + + val actual = binder.bind(userWalletId = userWalletId) + + Truth.assertThat(actual).isEqualTo(apiResponse) + + coVerifyOrder { + userWalletsStore.getSyncOrNull(userWalletId) + appsFlyerConversionStore.get() + tangemTechApi.createWallet(requestBody) + } + } + } + + @Nested + @TestInstance(TestInstance.Lifecycle.PER_CLASS) + inner class BindUserWallet { + + @Test + fun `bind successfully`() = runTest { + val requestBody = WalletIdBody( + walletId = userWalletId.stringValue, + name = "Wallet", + walletType = WalletType.COLD, + refcode = "refcode", + campaign = "campaign", + ) + val apiResponse = ApiResponse.Success(Unit) + + coEvery { appsFlyerConversionStore.get() } returns conversionData + coEvery { tangemTechApi.createWallet(requestBody) } returns apiResponse + + val actual = binder.bind(userWallet = userWallet) + + Truth.assertThat(actual).isEqualTo(apiResponse) + + coVerifyOrder { + appsFlyerConversionStore.get() + tangemTechApi.createWallet(requestBody) + } + } + + @Test + fun `bind successfully if appsFlyerConversionStore returns null`() = runTest { + val requestBody = WalletIdBody( + walletId = userWalletId.stringValue, + name = "Wallet", + walletType = WalletType.COLD, + ) + val apiResponse = ApiResponse.Success(Unit) + + coEvery { appsFlyerConversionStore.get() } returns null + coEvery { tangemTechApi.createWallet(requestBody) } returns apiResponse + + val actual = binder.bind(userWallet = userWallet) + + Truth.assertThat(actual).isEqualTo(apiResponse) + + coVerifyOrder { + appsFlyerConversionStore.get() + tangemTechApi.createWallet(requestBody) + } + } + + @Test + fun `bind returns error if request is failed`() = runTest { + val requestBody = WalletIdBody( + walletId = userWalletId.stringValue, + name = "Wallet", + walletType = WalletType.COLD, + refcode = "refcode", + campaign = "campaign", + ) + val apiResponse = ApiResponse.Error(ApiResponseError.TimeoutException()) as ApiResponse + + coEvery { appsFlyerConversionStore.get() } returns conversionData + coEvery { tangemTechApi.createWallet(requestBody) } returns apiResponse + + val actual = binder.bind(userWallet = userWallet) + + Truth.assertThat(actual).isEqualTo(apiResponse) + + coVerifyOrder { + appsFlyerConversionStore.get() + tangemTechApi.createWallet(requestBody) + } + } + } +} \ No newline at end of file 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 def9a96402..ab7d7a4c66 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 @@ -3,6 +3,7 @@ package com.tangem.data.wallets import arrow.core.Either import arrow.core.left import arrow.core.right +import com.tangem.data.common.wallet.WalletServerBinder import com.tangem.data.wallets.converters.UserWalletRemoteInfoConverter import com.tangem.datasource.api.common.AuthProvider import com.tangem.datasource.api.common.response.ApiResponse @@ -14,6 +15,7 @@ import com.tangem.datasource.api.tangemTech.TangemTechApi import com.tangem.datasource.api.tangemTech.converters.WalletIdBodyConverter import com.tangem.datasource.api.tangemTech.models.* import com.tangem.datasource.api.tangemTech.models.SeedPhraseNotificationDTO.Status +import com.tangem.datasource.local.appsflyer.AppsFlyerConversionStore import com.tangem.datasource.local.datastore.RuntimeStateStore import com.tangem.datasource.local.preferences.AppPreferencesStore import com.tangem.datasource.local.preferences.PreferencesKeys @@ -46,6 +48,8 @@ internal class DefaultWalletsRepository( private val seedPhraseNotificationVisibilityStore: RuntimeStateStore, private val dispatchers: CoroutineDispatcherProvider, private val authProvider: AuthProvider, + private val walletServerBinder: WalletServerBinder, + private val appsFlyerConversionStore: AppsFlyerConversionStore, private val accountsFeatureToggles: AccountsFeatureToggles, private val moshi: com.squareup.moshi.Moshi, ) : WalletsRepository { @@ -250,13 +254,7 @@ internal class DefaultWalletsRepository( } override suspend fun createWallet(userWalletId: UserWalletId) { - runCatching(dispatchers.io) { - val userWallet = userWalletsStore.getSyncStrict(key = userWalletId) - - tangemTechApi.createWallet( - body = WalletIdBodyConverter.convert(userWallet = userWallet), - ) - } + walletServerBinder.bind(userWalletId) } override suspend fun rejectSeedPhraseSecondNotification(userWalletId: UserWalletId) { @@ -413,10 +411,12 @@ internal class DefaultWalletsRepository( associateApplicationIdWithWallets().getOrThrow() } } else { + val conversionData = appsFlyerConversionStore.get() val publicKeys = authProvider.getCardsPublicKeys() val walletsBody = wallets.map { userWallet -> WalletIdBodyConverter.convert( userWallet = userWallet, + conversionData = conversionData, publicKeys = if (userWallet is UserWallet.Cold) { publicKeys.filterKeys { userWallet.cardsInWallet.contains(it) 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 a1e870a3e1..3ba821417e 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 @@ -1,6 +1,7 @@ package com.tangem.data.wallets.di import com.squareup.moshi.Moshi +import com.tangem.data.common.wallet.WalletServerBinder import com.tangem.data.wallets.DefaultWalletNamesMigrationRepository import com.tangem.data.wallets.DefaultWalletsRepository import com.tangem.data.wallets.cold.DefaultColdMapDerivationsRepository @@ -10,6 +11,7 @@ import com.tangem.data.wallets.hot.DefaultHotWalletAccessCodeAttemptsRepository import com.tangem.datasource.api.common.AuthProvider import com.tangem.datasource.api.tangemTech.TangemTechApi import com.tangem.datasource.di.NetworkMoshi +import com.tangem.datasource.local.appsflyer.AppsFlyerConversionStore import com.tangem.datasource.local.datastore.RuntimeStateStore import com.tangem.datasource.local.preferences.AppPreferencesStore import com.tangem.datasource.local.userwallet.UserWalletsStore @@ -40,6 +42,8 @@ internal object WalletsDataModule { userWalletsStore: UserWalletsStore, dispatchers: CoroutineDispatcherProvider, authProvider: AuthProvider, + walletServerBinder: WalletServerBinder, + appsFlyerConversionStore: AppsFlyerConversionStore, accountsFeatureToggles: AccountsFeatureToggles, @NetworkMoshi moshi: Moshi, ): WalletsRepository { @@ -50,6 +54,8 @@ internal object WalletsDataModule { seedPhraseNotificationVisibilityStore = RuntimeStateStore(defaultValue = emptyMap()), dispatchers = dispatchers, authProvider = authProvider, + walletServerBinder = walletServerBinder, + appsFlyerConversionStore = appsFlyerConversionStore, accountsFeatureToggles = accountsFeatureToggles, moshi = moshi, ) 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 80b3e8d659..22bbfbdf5c 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 @@ -4,6 +4,7 @@ import androidx.datastore.core.DataStore import androidx.datastore.preferences.core.Preferences import com.google.common.truth.Truth.assertThat import com.squareup.moshi.Moshi +import com.tangem.data.common.wallet.WalletServerBinder import com.tangem.datasource.api.common.AuthProvider import com.tangem.datasource.api.common.response.ApiResponse import com.tangem.datasource.api.common.response.ApiResponseError.HttpException @@ -11,6 +12,7 @@ import com.tangem.datasource.api.tangemTech.TangemTechApi import com.tangem.datasource.api.tangemTech.models.PromocodeActivationBody import com.tangem.datasource.api.tangemTech.models.PromocodeActivationResponse import com.tangem.datasource.api.tangemTech.models.WalletResponse +import com.tangem.datasource.local.appsflyer.AppsFlyerConversionStore import com.tangem.datasource.local.preferences.AppPreferencesStore import com.tangem.datasource.local.preferences.PreferencesKeys import com.tangem.domain.account.featuretoggle.AccountsFeatureToggles @@ -38,12 +40,16 @@ class DefaultWalletsRepositoryTest { ) private lateinit var tangemTechApi: TangemTechApi private lateinit var dispatchers: CoroutineDispatcherProvider + private lateinit var walletServerBinder: WalletServerBinder + private lateinit var appsFlyerConversionStore: AppsFlyerConversionStore private val testWalletId = UserWalletId("1234567890abcdef") @Before fun setup() { tangemTechApi = mockk() + walletServerBinder = mockk() + appsFlyerConversionStore = mockk() dispatchers = TestingCoroutineDispatcherProvider() repository = DefaultWalletsRepository( appPreferencesStore = appPreferenceStore, @@ -52,6 +58,8 @@ class DefaultWalletsRepositoryTest { seedPhraseNotificationVisibilityStore = mockk(), dispatchers = dispatchers, authProvider = mockk(), + walletServerBinder = walletServerBinder, + appsFlyerConversionStore = appsFlyerConversionStore, accountsFeatureToggles = mockk(), moshi = mockk(), ) @@ -218,10 +226,14 @@ class DefaultWalletsRepositoryTest { seedPhraseNotificationVisibilityStore = mockk(), dispatchers = dispatchers, authProvider = authProvider, + walletServerBinder = walletServerBinder, + appsFlyerConversionStore = appsFlyerConversionStore, accountsFeatureToggles = accountsFeatureToggles, moshi = mockk(), ) + coEvery { appsFlyerConversionStore.get() } returns null + coEvery { tangemTechApi.associateApplicationIdWithWallets( eq(applicationId),