Updated on 2026-08-14

This commit is contained in:
Tangem 2025-12-15 14:59:31 +07:00
parent b316ef4d87
commit 370690a359
42 changed files with 486 additions and 35 deletions

View file

@ -57,6 +57,7 @@ interface PortfolioSelectorController {
* combine and update with your Feature data and [PortfolioFetcher.data]
*/
val isEnabled: MutableStateFlow<(UserWallet, AccountStatus) -> Boolean>
suspend fun isAccountModeSync(): Boolean
fun selectAccount(accountId: AccountId?)
fun selectedAccountWithData(portfolioFetcher: PortfolioFetcher): Flow<Pair<UserWallet, AccountStatus>?>

View file

@ -0,0 +1,100 @@
package com.tangem.features.account.analytics
import com.tangem.core.analytics.models.AnalyticsEvent
import com.tangem.domain.models.account.AccountName
import com.tangem.domain.models.account.CryptoPortfolioIcon
import com.tangem.features.account.AccountCreateEditComponent
sealed class AccountSettingsAnalyticEvents(
category: String = "Settings / Account",
event: String,
params: Map<String, String> = emptyMap(),
) : AnalyticsEvent(category, event, params) {
class AccountSettingsScreenOpened : AccountSettingsAnalyticEvents(
event = "Account Settings Screen Opened",
)
class ButtonManageTokens : AccountSettingsAnalyticEvents(
event = "Button - Manage Tokens",
)
class ButtonArchiveAccount : AccountSettingsAnalyticEvents(
event = "Button - Archive Account",
)
class ButtonArchiveAccountConfirmation : AccountSettingsAnalyticEvents(
event = "Button - Archive Account Confirmation",
)
class ButtonCancelAccountArchivation : AccountSettingsAnalyticEvents(
event = "Button - Cancel Account Archivation",
)
class AccountArchived : AccountSettingsAnalyticEvents(
event = "Account Archived",
)
class ButtonEdit : AccountSettingsAnalyticEvents(
event = "Button - Edit",
)
class AccountEditScreenOpened : AccountSettingsAnalyticEvents(
event = "Account Edit Screen Opened",
)
class ButtonSave(
val name: AccountName,
val icon: CryptoPortfolioIcon,
) : AccountSettingsAnalyticEvents(
event = "Button - Save",
params = buildMap {
val accountName = when (name) {
is AccountName.Custom -> name.value
AccountName.DefaultMain -> "DefaultMain"
}
put("Name", accountName)
put("Color", icon.color.name)
put("Icon", icon.value.name)
},
)
class ButtonAddNewAccount(
val name: AccountName,
val icon: CryptoPortfolioIcon,
val derivationIndex: Int,
) : AccountSettingsAnalyticEvents(
event = "Button - Add New Account",
params = buildMap {
val accountName = when (name) {
is AccountName.Custom -> name.value
AccountName.DefaultMain -> "DefaultMain"
}
put("Name", accountName)
put("Color", icon.color.name)
put("Icon", icon.value.name)
put("Derivation", derivationIndex.toString())
},
)
class AccountError(
val source: Source,
val error: String,
) : AccountSettingsAnalyticEvents(
event = "Account Error",
params = buildMap {
put("Error", error)
},
)
enum class Source(val value: String) {
NEW_ACCOUNT("New Account"), EDIT("Edit"), ARCHIVE("Archive")
}
companion object {
fun AccountCreateEditComponent.Params.toAnalyticSource() = when (this) {
is AccountCreateEditComponent.Params.Create -> Source.NEW_ACCOUNT
is AccountCreateEditComponent.Params.Edit -> Source.EDIT
}
}
}

View file

@ -0,0 +1,26 @@
package com.tangem.features.account.analytics
import com.tangem.core.analytics.models.AnalyticsEvent
sealed class WalletSettingsAccountAnalyticEvents(
category: String = "Settings / Wallet Settings",
event: String,
params: Map<String, String> = emptyMap(),
) : AnalyticsEvent(category, event, params) {
class AccountCreated : WalletSettingsAccountAnalyticEvents(
event = "Account Created",
)
class AccountRecovered : WalletSettingsAccountAnalyticEvents(
event = "Account Recovered",
)
class ArchivedAccountsScreenOpened : WalletSettingsAccountAnalyticEvents(
event = "Archived Accounts Screen Opened",
)
class ButtonRecoverAccount : WalletSettingsAccountAnalyticEvents(
event = "Button - Recover Account",
)
}

View file

@ -1,5 +1,6 @@
package com.tangem.features.account.archived
import com.tangem.core.analytics.api.AnalyticsEventHandler
import com.tangem.core.analytics.api.AnalyticsExceptionHandler
import com.tangem.core.analytics.models.ExceptionAnalyticsEvent
import com.tangem.core.decompose.model.Model
@ -18,6 +19,7 @@ import com.tangem.domain.account.status.usecase.RecoverCryptoPortfolioUseCase
import com.tangem.domain.account.usecase.GetArchivedAccountsUseCase
import com.tangem.domain.models.account.AccountId
import com.tangem.features.account.ArchivedAccountListComponent
import com.tangem.features.account.analytics.WalletSettingsAccountAnalyticEvents
import com.tangem.features.account.archived.entity.AccountArchivedUM
import com.tangem.features.account.archived.entity.AccountArchivedUMBuilder
import com.tangem.features.account.archived.entity.AccountArchivedUMBuilder.Companion.toggleProgress
@ -40,6 +42,7 @@ internal class ArchivedAccountListModel @Inject constructor(
private val recoverCryptoPortfolioUseCase: RecoverCryptoPortfolioUseCase,
private val getArchivedAccountsUseCase: GetArchivedAccountsUseCase,
private val umBuilder: AccountArchivedUMBuilder,
private val analyticsEventHandler: AnalyticsEventHandler,
private val analyticsExceptionHandler: AnalyticsExceptionHandler,
) : Model() {
@ -52,6 +55,7 @@ internal class ArchivedAccountListModel @Inject constructor(
private val getArchivedAccountsJob = JobHolder()
init {
analyticsEventHandler.send(WalletSettingsAccountAnalyticEvents.ArchivedAccountsScreenOpened())
getArchivedAccounts()
}
@ -96,6 +100,7 @@ internal class ArchivedAccountListModel @Inject constructor(
}
private fun recoverCryptoPortfolio(accountId: AccountId) = modelScope.launch {
analyticsEventHandler.send(WalletSettingsAccountAnalyticEvents.ButtonRecoverAccount())
uiState.update { it.toggleProgress(accountId, isLoading = true) }
val result = withContext(dispatchers.default) {
recoverCryptoPortfolioUseCase(accountId)
@ -148,6 +153,7 @@ internal class ArchivedAccountListModel @Inject constructor(
}
private fun showSuccessRecoverMessage() {
analyticsEventHandler.send(WalletSettingsAccountAnalyticEvents.AccountRecovered())
val message = resourceReference(R.string.account_recover_success_message)
messageSender.send(ToastMessage(message = message))
}

View file

@ -4,6 +4,7 @@ import androidx.annotation.StringRes
import com.tangem.common.ui.account.AccountNameUM
import com.tangem.common.ui.account.toDomain
import com.tangem.common.ui.account.toUM
import com.tangem.core.analytics.api.AnalyticsEventHandler
import com.tangem.core.analytics.api.AnalyticsExceptionHandler
import com.tangem.core.analytics.models.ExceptionAnalyticsEvent
import com.tangem.core.decompose.di.ModelScoped
@ -25,6 +26,9 @@ import com.tangem.domain.models.account.CryptoPortfolioIcon
import com.tangem.domain.models.account.DerivationIndex
import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.features.account.AccountCreateEditComponent
import com.tangem.features.account.analytics.AccountSettingsAnalyticEvents
import com.tangem.features.account.analytics.AccountSettingsAnalyticEvents.Companion.toAnalyticSource
import com.tangem.features.account.analytics.WalletSettingsAccountAnalyticEvents
import com.tangem.features.account.createedit.entity.AccountCreateEditUM
import com.tangem.features.account.createedit.entity.AccountCreateEditUMBuilder
import com.tangem.features.account.createedit.entity.AccountCreateEditUMBuilder.Companion.portfolioIcon
@ -54,6 +58,7 @@ internal class AccountCreateEditModel @Inject constructor(
private val addCryptoPortfolioUseCase: AddCryptoPortfolioUseCase,
private val getUnoccupiedAccountIndexUseCase: GetUnoccupiedAccountIndexUseCase,
private val analyticsExceptionHandler: AnalyticsExceptionHandler,
private val analyticsEventHandler: AnalyticsEventHandler,
) : Model() {
private val params = paramsContainer.require<AccountCreateEditComponent.Params>()
@ -65,6 +70,8 @@ internal class AccountCreateEditModel @Inject constructor(
init {
if (params is AccountCreateEditComponent.Params.Create) {
updateDerivationInfo(userWalletId = params.userWalletId)
} else {
analyticsEventHandler.send(AccountSettingsAnalyticEvents.AccountEditScreenOpened())
}
}
@ -105,6 +112,12 @@ internal class AccountCreateEditModel @Inject constructor(
val icon = state.account.portfolioIcon.toDomain()
val index = state.account.derivationInfo.index ?: return
val derivationIndex = DerivationIndex(value = index).getOrNull() ?: return
val event = AccountSettingsAnalyticEvents.ButtonAddNewAccount(
name = name,
icon = icon,
derivationIndex = derivationIndex.value,
)
analyticsEventHandler.send(event)
uiState.value = uiState.value.toggleProgress(showProgress = true)
val result = addCryptoPortfolioUseCase(
@ -118,12 +131,22 @@ internal class AccountCreateEditModel @Inject constructor(
result
.onLeft(::handleAddAccountError)
.onRight {
analyticsEventHandler.send(WalletSettingsAccountAnalyticEvents.AccountCreated())
showMessage(R.string.account_create_success_message)
router.pop()
}
}
private fun handleAddAccountError(error: AddCryptoPortfolioUseCase.Error) {
val event = AccountSettingsAnalyticEvents.AccountError(
source = params.toAnalyticSource(),
error = when (error) {
is AddCryptoPortfolioUseCase.Error.AccountListRequirementsNotMet -> error.cause.tag
is AddCryptoPortfolioUseCase.Error.DataOperationFailed -> error.cause.message.orEmpty()
},
)
analyticsEventHandler.send(event)
val isDuplicateAccountNamesError = (error as? AddCryptoPortfolioUseCase.Error.AccountListRequirementsNotMet)
?.cause is AccountList.Error.DuplicateAccountNames
when {
@ -141,6 +164,7 @@ internal class AccountCreateEditModel @Inject constructor(
val icon = state.account.portfolioIcon.toDomain()
val isNewName = name != params.account.accountName
val isNewIcon = icon != params.account.portfolioIcon
analyticsEventHandler.send(AccountSettingsAnalyticEvents.ButtonSave(name, icon))
uiState.value = uiState.value.toggleProgress(showProgress = true)
val result = updateCryptoPortfolioUseCase(
@ -159,6 +183,15 @@ internal class AccountCreateEditModel @Inject constructor(
}
private fun handleEditAccountError(error: UpdateCryptoPortfolioUseCase.Error) {
val event = AccountSettingsAnalyticEvents.AccountError(
source = params.toAnalyticSource(),
error = when (error) {
is UpdateCryptoPortfolioUseCase.Error.AccountListRequirementsNotMet -> error.cause.tag
is UpdateCryptoPortfolioUseCase.Error.DataOperationFailed -> error.cause.message.orEmpty()
UpdateCryptoPortfolioUseCase.Error.NothingToUpdate -> error::class.simpleName.orEmpty()
},
)
analyticsEventHandler.send(event)
val isDuplicateAccountNamesError = (error as? UpdateCryptoPortfolioUseCase.Error.AccountListRequirementsNotMet)
?.cause is AccountList.Error.DuplicateAccountNames
when {

View file

@ -2,6 +2,7 @@ package com.tangem.features.account.details
import com.tangem.common.routing.AppRoute
import com.tangem.common.ui.account.toUM
import com.tangem.core.analytics.api.AnalyticsEventHandler
import com.tangem.core.decompose.di.ModelScoped
import com.tangem.core.decompose.model.Model
import com.tangem.core.decompose.model.ParamsContainer
@ -20,6 +21,7 @@ import com.tangem.domain.models.account.Account
import com.tangem.domain.models.wallet.isMultiCurrency
import com.tangem.domain.wallets.usecase.GetUserWalletUseCase
import com.tangem.features.account.AccountDetailsComponent
import com.tangem.features.account.analytics.AccountSettingsAnalyticEvents
import com.tangem.features.account.createedit.entity.AccountCreateEditUMBuilder.Companion.portfolioIcon
import com.tangem.features.account.details.entity.AccountDetailsUM
import com.tangem.features.account.details.entity.AccountDetailsUM.ArchiveMode
@ -37,6 +39,7 @@ internal class AccountDetailsModel @Inject constructor(
override val dispatchers: CoroutineDispatcherProvider,
private val archiveCryptoPortfolioUseCase: ArchiveCryptoPortfolioUseCase,
singleAccountSupplier: SingleAccountSupplier,
private val analyticsEventHandler: AnalyticsEventHandler,
private val getUserWalletUseCase: GetUserWalletUseCase,
) : Model() {
@ -48,12 +51,14 @@ internal class AccountDetailsModel @Inject constructor(
private val accountId = params.account.accountId
init {
analyticsEventHandler.send(AccountSettingsAnalyticEvents.AccountSettingsScreenOpened())
singleAccountSupplier(SingleAccountProducer.Params(accountId))
.onEach { account -> uiState.update { buildUI(account) } }
.launchIn(modelScope)
}
private fun onEditAccountClick(account: Account) {
analyticsEventHandler.send(AccountSettingsAnalyticEvents.ButtonEdit())
router.push(AppRoute.EditAccount(account))
}
@ -62,17 +67,21 @@ internal class AccountDetailsModel @Inject constructor(
source = AppRoute.ManageTokens.Source.SETTINGS,
portfolioId = PortfolioId(account.accountId),
)
analyticsEventHandler.send(AccountSettingsAnalyticEvents.ButtonManageTokens())
router.push(route)
}
private fun onArchiveAccountClick() {
analyticsEventHandler.send(AccountSettingsAnalyticEvents.ButtonArchiveAccount())
confirmArchiveDialog()
}
private fun confirmArchiveDialog() {
val secondAction = EventMessageAction(
title = resourceReference(R.string.common_cancel),
onClick = {},
onClick = {
analyticsEventHandler.send(AccountSettingsAnalyticEvents.ButtonCancelAccountArchivation())
},
)
val firstAction = EventMessageAction(
title = resourceReference(R.string.account_details_archive_action),
@ -90,6 +99,7 @@ internal class AccountDetailsModel @Inject constructor(
}
private fun archiveCryptoPortfolio() = modelScope.launch {
analyticsEventHandler.send(AccountSettingsAnalyticEvents.ButtonArchiveAccountConfirmation())
uiState.update { it.toggleProgress(true) }
archiveCryptoPortfolioUseCase(accountId)
.onLeft { error ->
@ -97,6 +107,7 @@ internal class AccountDetailsModel @Inject constructor(
uiState.update { it.toggleProgress(false) }
}
.onRight {
analyticsEventHandler.send(AccountSettingsAnalyticEvents.AccountArchived())
val message = resourceReference(R.string.account_archive_success_message)
messageSender.send(ToastMessage(message = message))
router.pop()
@ -104,6 +115,11 @@ internal class AccountDetailsModel @Inject constructor(
}
private fun failedArchiveDialog(error: ArchiveCryptoPortfolioUseCase.Error) {
val event = AccountSettingsAnalyticEvents.AccountError(
source = AccountSettingsAnalyticEvents.Source.ARCHIVE,
error = error.tag,
)
analyticsEventHandler.send(event)
val titleRes = R.string.common_something_went_wrong
val messageRes = when (error) {
is ArchiveCryptoPortfolioUseCase.Error.CriticalTechError.AccountListRequirementsNotMet,

View file

@ -29,6 +29,8 @@ internal class DefaultPortfolioSelectorController @Inject constructor(
override val isEnabled: MutableStateFlow<(UserWallet, AccountStatus) -> Boolean> = MutableStateFlow { _, _ -> true }
override suspend fun isAccountModeSync(): Boolean = isAccountsModeEnabledUseCase.invokeSync()
override fun selectAccount(accountId: AccountId?) {
_selectedAccount.tryEmit(accountId)
}