Updated on 2026-08-14

This commit is contained in:
Tangem 2025-10-29 15:59:29 +04:00
parent 4215c91a37
commit df186a5f5d
9 changed files with 74 additions and 9 deletions

View file

@ -1,5 +1,6 @@
package com.tangem.common.ui.account package com.tangem.common.ui.account
import androidx.annotation.StringRes
import androidx.compose.runtime.Immutable import androidx.compose.runtime.Immutable
import arrow.core.Either import arrow.core.Either
import arrow.core.raise.either import arrow.core.raise.either
@ -25,7 +26,10 @@ sealed interface AccountNameUM {
*/ */
data object DefaultMain : AccountNameUM { data object DefaultMain : AccountNameUM {
override val value: TextReference = resourceReference(R.string.account_main_account_title) @get:StringRes
val stringResId: Int = R.string.account_main_account_title
override val value: TextReference = resourceReference(stringResId)
} }
/** /**

View file

@ -15,9 +15,14 @@ tasks.withType<Test>().configureEach {
dependencies { dependencies {
// region Project - Common
implementation(projects.common.ui) // It's needed for getting AccountName.DefaultMain value
// endregion
// region Project - Core // region Project - Core
implementation(projects.core.datasource) implementation(projects.core.datasource)
implementation(projects.core.configToggles) implementation(projects.core.configToggles)
implementation(projects.core.res)
api(projects.core.utils) api(projects.core.utils)
// endregion // endregion

View file

@ -1,5 +1,6 @@
package com.tangem.data.account.di package com.tangem.data.account.di
import android.content.Context
import com.tangem.core.configtoggle.feature.FeatureTogglesManager import com.tangem.core.configtoggle.feature.FeatureTogglesManager
import com.tangem.data.account.converter.AccountConverterFactoryContainer import com.tangem.data.account.converter.AccountConverterFactoryContainer
import com.tangem.data.account.featuretoggle.DefaultAccountsFeatureToggles import com.tangem.data.account.featuretoggle.DefaultAccountsFeatureToggles
@ -21,6 +22,7 @@ import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import dagger.Module import dagger.Module
import dagger.Provides import dagger.Provides
import dagger.hilt.InstallIn import dagger.hilt.InstallIn
import dagger.hilt.android.qualifiers.ApplicationContext
import dagger.hilt.components.SingletonComponent import dagger.hilt.components.SingletonComponent
import javax.inject.Singleton import javax.inject.Singleton
@ -43,6 +45,7 @@ internal object AccountDataModule {
userWalletsStore: UserWalletsStore, userWalletsStore: UserWalletsStore,
userTokensSaver: UserTokensSaver, userTokensSaver: UserTokensSaver,
accountConverterFactoryContainer: AccountConverterFactoryContainer, accountConverterFactoryContainer: AccountConverterFactoryContainer,
@ApplicationContext context: Context,
dispatchers: CoroutineDispatcherProvider, dispatchers: CoroutineDispatcherProvider,
): AccountsCRUDRepository { ): AccountsCRUDRepository {
return DefaultAccountsCRUDRepository( return DefaultAccountsCRUDRepository(
@ -54,6 +57,7 @@ internal object AccountDataModule {
userTokensSaver = userTokensSaver, userTokensSaver = userTokensSaver,
archivedAccountsETagStore = RuntimeStateStore(emptyMap()), archivedAccountsETagStore = RuntimeStateStore(emptyMap()),
convertersContainer = accountConverterFactoryContainer, convertersContainer = accountConverterFactoryContainer,
resources = context.resources,
dispatchers = dispatchers, dispatchers = dispatchers,
) )
} }

View file

@ -1,8 +1,11 @@
package com.tangem.data.account.repository package com.tangem.data.account.repository
import android.content.res.Resources
import arrow.core.Option import arrow.core.Option
import arrow.core.raise.option import arrow.core.raise.option
import arrow.core.toOption import arrow.core.toOption
import com.tangem.common.ui.account.AccountNameUM
import com.tangem.core.res.getStringSafe
import com.tangem.data.account.converter.AccountConverterFactoryContainer import com.tangem.data.account.converter.AccountConverterFactoryContainer
import com.tangem.data.account.converter.ArchivedAccountConverter import com.tangem.data.account.converter.ArchivedAccountConverter
import com.tangem.data.account.store.AccountsResponseStore import com.tangem.data.account.store.AccountsResponseStore
@ -26,6 +29,7 @@ import com.tangem.domain.account.models.ArchivedAccount
import com.tangem.domain.account.repository.AccountsCRUDRepository import com.tangem.domain.account.repository.AccountsCRUDRepository
import com.tangem.domain.models.account.Account import com.tangem.domain.models.account.Account
import com.tangem.domain.models.account.AccountId import com.tangem.domain.models.account.AccountId
import com.tangem.domain.models.account.AccountName
import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.models.wallet.UserWallet
import com.tangem.domain.models.wallet.UserWalletId import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.utils.coroutines.CoroutineDispatcherProvider import com.tangem.utils.coroutines.CoroutineDispatcherProvider
@ -48,6 +52,7 @@ internal class DefaultAccountsCRUDRepository(
private val userTokensSaver: UserTokensSaver, private val userTokensSaver: UserTokensSaver,
private val archivedAccountsETagStore: RuntimeStateStore<Map<String, String?>>, private val archivedAccountsETagStore: RuntimeStateStore<Map<String, String?>>,
private val convertersContainer: AccountConverterFactoryContainer, private val convertersContainer: AccountConverterFactoryContainer,
private val resources: Resources,
private val dispatchers: CoroutineDispatcherProvider, private val dispatchers: CoroutineDispatcherProvider,
) : AccountsCRUDRepository { ) : AccountsCRUDRepository {
@ -197,6 +202,19 @@ internal class DefaultAccountsCRUDRepository(
override fun getUserWalletsSync(): List<UserWallet> = userWalletsStore.userWalletsSync override fun getUserWalletsSync(): List<UserWallet> = userWalletsStore.userWalletsSync
override fun checkDefaultAccountName(accountList: AccountList, accountName: AccountName) {
val hasDefaultName = accountList.accounts.any { it.accountName is AccountName.DefaultMain }
if (!hasDefaultName) return
val defaultName = resources.getStringSafe(AccountNameUM.DefaultMain.stringResId)
.let(AccountName::invoke).getOrNull()
require(defaultName != accountName) {
"Cannot use default account name \"$accountName\" for custom accounts"
}
}
private suspend fun saveETag(userWalletId: UserWalletId, apiResponse: ApiResponse<*>) { private suspend fun saveETag(userWalletId: UserWalletId, apiResponse: ApiResponse<*>) {
val eTag = apiResponse.headers[ETAG_HEADER]?.firstOrNull() val eTag = apiResponse.headers[ETAG_HEADER]?.firstOrNull()

View file

@ -1,5 +1,6 @@
package com.tangem.data.account.repository package com.tangem.data.account.repository
import android.content.res.Resources
import arrow.core.None import arrow.core.None
import arrow.core.toOption import arrow.core.toOption
import com.google.common.truth.Truth import com.google.common.truth.Truth
@ -59,6 +60,8 @@ class DefaultAccountsCRUDRepositoryTest {
private val accountListConverter: AccountListConverter = mockk() private val accountListConverter: AccountListConverter = mockk()
private val cryptoPortfolioConverter: CryptoPortfolioConverter = mockk() private val cryptoPortfolioConverter: CryptoPortfolioConverter = mockk()
private val resources: Resources = mockk()
private val repository = DefaultAccountsCRUDRepository( private val repository = DefaultAccountsCRUDRepository(
tangemTechApi = tangemTechApi, tangemTechApi = tangemTechApi,
walletAccountsSaver = walletAccountsSaver, walletAccountsSaver = walletAccountsSaver,
@ -68,6 +71,7 @@ class DefaultAccountsCRUDRepositoryTest {
userTokensSaver = userTokensSaver, userTokensSaver = userTokensSaver,
archivedAccountsETagStore = archivedAccountsETagStore, archivedAccountsETagStore = archivedAccountsETagStore,
convertersContainer = convertersContainer, convertersContainer = convertersContainer,
resources = resources,
dispatchers = TestingCoroutineDispatcherProvider(), dispatchers = TestingCoroutineDispatcherProvider(),
) )

View file

@ -5,6 +5,7 @@ import com.tangem.domain.account.models.AccountList
import com.tangem.domain.account.models.ArchivedAccount import com.tangem.domain.account.models.ArchivedAccount
import com.tangem.domain.models.account.Account import com.tangem.domain.models.account.Account
import com.tangem.domain.models.account.AccountId import com.tangem.domain.models.account.AccountId
import com.tangem.domain.models.account.AccountName
import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.models.wallet.UserWallet
import com.tangem.domain.models.wallet.UserWalletId import com.tangem.domain.models.wallet.UserWalletId
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
@ -113,4 +114,12 @@ interface AccountsCRUDRepository {
/** Synchronously retrieves all user wallets */ /** Synchronously retrieves all user wallets */
fun getUserWalletsSync(): List<UserWallet> fun getUserWalletsSync(): List<UserWallet>
/** Checks if the provided account name is the default name within the given account list
*
* @param accountList the list of accounts to check against
* @param accountName the account name to be checked
* @throws IllegalArgumentException if the account name matches the default name
*/
fun checkDefaultAccountName(accountList: AccountList, accountName: AccountName)
} }

View file

@ -1,10 +1,12 @@
package com.tangem.domain.account.usecase package com.tangem.domain.account.usecase
import arrow.core.Either import arrow.core.Either
import arrow.core.Either.Companion.catch
import arrow.core.getOrElse import arrow.core.getOrElse
import arrow.core.raise.Raise import arrow.core.raise.Raise
import arrow.core.raise.catch import arrow.core.raise.catch
import arrow.core.raise.either import arrow.core.raise.either
import arrow.core.raise.withError
import com.tangem.domain.account.fetcher.SingleAccountListFetcher import com.tangem.domain.account.fetcher.SingleAccountListFetcher
import com.tangem.domain.account.models.AccountList import com.tangem.domain.account.models.AccountList
import com.tangem.domain.account.repository.AccountsCRUDRepository import com.tangem.domain.account.repository.AccountsCRUDRepository
@ -49,8 +51,10 @@ class AddCryptoPortfolioUseCase(
val newAccount = createAccount(userWalletId, accountName, icon, derivationIndex) val newAccount = createAccount(userWalletId, accountName, icon, derivationIndex)
val updatedAccounts = (accountList + newAccount).getOrElse { val updatedAccounts = withError({ Error.AccountListRequirementsNotMet(it) }) {
raise(Error.AccountListRequirementsNotMet(it)) checkDefaultName(accountList, accountName)
(accountList + newAccount).bind()
} }
saveAccounts(accountList = updatedAccounts) saveAccounts(accountList = updatedAccounts)
@ -81,6 +85,12 @@ class AddCryptoPortfolioUseCase(
) )
} }
private fun Raise<AccountList.Error>.checkDefaultName(accountList: AccountList, accountName: AccountName) {
withError({ AccountList.Error.DuplicateAccountNames }) {
catch { crudRepository.checkDefaultAccountName(accountList, accountName) }.bind()
}
}
private suspend fun Raise<Error>.getAccountList(userWalletId: UserWalletId): AccountList { private suspend fun Raise<Error>.getAccountList(userWalletId: UserWalletId): AccountList {
return catch( return catch(
block = { crudRepository.getAccountListSync(userWalletId = userWalletId) }, block = { crudRepository.getAccountListSync(userWalletId = userWalletId) },

View file

@ -2,10 +2,7 @@ package com.tangem.domain.account.usecase
import arrow.core.Either import arrow.core.Either
import arrow.core.getOrElse import arrow.core.getOrElse
import arrow.core.raise.Raise import arrow.core.raise.*
import arrow.core.raise.catch
import arrow.core.raise.either
import arrow.core.raise.ensure
import com.tangem.domain.account.models.AccountList import com.tangem.domain.account.models.AccountList
import com.tangem.domain.account.repository.AccountsCRUDRepository import com.tangem.domain.account.repository.AccountsCRUDRepository
import com.tangem.domain.models.account.Account import com.tangem.domain.models.account.Account
@ -50,8 +47,12 @@ class UpdateCryptoPortfolioUseCase(
.setName(name = accountName) .setName(name = accountName)
.setIcon(icon = icon) .setIcon(icon = icon)
val updatedAccounts = (accountList + updatedAccount).getOrElse { val updatedAccounts = withError({ Error.AccountListRequirementsNotMet(it) }) {
raise(Error.AccountListRequirementsNotMet(it)) if (accountName != null) {
checkDefaultName(accountList, accountName)
}
(accountList + updatedAccount).bind()
} }
saveAccounts(updatedAccounts) saveAccounts(updatedAccounts)
@ -88,6 +89,12 @@ class UpdateCryptoPortfolioUseCase(
return if (icon != null) this.copy(icon = icon) else this return if (icon != null) this.copy(icon = icon) else this
} }
private fun Raise<AccountList.Error>.checkDefaultName(accountList: AccountList, accountName: AccountName) {
withError({ AccountList.Error.DuplicateAccountNames }) {
Either.Companion.catch { crudRepository.checkDefaultAccountName(accountList, accountName) }.bind()
}
}
/** /**
* Represents possible errors that can occur during the update operation * Represents possible errors that can occur during the update operation
*/ */

View file

@ -69,6 +69,7 @@ class AddCryptoPortfolioUseCaseTest {
coVerifySequence { coVerifySequence {
singleAccountListFetcher(SingleAccountListFetcher.Params(userWalletId)) singleAccountListFetcher(SingleAccountListFetcher.Params(userWalletId))
crudRepository.getAccountListSync(userWalletId) crudRepository.getAccountListSync(userWalletId)
crudRepository.checkDefaultAccountName(accountList, newAccount.accountName)
crudRepository.saveAccounts(updatedAccountList) crudRepository.saveAccounts(updatedAccountList)
mainAccountTokensMigration.migrate(userWalletId, newAccount.derivationIndex) mainAccountTokensMigration.migrate(userWalletId, newAccount.derivationIndex)
} }
@ -177,6 +178,7 @@ class AddCryptoPortfolioUseCaseTest {
coVerifySequence { coVerifySequence {
singleAccountListFetcher(SingleAccountListFetcher.Params(userWalletId)) singleAccountListFetcher(SingleAccountListFetcher.Params(userWalletId))
crudRepository.getAccountListSync(userWalletId) crudRepository.getAccountListSync(userWalletId)
crudRepository.checkDefaultAccountName(accountList, newAccount.accountName)
} }
coVerify(inverse = true) { coVerify(inverse = true) {
@ -249,6 +251,7 @@ class AddCryptoPortfolioUseCaseTest {
coVerifySequence { coVerifySequence {
singleAccountListFetcher(SingleAccountListFetcher.Params(userWalletId)) singleAccountListFetcher(SingleAccountListFetcher.Params(userWalletId))
crudRepository.getAccountListSync(userWalletId) crudRepository.getAccountListSync(userWalletId)
crudRepository.checkDefaultAccountName(accountList, newAccount.accountName)
crudRepository.saveAccounts(updatedAccountList) crudRepository.saveAccounts(updatedAccountList)
} }
@ -288,6 +291,7 @@ class AddCryptoPortfolioUseCaseTest {
coVerifySequence { coVerifySequence {
singleAccountListFetcher(SingleAccountListFetcher.Params(userWalletId)) singleAccountListFetcher(SingleAccountListFetcher.Params(userWalletId))
crudRepository.getAccountListSync(userWalletId) crudRepository.getAccountListSync(userWalletId)
crudRepository.checkDefaultAccountName(accountList, newAccount.accountName)
crudRepository.saveAccounts(updatedAccountList) crudRepository.saveAccounts(updatedAccountList)
mainAccountTokensMigration.migrate(userWalletId, newAccount.derivationIndex) mainAccountTokensMigration.migrate(userWalletId, newAccount.derivationIndex)
} }