Updated on 2026-08-14
This commit is contained in:
parent
231c97e371
commit
373880691e
12 changed files with 129 additions and 61 deletions
|
|
@ -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,
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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")
|
||||
|
|
|
|||
|
|
@ -0,0 +1,5 @@
|
|||
package com.tangem.datasource.utils
|
||||
|
||||
@Target(AnnotationTarget.CLASS)
|
||||
@Retention(AnnotationRetention.RUNTIME)
|
||||
annotation class SerializeNulls
|
||||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
|
@ -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"))
|
||||
}
|
||||
}
|
||||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
) {
|
||||
|
||||
|
|
|
|||
|
|
@ -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(),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
Loading…
Add table
Add a link
Reference in a new issue