Updated on 2026-08-14

This commit is contained in:
Tangem 2025-11-20 15:18:22 +04:00
parent fcc995a98a
commit 7d1cf1c728
15 changed files with 107 additions and 95 deletions

View file

@ -12,6 +12,7 @@ interface CardTypesResolver {
fun isShibaWallet(): Boolean
// 4.12
fun isWhiteWallet(): Boolean
fun isWallet2(): Boolean

View file

@ -3,6 +3,7 @@ package com.tangem.domain.wallets.extension
import com.tangem.blockchain.blockchains.cardano.CardanoUtils
import com.tangem.blockchain.common.Blockchain
import com.tangem.crypto.hdWallet.DerivationPath
import com.tangem.domain.card.common.util.cardTypesResolver
import com.tangem.domain.card.common.util.hasDerivation
import com.tangem.domain.models.wallet.UserWallet
import com.tangem.domain.wallets.config.curvesConfig
@ -25,4 +26,17 @@ fun UserWallet.hasDerivation(blockchain: Blockchain, derivationPath: String): Bo
}
}
}
}
}
/**
* Check if accounts are supported for the [UserWallet]
*/
val UserWallet.isAccountsSupported
get() = when (this) {
is UserWallet.Hot -> true
is UserWallet.Cold -> {
with(this.cardTypesResolver) {
isMultiwalletAllowed() && !isWhiteWallet()
}
}
}

View file

@ -2,26 +2,8 @@
<SmellBaseline>
<ManuallySuppressedIssues/>
<CurrentIssues>
<ID>BooleanPropertyNaming:AccountItemsDelegate.kt$AccountItemsDelegate$val addAccountEnabled = accounts.size &lt; AccountList.MAX_ACCOUNTS_COUNT</ID>
<ID>BooleanPropertyNaming:AccountItemsDelegate.kt$AccountItemsDelegate$val showDescription = accounts.size &gt; 1</ID>
<ID>BooleanPropertyNaming:WalletSettingsItemUM.kt$WalletSettingsAccountsUM.Footer$val showDescription: Boolean</ID>
<ID>BooleanPropertyNaming:WalletSettingsItemUM.kt$WalletSettingsAccountsUM.Footer.AddAccountUM$val addAccountEnabled: Boolean</ID>
<ID>BooleanPropertyNaming:WalletSettingsModel.kt$WalletSettingsModel$val accountsFeatureEnabled = accountsFeatureToggles.isFeatureEnabled</ID>
<ID>BooleanPropertyNaming:WalletSettingsUM.kt$WalletSettingsUM$val requestPushNotificationsPermission: Boolean = false</ID>
<ID>BooleanPropertyNaming:WalletSettingsUM.kt$WalletSettingsUM$val walletUpgradeDismissed: Boolean = false</ID>
<ID>MultilineLambdaItParameter:RenameWalletModel.kt$RenameWalletModel${ Timber.e("Unable to rename wallet: $it") showRenameWalletError(error = it, updatedName = newName.text) }</ID>
<ID>MultilineLambdaItParameter:RenameWalletModel.kt$RenameWalletModel${ val newName = value.text it.copy( walletNameValue = value, isConfirmEnabled = getConfirmButtonAvailability(newName), error = getErrorOrNull(newName), ) }</ID>
<ID>MultilineLambdaItParameter:WalletSettingsModel.kt$WalletSettingsModel${ Timber.e("Unable to delete wallet: $it") messageSender.send( message = SnackbarMessage(resourceReference(R.string.common_unknown_error)), ) return@launch }</ID>
<ID>MultilineLambdaItParameter:WalletSettingsModel.kt$WalletSettingsModel${ val id = (it as? WalletSettingsAccountsUM.Account)?.state?.id ?: return@mapNotNull null AccountId.forCryptoPortfolio(userWalletId = params.userWalletId, value = id).getOrNull() }</ID>
<ID>NonBooleanPropertyPrefixedWithIs:AccountItemsDelegate.kt$AccountItemsDelegate$private val isAccountsModeEnabledUseCase: IsAccountsModeEnabledUseCase</ID>
<ID>NonBooleanPropertyPrefixedWithIs:WalletSettingsModel.kt$WalletSettingsModel$private val isDemoCardUseCase: IsDemoCardUseCase</ID>
<ID>NonBooleanPropertyPrefixedWithIs:WalletSettingsModel.kt$WalletSettingsModel$private val isUpgradeWalletNotificationEnabledUseCase: IsUpgradeWalletNotificationEnabledUseCase</ID>
<ID>PropertyUsedBeforeDeclaration:NetworksAvailableForNotificationsModel.kt$NetworksAvailableForNotificationsModel$_state</ID>
<ID>PropertyUsedBeforeDeclaration:RenameWalletModel.kt$RenameWalletModel$_state</ID>
<ID>UnnecessaryLet:AccountItemsDelegate.kt$AccountItemsDelegate$let(::add)</ID>
<ID>UnnecessaryLet:ItemsBuilder.kt$ItemsBuilder$let(::add)</ID>
<ID>UnnecessaryLet:PreviewWalletSettingsComponent.kt$PreviewWalletSettingsComponent$let(::add)</ID>
<ID>UseEmptyCounterpart:Settings.kt$Settings$mapOf()</ID>
<ID>UseEmptyCounterpart:WalletSettingsAnalyticEvents.kt$WalletSettingsAnalyticEvents$mapOf()</ID>
</CurrentIssues>
</SmellBaseline>

View file

@ -5,7 +5,7 @@ import com.tangem.core.analytics.models.AnalyticsEvent
internal sealed class Settings(
category: String = "Settings",
event: String,
params: Map<String, String> = mapOf(),
params: Map<String, String> = emptyMap(),
) : AnalyticsEvent(category, event, params) {
data object ButtonCreateBackup : Settings(event = "Button - Create Backup")

View file

@ -7,7 +7,7 @@ import com.tangem.core.analytics.models.AnalyticsParam.Key.STATUS
internal sealed class WalletSettingsAnalyticEvents(
category: String = "Settings / Wallet",
event: String,
params: Map<String, String> = mapOf(),
params: Map<String, String> = emptyMap(),
) : AnalyticsEvent(category, event, params) {
data class NftToggleSwitch(val enabled: AnalyticsParam.OnOffState) : WalletSettingsAnalyticEvents(

View file

@ -100,7 +100,7 @@ internal class DefaultWalletSettingsComponent @AssistedInject constructor(
permission = PUSH_PERMISSION,
)
if (state.requestPushNotificationsPermission) {
if (state.hasRequestPushNotificationsPermission) {
LaunchedEffect(Unit) {
requestPushPermission()
}

View file

@ -19,19 +19,18 @@ internal class NetworksAvailableForNotificationsModel @Inject constructor(
override val dispatchers: CoroutineDispatcherProvider,
) : Model() {
val state: StateFlow<NetworksAvailableForNotificationsUM> get() = _state
private val _state = MutableStateFlow(
value = NetworksAvailableForNotificationsUM(
networks = persistentListOf(),
isLoading = true,
),
)
val state: StateFlow<NetworksAvailableForNotificationsUM>
field = MutableStateFlow(
value = NetworksAvailableForNotificationsUM(
networks = persistentListOf(),
isLoading = true,
),
)
init {
modelScope.launch {
getNetworksAvailableForNotificationsUseCase().onRight { networks ->
_state.update {
state.update {
it.copy(networks = networks.toImmutableList(), isLoading = false)
}
}

View file

@ -50,27 +50,26 @@ internal class RenameWalletModel @Inject constructor(
private val initialName = params.currentName
private val walletNames by lazy(getWalletNamesUseCase::invoke)
val state: StateFlow<RenameWalletUM> get() = _state
private val _state = MutableStateFlow(
value = RenameWalletUM(
walletNameValue = TextFieldValue(text = params.currentName),
onValueChange = ::onValueChange,
isConfirmEnabled = false,
onConfirmClick = ::onConfirmClick,
error = null,
),
)
val state: StateFlow<RenameWalletUM>
field = MutableStateFlow(
value = RenameWalletUM(
walletNameValue = TextFieldValue(text = params.currentName),
onValueChange = ::onValueChange,
isConfirmEnabled = false,
onConfirmClick = ::onConfirmClick,
error = null,
),
)
fun dismiss() {
params.onDismiss()
}
private fun onValueChange(value: TextFieldValue) {
_state.update {
state.update { currentState ->
val newName = value.text
it.copy(
currentState.copy(
walletNameValue = value,
isConfirmEnabled = getConfirmButtonAvailability(newName),
error = getErrorOrNull(newName),
@ -100,12 +99,12 @@ internal class RenameWalletModel @Inject constructor(
private fun onConfirmClick() {
modelScope.launch {
withContext(NonCancellable) {
val newName = _state.value.walletNameValue
val newName = state.value.walletNameValue
renameWalletUseCase(userWalletId = params.userWalletId, name = newName.text)
.onLeft {
Timber.e("Unable to rename wallet: $it")
showRenameWalletError(error = it, updatedName = newName.text)
.onLeft { error ->
Timber.e("Unable to rename wallet: $error")
showRenameWalletError(error = error, updatedName = newName.text)
}
}
}

View file

@ -74,7 +74,7 @@ internal class PreviewWalletSettingsComponent : WalletSettingsComponent {
accountsUM = previewAccounts(),
cardItem = previewCardBlock(),
),
requestPushNotificationsPermission = false,
hasRequestPushNotificationsPermission = false,
onPushNotificationPermissionGranted = {},
accountReorderUM = AccountReorderUM(
isDragEnabled = true,
@ -83,17 +83,17 @@ internal class PreviewWalletSettingsComponent : WalletSettingsComponent {
),
)
private fun previewAccounts() = buildList {
private fun previewAccounts() = listOf(
WalletSettingsAccountsUM.Header(
id = "accounts_header",
text = resourceReference(R.string.common_accounts),
).let(::add)
add(WalletSettingsAccountsUM.Account(accountItem))
),
WalletSettingsAccountsUM.Account(accountItem),
WalletSettingsAccountsUM.Footer(
id = "accounts_footer",
addAccount = AddAccountUM(
title = resourceReference(R.string.account_form_title_create),
addAccountEnabled = true,
isAddAccountEnabled = true,
onAddAccountClick = {},
),
archivedAccounts = BlockUM(
@ -101,10 +101,10 @@ internal class PreviewWalletSettingsComponent : WalletSettingsComponent {
iconRes = R.drawable.ic_archive_24,
onClick = {},
),
showDescription = true,
shouldShowDescription = true,
description = resourceReference(R.string.account_reorder_description),
).let(::add)
}
),
)
private fun previewCardBlock() = WalletSettingsItemUM.CardBlock(
id = "wallet_name",

View file

@ -74,13 +74,13 @@ internal sealed class WalletSettingsAccountsUM : WalletSettingsItemUM() {
override val id: String,
val addAccount: AddAccountUM,
val archivedAccounts: BlockUM?,
val showDescription: Boolean,
val shouldShowDescription: Boolean,
val description: TextReference,
) : WalletSettingsAccountsUM() {
data class AddAccountUM(
val title: TextReference,
val addAccountEnabled: Boolean,
val isAddAccountEnabled: Boolean,
val onAddAccountClick: () -> Unit,
)
}

View file

@ -7,11 +7,11 @@ import kotlinx.collections.immutable.PersistentList
internal data class WalletSettingsUM(
val popBack: () -> Unit,
val items: PersistentList<WalletSettingsItemUM>,
val requestPushNotificationsPermission: Boolean = false,
val hasRequestPushNotificationsPermission: Boolean = false,
val onPushNotificationPermissionGranted: (Boolean) -> Unit,
val accountReorderUM: AccountReorderUM,
val isWalletBackedUp: Boolean = true,
val walletUpgradeDismissed: Boolean = false,
val isWalletUpgradeDismissed: Boolean = false,
)
internal data class AccountReorderUM(

View file

@ -98,10 +98,10 @@ internal class WalletSettingsModel @Inject constructor(
value = WalletSettingsUM(
popBack = router::pop,
items = persistentListOf(),
requestPushNotificationsPermission = false,
hasRequestPushNotificationsPermission = false,
onPushNotificationPermissionGranted = ::onPushNotificationPermissionGranted,
isWalletBackedUp = true,
walletUpgradeDismissed = false,
isWalletUpgradeDismissed = false,
accountReorderUM = AccountReorderUM(
isDragEnabled = false,
onMove = ::onAccountReorder,
@ -124,7 +124,7 @@ internal class WalletSettingsModel @Inject constructor(
.conflate(),
flow3 = isUpgradeWalletNotificationEnabledUseCase(params.userWalletId),
flow4 = walletCardItemDelegate.cardItemFlow(wallet),
flow5 = accountItemsDelegate.loadAccount(),
flow5 = accountItemsDelegate.loadAccount(wallet),
) { nftEnabled, notificationsEnabled, isUpgradeNotificationEnabled, cardItem, accountList ->
val isWalletBackedUp = when (wallet) {
is UserWallet.Hot -> wallet.backedUp
@ -182,7 +182,7 @@ internal class WalletSettingsModel @Inject constructor(
isUpgradeNotificationEnabled: Boolean,
accountList: List<WalletSettingsAccountsUM>,
): PersistentList<WalletSettingsItemUM> {
val accountsFeatureEnabled = accountsFeatureToggles.isFeatureEnabled
val isAccountsFeatureEnabled = accountsFeatureToggles.isFeatureEnabled
val isMultiCurrency = when (userWallet) {
is UserWallet.Cold -> userWallet.isMultiCurrency
is UserWallet.Hot -> true
@ -198,7 +198,7 @@ internal class WalletSettingsModel @Inject constructor(
is UserWallet.Cold -> userWallet.scanResponse.card.backupStatus == CardDTO.BackupStatus.NoBackup
is UserWallet.Hot -> false
},
isManageTokensAvailable = if (accountsFeatureEnabled) {
isManageTokensAvailable = if (isAccountsFeatureEnabled) {
isMultiCurrency && accountList.count { it is WalletSettingsAccountsUM.Account } == 0
} else {
isMultiCurrency
@ -221,7 +221,7 @@ internal class WalletSettingsModel @Inject constructor(
router.push(
AppRoute.ManageTokens(
source = Source.SETTINGS,
portfolioId = if (accountsFeatureEnabled) {
portfolioId = if (isAccountsFeatureEnabled) {
PortfolioId(
accountId = AccountId.forMainCryptoPortfolio(userWalletId = userWallet.walletId),
)
@ -244,8 +244,8 @@ internal class WalletSettingsModel @Inject constructor(
}
private fun forgetWallet() = modelScope.launch {
val hasUserWallets = deleteWalletUseCase(params.userWalletId).getOrElse {
Timber.e("Unable to delete wallet: $it")
val hasUserWallets = deleteWalletUseCase(params.userWalletId).getOrElse { error ->
Timber.e("Unable to delete wallet: $error")
messageSender.send(
message = SnackbarMessage(resourceReference(R.string.common_unknown_error)),
@ -293,7 +293,7 @@ internal class WalletSettingsModel @Inject constructor(
if (isChecked) {
state.update { value ->
value.copy(
requestPushNotificationsPermission = true,
hasRequestPushNotificationsPermission = true,
)
}
} else {
@ -315,7 +315,7 @@ internal class WalletSettingsModel @Inject constructor(
analyticsEventHandler.send(PushNotificationAnalyticEvents.PermissionStatus(isGranted))
state.update { value ->
value.copy(
requestPushNotificationsPermission = false,
hasRequestPushNotificationsPermission = false,
)
}
if (isGranted) {
@ -449,8 +449,8 @@ internal class WalletSettingsModel @Inject constructor(
}
private fun onAccountDragStopped() {
val accountIds = state.value.items.mapNotNull {
val id = (it as? WalletSettingsAccountsUM.Account)?.state?.id
val accountIds = state.value.items.mapNotNull { itemUM ->
val id = (itemUM as? WalletSettingsAccountsUM.Account)?.state?.id
?: return@mapNotNull null
AccountId.forCryptoPortfolio(userWalletId = params.userWalletId, value = id).getOrNull()

View file

@ -393,7 +393,7 @@ private fun AccountsFooter(model: WalletSettingsAccountsUM.Footer, modifier: Mod
}
}
}
if (!model.showDescription) return
if (!model.shouldShowDescription) return
SpacerH8()
Text(
modifier = Modifier.padding(horizontal = TangemTheme.dimens.spacing12),
@ -411,7 +411,7 @@ private fun AddAccountRow(model: WalletSettingsAccountsUM.Footer.AddAccountUM, m
val iconTint: Color
val backgroundColor: Color
val textColor: Color
if (model.addAccountEnabled) {
if (model.isAddAccountEnabled) {
iconTint = TangemTheme.colors.icon.accent
backgroundColor = TangemTheme.colors.icon.accent.copy(alpha = 0.1f)
textColor = TangemTheme.colors.text.accent

View file

@ -22,7 +22,9 @@ import com.tangem.domain.balancehiding.GetBalanceHidingSettingsUseCase
import com.tangem.domain.models.account.Account
import com.tangem.domain.models.account.AccountId
import com.tangem.domain.models.account.AccountStatus
import com.tangem.domain.models.wallet.UserWallet
import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.domain.wallets.extension.isAccountsSupported
import com.tangem.feature.walletsettings.component.WalletSettingsComponent
import com.tangem.feature.walletsettings.entity.WalletSettingsAccountsUM
import com.tangem.feature.walletsettings.entity.WalletSettingsAccountsUM.Footer.AddAccountUM
@ -48,8 +50,8 @@ internal class AccountItemsDelegate @Inject constructor(
private val userWalletId = paramsContainer.require<WalletSettingsComponent.Params>().userWalletId
fun loadAccount(): Flow<List<WalletSettingsAccountsUM>> {
if (!accountsFeatureToggles.isFeatureEnabled) return flowOf(emptyList())
fun loadAccount(wallet: UserWallet): Flow<List<WalletSettingsAccountsUM>> {
if (!accountsFeatureToggles.isFeatureEnabled || !wallet.isAccountsSupported) return flowOf(emptyList())
return combine(
flow = singleAccountStatusListSupplier(userWalletId),
@ -83,26 +85,29 @@ internal class AccountItemsDelegate @Inject constructor(
}
val accounts = accountStatusList.accountStatuses
WalletSettingsAccountsUM.Header(
val header = WalletSettingsAccountsUM.Header(
id = "accounts_header",
text = resourceReference(R.string.common_accounts),
).let(::add)
)
add(header)
if (isAccountsMode) {
addAll(accounts.map(::mapAccount).applySortingOrder(order = accountsOrder))
}
val addAccountEnabled = accounts.size < AccountList.MAX_ACCOUNTS_COUNT
val showDescription = accounts.size > 1
val isAddAccountEnabled = accounts.size < AccountList.MAX_ACCOUNTS_COUNT
val shouldShowDescription = accounts.size > 1
val isArchivedAccountsEnabled = accountStatusList.accountStatuses.size != accountStatusList.totalAccounts
WalletSettingsAccountsUM.Footer(
val footer = WalletSettingsAccountsUM.Footer(
id = "accounts_footer",
addAccount = AddAccountUM(
title = resourceReference(R.string.account_form_title_create),
addAccountEnabled = addAccountEnabled,
isAddAccountEnabled = isAddAccountEnabled,
onAddAccountClick = {
if (addAccountEnabled) openAddAccount(userWalletId) else canNotAddAccountDialog()
if (isAddAccountEnabled) openAddAccount(userWalletId) else canNotAddAccountDialog()
},
),
archivedAccounts = if (isArchivedAccountsEnabled) {
@ -114,9 +119,11 @@ internal class AccountItemsDelegate @Inject constructor(
} else {
null
},
showDescription = showDescription,
shouldShowDescription = shouldShowDescription,
description = resourceReference(R.string.account_reorder_description),
).let(::add)
)
add(footer)
}
private fun List<WalletSettingsAccountsUM>.applySortingOrder(

View file

@ -181,7 +181,7 @@ internal class ItemsBuilder @Inject constructor(
val isHotWallet = userWallet is UserWallet.Hot
if (isHotWallet) {
val hasBackup = userWallet.backedUp
BlockUM(
val backupBlock = BlockUM(
text = resourceReference(R.string.common_backup),
iconRes = R.drawable.ic_more_cards_24,
onClick = { router.push(AppRoute.WalletBackup(userWalletId)) },
@ -195,39 +195,49 @@ internal class ItemsBuilder @Inject constructor(
),
)
},
).let(::add)
)
add(backupBlock)
}
if (isManageTokensAvailable) {
BlockUM(
val manageTokensBlock = BlockUM(
text = resourceReference(R.string.add_tokens_title),
iconRes = R.drawable.ic_tether_24,
onClick = onManageTokensClick,
).let(::add)
)
add(manageTokensBlock)
}
if (isLinkMoreCardsAvailable) {
BlockUM(
val linkMoreCardsBlock = BlockUM(
text = resourceReference(R.string.details_row_title_create_backup),
iconRes = R.drawable.ic_more_cards_24,
onClick = onLinkMoreCardsClick,
).let(::add)
)
add(linkMoreCardsBlock)
}
if (!isHotWallet) {
BlockUM(
val deviceSettingsBlock = BlockUM(
text = resourceReference(R.string.card_settings_title),
iconRes = R.drawable.ic_card_settings_24,
onClick = { router.push(AppRoute.CardSettings(userWalletId)) },
).let(::add)
)
add(deviceSettingsBlock)
}
if (isReferralAvailable) {
BlockUM(
val referralBlock = BlockUM(
text = resourceReference(R.string.details_referral_title),
iconRes = R.drawable.ic_add_friends_24,
onClick = onReferralClick,
).let(::add)
)
add(referralBlock)
}
}.toImmutableList(),
)