Updated on 2026-08-14

This commit is contained in:
Tangem 2025-05-12 18:42:37 +05:00
parent 3171058cb6
commit a6e4099935
16 changed files with 242 additions and 33 deletions

View file

@ -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<String, String> {
return userWalletsListManager.userWalletsSync.associate {
it.scanResponse.card.cardId to it.scanResponse.card.cardPublicKey.toHexString()
}
}
}

View file

@ -11,4 +11,9 @@ interface AuthProvider {
fun getCardPublicKey(): String
fun getCardId(): String
/**
* Returns map where keys(cardId) associated with cardPublicKey
*/
fun getCardsPublicKeys(): Map<String, String>
}

View file

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

View file

@ -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<CardInfoBody>,
)

View file

@ -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<String>) =
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(

View file

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

View file

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

View file

@ -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<SeedPhraseNotificationsStatuses>,
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<UserWallet>) =
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

View file

@ -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<String, String>): WalletIdBody {
return WalletIdBody(
walletId = userWallet.walletId.stringValue,
name = userWallet.name,
cards = publicKeys.map {
CardInfoBody(
cardId = it.key,
cardPublicKey = it.value,
)
},
)
}
}

View file

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

View file

@ -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<UserWallet> {
every { cardsInWallet } returns setOf(card1PublicKey)
every { walletId } returns UserWalletId(wallet1Id)
every { name } returns "Wallet 1"
},
mockk<UserWallet> {
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<AuthProvider> {
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"
}
},
)
}
}
}

View file

@ -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<String, String>()
// WHEN
val result = WalletIdBodyConverter.convert(userWallet, publicKeys)
// THEN
assertThat(result).isEqualTo(
WalletIdBody(
walletId = walletId.stringValue,
name = walletName,
cards = emptyList(),
),
)
}
}

View file

@ -16,9 +16,6 @@ interface NotificationsRepository {
suspend fun incrementTronTokenFeeNotificationShowCounter()
@Throws
suspend fun associateApplicationIdWithWallets(appId: String, wallets: List<String>)
@Throws
suspend fun sendPushToken(appId: ApplicationId, pushToken: String)

View file

@ -27,6 +27,7 @@ class DefaultUserWalletsSyncDelegate(
}
}
// TODO remove dispatchers whnen UserWalletsListManager will be main safe
private suspend fun renameUserWallet(
userWalletId: UserWalletId,
name: String,

View file

@ -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<UserWalletRemoteInfo>
@Throws
suspend fun associateWallets(applicationId: String, wallets: List<UserWallet>)
}

View file

@ -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<Throwable, Unit> = Either.catch {
val wallets = userWalletsListManager.userWalletsSync
walletsRepository.associateWallets(applicationId.value, wallets)
}
}