Updated on 2026-08-14

This commit is contained in:
Tangem 2025-10-07 17:12:39 +04:00
parent 231c97e371
commit 373880691e
12 changed files with 129 additions and 61 deletions

View file

@ -2,8 +2,37 @@ package com.tangem.datasource.api.tangemTech.models.account
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
import com.tangem.datasource.utils.SerializeNulls
@JsonClass(generateAdapter = true)
data class SaveWalletAccountsResponse(
@Json(name = "accounts") val accounts: List<WalletAccountDTO>,
)
@Json(name = "accounts") val accounts: List<AccountDTO>,
) {
@SerializeNulls
@JsonClass(generateAdapter = true)
data class AccountDTO(
@Json(name = "id") val id: String,
@Json(name = "name") val name: String?,
@Json(name = "derivation") val derivationIndex: Int,
@Json(name = "icon") val icon: String,
@Json(name = "iconColor") val iconColor: String,
)
companion object {
operator fun invoke(accounts: List<WalletAccountDTO>): SaveWalletAccountsResponse {
return SaveWalletAccountsResponse(
accounts = accounts.map { accountDto ->
AccountDTO(
id = accountDto.id,
name = accountDto.name,
derivationIndex = accountDto.derivationIndex,
icon = accountDto.icon,
iconColor = accountDto.iconColor,
)
},
)
}
}
}

View file

@ -9,6 +9,7 @@ import com.tangem.common.json.MoshiJsonConverter
import com.tangem.datasource.api.common.adapter.*
import com.tangem.datasource.local.config.providers.models.ProviderModel
import com.tangem.datasource.local.network.entity.NetworkStatusDM
import com.tangem.datasource.utils.SerializeNullsFactory
import com.tangem.domain.models.scan.serialization.*
import com.tangem.domain.visa.model.VisaActivationRemoteState
import com.tangem.domain.visa.model.VisaCardActivationStatus
@ -28,6 +29,7 @@ class MoshiModule {
@NetworkMoshi
fun provideNetworkMoshi(): Moshi {
return Moshi.Builder()
.add(SerializeNullsFactory)
.add(
PolymorphicJsonAdapterFactory.of(ProviderModel::class.java, "type")
.withSubtype(ProviderModel.Public::class.java, "public")

View file

@ -0,0 +1,5 @@
package com.tangem.datasource.utils
@Target(AnnotationTarget.CLASS)
@Retention(AnnotationRetention.RUNTIME)
annotation class SerializeNulls

View file

@ -0,0 +1,25 @@
package com.tangem.datasource.utils
import com.squareup.moshi.JsonAdapter
import com.squareup.moshi.Moshi
import com.squareup.moshi.Types
import java.lang.reflect.Type
/**
* Factory to serialize nulls in Moshi if the class is annotated with [SerializeNulls].
*
[REDACTED_AUTHOR]
*/
internal object SerializeNullsFactory : JsonAdapter.Factory {
override fun create(type: Type, annotations: MutableSet<out Annotation>, moshi: Moshi): JsonAdapter<*>? {
val rawType = Types.getRawType(type)
if (!rawType.isAnnotationPresent(SerializeNulls::class.java)) {
return null
}
val nextAdapter: JsonAdapter<Any> = moshi.nextAdapter(this, type, annotations)
return nextAdapter.serializeNulls()
}
}

View file

@ -0,0 +1,51 @@
package com.tangem.datasource.utils
import com.google.common.truth.Truth.assertThat
import com.squareup.moshi.JsonClass
import com.squareup.moshi.Moshi
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.TestInstance
// --- DTO ---
@SerializeNulls
@JsonClass(generateAdapter = true)
data class UserWithNulls(val id: String?, val name: String?)
@JsonClass(generateAdapter = true)
data class UserWithoutNulls(val id: String?, val name: String?)
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
class SerializeNullsFactoryTest {
private val moshi = Moshi.Builder()
.add(SerializeNullsFactory)
.build()
@Test
fun `should serialize nulls for annotated class`() {
val adapter = moshi.adapter(UserWithNulls::class.java)
val json = adapter.toJson(UserWithNulls(id = null, name = "John"))
assertThat(json).isEqualTo("""{"id":null,"name":"John"}""")
}
@Test
fun `should skip nulls for non-annotated class`() {
val adapter = moshi.adapter(UserWithoutNulls::class.java)
val json = adapter.toJson(UserWithoutNulls(id = null, name = "John"))
assertThat(json).isEqualTo("""{"name":"John"}""")
}
@Test
fun `should deserialize annotated class correctly`() {
val adapter = moshi.adapter(UserWithNulls::class.java)
val json = """{"id":null,"name":"Jane"}"""
val result = adapter.fromJson(json)
assertThat(result).isEqualTo(UserWithNulls(id = null, name = "Jane"))
}
}

View file

@ -1,7 +1,6 @@
package com.tangem.data.account.converter
import com.tangem.datasource.api.tangemTech.models.account.SaveWalletAccountsResponse
import com.tangem.datasource.api.tangemTech.models.account.WalletAccountDTO
import com.tangem.domain.account.models.AccountList
import com.tangem.domain.models.account.Account
import com.tangem.utils.converter.Converter
@ -21,8 +20,8 @@ internal object SaveWalletAccountsResponseConverter : Converter<AccountList, Sav
)
}
private fun toDTO(account: Account.CryptoPortfolio): WalletAccountDTO {
return WalletAccountDTO(
private fun toDTO(account: Account.CryptoPortfolio): SaveWalletAccountsResponse.AccountDTO {
return SaveWalletAccountsResponse.AccountDTO(
id = account.accountId.value,
name = AccountNameConverter.convert(value = account.accountName),
derivationIndex = account.derivationIndex.value,

View file

@ -86,11 +86,7 @@ internal class DefaultWalletAccountsFetcher @Inject constructor(
tangemTechApi.saveWalletAccounts(
walletId = userWalletId.stringValue,
eTag = eTag,
body = body.copy(
accounts = body.accounts.map {
it.copy(tokens = null, totalTokens = null, totalNetworks = null)
},
),
body = body,
)
}

View file

@ -2,7 +2,6 @@ package com.tangem.data.account.converter
import com.google.common.truth.Truth
import com.tangem.datasource.api.tangemTech.models.account.SaveWalletAccountsResponse
import com.tangem.datasource.api.tangemTech.models.account.WalletAccountDTO
import com.tangem.domain.account.models.AccountList
import com.tangem.domain.models.account.Account
import com.tangem.domain.models.account.AccountName
@ -31,7 +30,7 @@ class SaveWalletAccountsResponseConverterTest {
// Assert
val expected = SaveWalletAccountsResponse(
accounts = listOf(
WalletAccountDTO(
SaveWalletAccountsResponse.AccountDTO(
id = accountList.mainAccount.accountId.value,
name = (accountList.mainAccount.accountName as? AccountName.Custom)?.value,
derivationIndex = accountList.mainAccount.derivationIndex.value,

View file

@ -17,7 +17,7 @@ import com.tangem.core.ui.UiDependencies
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.screen.ComposeActivity
import com.tangem.feature.tester.presentation.accounts.ui.AccountsScreen
import com.tangem.feature.tester.presentation.accounts.viewmodel.AccountsViewModel
import com.tangem.feature.tester.presentation.accounts.viewmodel.TesterAccountsViewModel
import com.tangem.feature.tester.presentation.actions.TesterActionsScreen
import com.tangem.feature.tester.presentation.actions.TesterActionsViewModel
import com.tangem.feature.tester.presentation.environments.ui.EnvironmentTogglesScreen
@ -155,7 +155,7 @@ internal class TesterActivity : ComposeActivity() {
}
composable(route = TesterScreen.ACCOUNTS.name) {
val viewModel = hiltViewModel<AccountsViewModel>().apply {
val viewModel = hiltViewModel<TesterAccountsViewModel>().apply {
setupNavigation(innerTesterRouter)
}
val state by viewModel.uiState.collectAsStateWithLifecycle()

View file

@ -9,9 +9,8 @@ internal data class AccountsUM(
val onBackClick: () -> Unit,
val walletSelector: WalletSelector,
val accountListBottomSheetConfig: AccountListBottomSheetConfig,
val onAccountsClick: () -> Unit,
val onAccountsClick: () -> Boolean,
val onFetchAccountsClick: () -> Unit,
val onCreateMainAccountClick: () -> Unit,
val onClearETagClick: () -> Unit,
) {

View file

@ -62,8 +62,9 @@ internal fun AccountsScreen(state: AccountsUM, modifier: Modifier = Modifier) {
ManageAccountsButtons(
state = state,
onAccountsClick = { context ->
if (state.accountListBottomSheetConfig.accounts.isNotEmpty()) {
state.onAccountsClick()
val isEmpty = state.onAccountsClick()
if (!isEmpty) {
isAccountListShown = true
} else {
Toast.makeText(context, "No accounts found", Toast.LENGTH_SHORT).show()
@ -242,16 +243,4 @@ private fun LazyListScope.ManageAccountsButtons(state: AccountsUM, onAccountsCli
.fillMaxWidth(),
)
}
if (state.accountListBottomSheetConfig.accounts.none { it.isMainAccount }) {
item {
PrimaryButton(
text = "Create Main account",
onClick = state.onCreateMainAccountClick,
modifier = Modifier
.padding(horizontal = 16.dp, vertical = 8.dp)
.fillMaxWidth(),
)
}
}
}

View file

@ -6,13 +6,9 @@ import com.tangem.data.common.cache.etag.ETagsStore
import com.tangem.domain.account.fetcher.SingleAccountListFetcher
import com.tangem.domain.account.models.AccountList
import com.tangem.domain.account.producer.SingleAccountListProducer
import com.tangem.domain.account.repository.AccountsCRUDRepository
import com.tangem.domain.account.supplier.SingleAccountListSupplier
import com.tangem.domain.common.wallets.UserWalletsListRepository
import com.tangem.domain.models.TokensGroupType
import com.tangem.domain.models.TokensSortType
import com.tangem.domain.models.account.Account
import com.tangem.domain.models.account.AccountName
import com.tangem.domain.models.wallet.UserWallet
import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.feature.tester.presentation.accounts.entity.AccountsUM
@ -28,11 +24,10 @@ import javax.inject.Inject
@OptIn(ExperimentalCoroutinesApi::class)
@HiltViewModel
internal class AccountsViewModel @Inject constructor(
internal class TesterAccountsViewModel @Inject constructor(
private val userWalletsListRepository: UserWalletsListRepository,
private val singleAccountListFetcher: SingleAccountListFetcher,
private val singleAccountListSupplier: SingleAccountListSupplier,
private val accountsCRUDRepository: AccountsCRUDRepository,
private val eTagsStore: ETagsStore,
private val dispatchers: CoroutineDispatcherProvider,
) : ViewModel() {
@ -88,7 +83,6 @@ internal class AccountsViewModel @Inject constructor(
),
onAccountsClick = ::updateAccountsList,
onFetchAccountsClick = ::fetchAccounts,
onCreateMainAccountClick = ::createMainAccount,
onClearETagClick = ::clearETag,
)
}
@ -107,8 +101,8 @@ internal class AccountsViewModel @Inject constructor(
}
}
private fun updateAccountsList() {
val userWalletId = uiState.value.walletSelector.selected?.walletId ?: return
private fun updateAccountsList(): Boolean {
val userWalletId = uiState.value.walletSelector.selected?.walletId ?: return false
val accounts = walletAccounts.value[userWalletId]?.accounts
?.filterIsInstance<Account.CryptoPortfolio>()
@ -122,6 +116,8 @@ internal class AccountsViewModel @Inject constructor(
),
)
}
return accounts.isEmpty()
}
private fun fetchAccounts() {
@ -134,28 +130,6 @@ internal class AccountsViewModel @Inject constructor(
}
}
private fun createMainAccount() {
viewModelScope.launch {
val userWallet = uiState.value.walletSelector.selected ?: return@launch
// It's temporary solution to create main account for testing purposes
val accountList = AccountList(
userWalletId = userWallet.walletId,
accounts = setOf(
Account.CryptoPortfolio.createMainAccount(userWallet.walletId).copy(
accountName = AccountName.invoke(value = "Main Account").getOrNull()!!,
),
),
totalAccounts = 1,
sortType = TokensSortType.NONE,
groupType = TokensGroupType.NONE,
)
.getOrNull()!!
accountsCRUDRepository.saveAccounts(accountList)
}
}
private fun clearETag() {
viewModelScope.launch {
val userWallet = uiState.value.walletSelector.selected ?: return@launch