Updated on 2026-08-14

This commit is contained in:
Tangem 2025-08-14 14:29:51 +07:00
parent ba6260e063
commit 948ed18ca6
14 changed files with 471 additions and 11 deletions

View file

@ -9,6 +9,7 @@ import com.tangem.feature.stories.api.StoriesComponent
import com.tangem.feature.usedesk.api.UsedeskComponent
import com.tangem.feature.walletsettings.component.WalletSettingsComponent
import com.tangem.features.account.AccountCreateEditComponent
import com.tangem.features.account.AccountDetailsComponent
import com.tangem.features.createwalletselection.CreateWalletSelectionComponent
import com.tangem.features.details.component.DetailsComponent
import com.tangem.features.disclaimer.api.components.DisclaimerComponent
@ -88,6 +89,7 @@ internal class ChildFactory @Inject constructor(
private val sendComponentFactoryV2: SendComponent.Factory,
private val redesignedWalletConnectComponentFactory: WalletConnectEntryComponent.Factory,
private val accountCreateEditComponentFactory: AccountCreateEditComponent.Factory,
private val accountDetailsComponentFactory: AccountDetailsComponent.Factory,
private val nftComponentFactory: NFTComponent.Factory,
private val nftSendComponentFactory: NFTSendComponent.Factory,
private val usedeskComponentFactory: UsedeskComponent.Factory,
@ -528,6 +530,15 @@ internal class ChildFactory @Inject constructor(
componentFactory = accountCreateEditComponentFactory,
)
}
is AppRoute.AccountDetails -> {
createComponentChild(
context = context,
params = AccountDetailsComponent.Params(
account = route.account,
),
componentFactory = accountDetailsComponentFactory,
)
}
}
}
}

View file

@ -336,4 +336,9 @@ sealed class AppRoute(val path: String) : Route {
data class EditAccount(
val account: Account,
) : AppRoute(path = "/edit_account/${account.accountId.value}")
@Serializable
data class AccountDetails(
val account: Account,
) : AppRoute(path = "/account_details/${account.accountId.value}")
}

View file

@ -0,0 +1,13 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<group>
<clip-path
android:pathData="M0,0h24v24h-24z"/>
<path
android:pathData="M12,18L16,14L14.6,12.6L13,14.2V10H11V14.2L9.4,12.6L8,14L12,18ZM5,8V19H19V8H5ZM5,21C4.45,21 3.979,20.804 3.588,20.413C3.196,20.021 3,19.55 3,19V6.525C3,6.292 3.037,6.067 3.112,5.85C3.188,5.633 3.3,5.433 3.45,5.25L4.7,3.725C4.883,3.492 5.113,3.313 5.387,3.188C5.662,3.063 5.95,3 6.25,3H17.75C18.05,3 18.337,3.063 18.612,3.188C18.888,3.313 19.117,3.492 19.3,3.725L20.55,5.25C20.7,5.433 20.813,5.633 20.888,5.85C20.962,6.067 21,6.292 21,6.525V19C21,19.55 20.804,20.021 20.413,20.413C20.021,20.804 19.55,21 19,21H5ZM5.4,6H18.6L17.75,5H6.25L5.4,6Z"
android:fillColor="#000000"/>
</group>
</vector>

View file

@ -0,0 +1,11 @@
package com.tangem.features.account
import com.tangem.core.decompose.factory.ComponentFactory
import com.tangem.core.ui.decompose.ComposableContentComponent
import com.tangem.domain.models.account.Account
interface AccountDetailsComponent : ComposableContentComponent {
interface Factory : ComponentFactory<Params, AccountDetailsComponent>
data class Params(val account: Account)
}

View file

@ -0,0 +1,18 @@
package com.tangem.features.account.common
import com.tangem.domain.models.account.CryptoPortfolioIcon
import com.tangem.domain.models.account.CryptoPortfolioIcon.Color
import com.tangem.domain.models.account.CryptoPortfolioIcon.Icon
data class CryptoPortfolioIconUM(
val value: Icon,
val color: Color,
) {
constructor(domainModel: CryptoPortfolioIcon) : this(
value = domainModel.value,
color = domainModel.color,
)
}
fun CryptoPortfolioIcon.toUM() = CryptoPortfolioIconUM(this)
fun CryptoPortfolioIconUM.toDomain() = CryptoPortfolioIcon.ofCustomAccount(this.value, this.color)

View file

@ -15,6 +15,7 @@ import com.tangem.domain.models.account.AccountName
import com.tangem.domain.models.account.CryptoPortfolioIcon
import com.tangem.domain.models.account.DerivationIndex
import com.tangem.features.account.AccountCreateEditComponent
import com.tangem.features.account.common.toDomain
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
@ -74,7 +75,7 @@ internal class AccountCreateEditModel @Inject constructor(
private suspend fun createNewCryptoPortfolio(params: AccountCreateEditComponent.Params.Create) {
val state = uiState.value
val name = AccountName(state.account.name).getOrNull() ?: return
val icon = state.account.portfolioIcon
val icon = state.account.portfolioIcon.toDomain()
addCryptoPortfolioUseCase(
userWalletId = params.userWalletId,
accountName = name,
@ -86,7 +87,7 @@ internal class AccountCreateEditModel @Inject constructor(
private suspend fun editCryptoPortfolio(params: AccountCreateEditComponent.Params.Edit) {
val state = uiState.value
val name = AccountName(state.account.name).getOrNull() ?: return
val icon = state.account.portfolioIcon
val icon = state.account.portfolioIcon.toDomain()
val isNewName = name != params.account.name
val isNewIcon = icon != params.account.portfolioIcon
updateCryptoPortfolioUseCase(

View file

@ -2,6 +2,7 @@ package com.tangem.features.account.createedit.entity
import com.tangem.core.ui.extensions.TextReference
import com.tangem.domain.models.account.CryptoPortfolioIcon
import com.tangem.features.account.common.CryptoPortfolioIconUM
import kotlinx.collections.immutable.ImmutableList
data class AccountCreateEditUM(
@ -15,7 +16,7 @@ data class AccountCreateEditUM(
data class Account(
val name: String,
val portfolioIcon: CryptoPortfolioIcon,
val portfolioIcon: CryptoPortfolioIconUM,
val derivationInfo: TextReference,
val inputPlaceholder: TextReference,
val onNameChange: (String) -> Unit,

View file

@ -6,6 +6,7 @@ import com.tangem.core.ui.extensions.resourceReference
import com.tangem.domain.models.account.Account
import com.tangem.domain.models.account.CryptoPortfolioIcon
import com.tangem.features.account.AccountCreateEditComponent
import com.tangem.features.account.common.toUM
import kotlinx.collections.immutable.toImmutableList
import javax.inject.Inject
@ -15,7 +16,7 @@ internal class AccountCreateEditUMBuilder @Inject constructor(
private val accountColors = CryptoPortfolioIcon.Color.entries.toImmutableList()
private val accountIcons = CryptoPortfolioIcon.Icon.entries.toImmutableList()
private val createIcon = CryptoPortfolioIcon.ofDefaultCustomAccount()
private val createIcon = CryptoPortfolioIcon.ofDefaultCustomAccount().toUM()
val toolbarTitle: TextReference
get() = when (params) {
@ -34,7 +35,7 @@ internal class AccountCreateEditUMBuilder @Inject constructor(
)
is AccountCreateEditComponent.Params.Edit -> AccountCreateEditUM.Account(
name = params.account.name.value,
portfolioIcon = params.account.portfolioIcon,
portfolioIcon = params.account.portfolioIcon.toUM(),
derivationInfo = TextReference.EMPTY, // todo account use Account.CryptoPortfolio.derivationIndex ?
inputPlaceholder = resourceReference(R.string.account_form_placeholder_edit_account),
onNameChange = onNameChange,
@ -86,8 +87,7 @@ internal class AccountCreateEditUMBuilder @Inject constructor(
}
fun AccountCreateEditUM.updateColorSelect(color: CryptoPortfolioIcon.Color): AccountCreateEditUM {
val newIcon = CryptoPortfolioIcon.ofCustomAccount(
value = account.portfolioIcon.value,
val newIcon = this.account.portfolioIcon.copy(
color = color,
)
return this.copy(
@ -97,9 +97,8 @@ internal class AccountCreateEditUMBuilder @Inject constructor(
}
fun AccountCreateEditUM.updateIconSelect(icon: CryptoPortfolioIcon.Icon): AccountCreateEditUM {
val newIcon = CryptoPortfolioIcon.ofCustomAccount(
val newIcon = this.account.portfolioIcon.copy(
value = icon,
color = account.portfolioIcon.color,
)
return this.copy(
account = this.account.copy(portfolioIcon = newIcon),

View file

@ -39,6 +39,7 @@ import com.tangem.core.ui.extensions.stringResourceSafe
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreview
import com.tangem.domain.models.account.CryptoPortfolioIcon
import com.tangem.features.account.common.toUM
import com.tangem.features.account.createedit.entity.AccountCreateEditUM
import com.tangem.features.account.createedit.entity.AccountCreateEditUM.Account
import kotlinx.collections.immutable.toImmutableList
@ -298,7 +299,7 @@ private class PreviewStateProvider : CollectionPreviewParameterProvider<AccountC
buildList {
val colors = CryptoPortfolioIcon.Color.entries.toImmutableList()
val icons = CryptoPortfolioIcon.Icon.entries.toImmutableList()
var portfolioIcon = CryptoPortfolioIcon.ofDefaultCustomAccount()
var portfolioIcon = CryptoPortfolioIcon.ofDefaultCustomAccount().toUM()
val first = AccountCreateEditUM(
title = stringReference("Add account"),
onCloseClick = {},
@ -327,7 +328,7 @@ private class PreviewStateProvider : CollectionPreviewParameterProvider<AccountC
)
add(first)
portfolioIcon = CryptoPortfolioIcon.ofCustomAccount(
portfolioIcon = portfolioIcon.copy(
value = CryptoPortfolioIcon.Icon.Letter,
color = CryptoPortfolioIcon.Color.entries.random(),
)

View file

@ -0,0 +1,85 @@
package com.tangem.features.account.details
import com.tangem.common.routing.AppRoute
import com.tangem.core.decompose.di.ModelScoped
import com.tangem.core.decompose.model.Model
import com.tangem.core.decompose.model.ParamsContainer
import com.tangem.core.decompose.navigation.Router
import com.tangem.core.decompose.ui.UiMessageSender
import com.tangem.core.res.R
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.message.DialogMessage
import com.tangem.core.ui.message.EventMessageAction
import com.tangem.domain.account.usecase.ArchiveCryptoPortfolioUseCase
import com.tangem.features.account.AccountDetailsComponent
import com.tangem.features.account.common.toUM
import com.tangem.features.account.createedit.entity.AccountCreateEditUMBuilder.Companion.portfolioIcon
import com.tangem.features.account.details.entity.AccountDetailsUM
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.launch
import javax.inject.Inject
@ModelScoped
internal class AccountDetailsModel @Inject constructor(
paramsContainer: ParamsContainer,
private val messageSender: UiMessageSender,
private val router: Router,
override val dispatchers: CoroutineDispatcherProvider,
private val archiveCryptoPortfolioUseCase: ArchiveCryptoPortfolioUseCase,
) : Model() {
private val params = paramsContainer.require<AccountDetailsComponent.Params>()
val uiState: StateFlow<AccountDetailsUM> get() = _uiState
private val _uiState: MutableStateFlow<AccountDetailsUM> = MutableStateFlow(getInitialState())
private fun onEditAccountClick() {
router.push(AppRoute.EditAccount(params.account))
}
private fun onManageTokensClick() {
// todo account add account param
router.push(AppRoute.ManageTokens(source = AppRoute.ManageTokens.Source.SETTINGS))
}
private fun onArchiveAccountClick() {
confirmArchiveDialog()
}
private fun confirmArchiveDialog() {
val secondAction = EventMessageAction(
title = resourceReference(R.string.common_cancel),
onClick = {},
)
val firstAction = EventMessageAction(
title = resourceReference(R.string.account_details_archive_action),
warning = true,
onClick = ::archiveCryptoPortfolio,
)
messageSender.send(
DialogMessage(
title = resourceReference(R.string.account_details_archive),
message = resourceReference(R.string.account_details_archive_description),
firstActionBuilder = { firstAction },
secondActionBuilder = { secondAction },
),
)
}
private fun archiveCryptoPortfolio() = modelScope.launch {
archiveCryptoPortfolioUseCase(params.account.accountId)
}
private fun getInitialState(): AccountDetailsUM {
return AccountDetailsUM(
accountName = params.account.name.value,
accountIcon = params.account.portfolioIcon.toUM(),
onCloseClick = { router.pop() },
onAccountEditClick = ::onEditAccountClick,
onManageTokensClick = ::onManageTokensClick,
onArchiveAccountClick = ::onArchiveAccountClick,
)
}
}

View file

@ -0,0 +1,40 @@
package com.tangem.features.account.details
import androidx.activity.compose.BackHandler
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.tangem.core.decompose.context.AppComponentContext
import com.tangem.core.decompose.model.getOrCreateModel
import com.tangem.features.account.AccountDetailsComponent
import com.tangem.features.account.details.ui.AccountDetailsContent
import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
internal class DefaultAccountDetailsComponent @AssistedInject constructor(
@Assisted appComponentContext: AppComponentContext,
@Assisted params: AccountDetailsComponent.Params,
) : AppComponentContext by appComponentContext, AccountDetailsComponent {
private val model: AccountDetailsModel = getOrCreateModel(params)
@Composable
override fun Content(modifier: Modifier) {
val state by model.uiState.collectAsStateWithLifecycle()
AccountDetailsContent(
modifier = modifier,
state = state,
)
BackHandler(onBack = state.onCloseClick)
}
@AssistedFactory
interface Factory : AccountDetailsComponent.Factory {
override fun create(
context: AppComponentContext,
params: AccountDetailsComponent.Params,
): DefaultAccountDetailsComponent
}
}

View file

@ -0,0 +1,27 @@
package com.tangem.features.account.details.di
import com.tangem.core.decompose.model.Model
import com.tangem.features.account.AccountDetailsComponent
import com.tangem.features.account.details.AccountDetailsModel
import com.tangem.features.account.details.DefaultAccountDetailsComponent
import dagger.Binds
import dagger.Module
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import dagger.multibindings.ClassKey
import dagger.multibindings.IntoMap
@Module
@InstallIn(SingletonComponent::class)
internal interface AccountDetailsModule {
@Binds
fun bindAccountDetailsComponentFactory(
impl: DefaultAccountDetailsComponent.Factory,
): AccountDetailsComponent.Factory
@Binds
@IntoMap
@ClassKey(AccountDetailsModel::class)
fun bindAccountDetailsModel(model: AccountDetailsModel): Model
}

View file

@ -0,0 +1,12 @@
package com.tangem.features.account.details.entity
import com.tangem.features.account.common.CryptoPortfolioIconUM
data class AccountDetailsUM(
val accountName: String,
val accountIcon: CryptoPortfolioIconUM,
val onCloseClick: () -> Unit,
val onAccountEditClick: () -> Unit,
val onManageTokensClick: () -> Unit,
val onArchiveAccountClick: () -> Unit,
)

View file

@ -0,0 +1,236 @@
package com.tangem.features.account.details.ui
import android.content.res.Configuration
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.Icon
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.res.vectorResource
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider
import androidx.compose.ui.unit.dp
import com.tangem.common.ui.R
import com.tangem.common.ui.account.getResId
import com.tangem.common.ui.account.getUiColor
import com.tangem.core.ui.components.SpacerH
import com.tangem.core.ui.components.SpacerH16
import com.tangem.core.ui.components.appbar.AppBarWithBackButton
import com.tangem.core.ui.components.buttons.SecondarySmallButton
import com.tangem.core.ui.components.buttons.SmallButtonConfig
import com.tangem.core.ui.components.fields.AutoSizeTextField
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.extensions.stringResourceSafe
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreview
import com.tangem.domain.models.account.CryptoPortfolioIcon
import com.tangem.features.account.common.CryptoPortfolioIconUM
import com.tangem.features.account.common.toUM
import com.tangem.features.account.details.entity.AccountDetailsUM
@Composable
internal fun AccountDetailsContent(state: AccountDetailsUM, modifier: Modifier = Modifier) {
Column(
modifier = modifier
.background(color = TangemTheme.colors.background.secondary)
.fillMaxSize()
.imePadding()
.systemBarsPadding(),
horizontalAlignment = Alignment.CenterHorizontally,
) {
AppBarWithBackButton(
onBackClick = state.onCloseClick,
modifier = Modifier.height(TangemTheme.dimens.size56),
)
Column(
modifier = Modifier
.fillMaxSize()
.padding(horizontal = TangemTheme.dimens.spacing16)
.weight(1f),
) {
Text(
modifier = Modifier.padding(top = TangemTheme.dimens.spacing12),
text = stringResourceSafe(R.string.account_details_title),
style = TangemTheme.typography.h1,
color = TangemTheme.colors.text.primary1,
)
SpacerH16()
AccountRow(state)
SpacerH16()
ManageTokensRow(state)
SpacerH16()
ArchiveAccountRow(state)
SpacerH(8.dp)
Text(
modifier = Modifier.padding(horizontal = 12.dp),
text = stringResourceSafe(R.string.account_details_archive_description),
style = TangemTheme.typography.caption2,
color = TangemTheme.colors.text.tertiary,
)
}
}
}
@Composable
private fun ArchiveAccountRow(state: AccountDetailsUM) {
Row(
modifier = Modifier
.fillMaxWidth()
.clip(RoundedCornerShape(TangemTheme.dimens.radius12))
.background(TangemTheme.colors.background.primary)
.clickable(onClick = state.onArchiveAccountClick)
.padding(all = TangemTheme.dimens.spacing12),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing12),
) {
Icon(
tint = TangemTheme.colors.icon.warning,
imageVector = ImageVector.vectorResource(id = R.drawable.ic_archive_24),
contentDescription = null,
)
Text(
text = stringResourceSafe(R.string.account_details_archive),
color = TangemTheme.colors.text.warning,
style = TangemTheme.typography.subtitle1,
)
}
}
@Composable
private fun ManageTokensRow(state: AccountDetailsUM) {
Row(
modifier = Modifier
.fillMaxWidth()
.clip(RoundedCornerShape(TangemTheme.dimens.radius12))
.background(TangemTheme.colors.background.primary)
.clickable(onClick = state.onManageTokensClick)
.padding(all = TangemTheme.dimens.spacing12),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing12),
) {
Icon(
imageVector = ImageVector.vectorResource(id = R.drawable.ic_group_24),
tint = TangemTheme.colors.icon.secondary,
contentDescription = null,
)
Text(
text = stringResourceSafe(R.string.main_manage_tokens),
color = TangemTheme.colors.text.primary1,
style = TangemTheme.typography.subtitle1,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
)
}
}
@Composable
private fun AccountRow(state: AccountDetailsUM) {
Row(
modifier = Modifier
.fillMaxWidth()
.clip(RoundedCornerShape(TangemTheme.dimens.radius12))
.background(TangemTheme.colors.background.primary)
.clickable(onClick = state.onAccountEditClick)
.padding(all = TangemTheme.dimens.spacing12),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing12),
) {
AccountIcon(
modifier = Modifier
.size(36.dp)
.clip(RoundedCornerShape(9.dp)),
accountName = state.accountName,
accountIcon = state.accountIcon,
)
Column(
modifier = Modifier
.weight(1f),
verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing2),
) {
Text(
text = stringResourceSafe(R.string.account_form_name),
color = TangemTheme.colors.text.tertiary,
style = TangemTheme.typography.caption2,
)
AutoSizeTextField(
textStyle = TangemTheme.typography.subtitle2,
color = TangemTheme.colors.text.primary1,
value = state.accountName,
singleLine = true,
readOnly = true,
onValueChange = {},
)
}
SecondarySmallButton(
config = SmallButtonConfig(
text = resourceReference(R.string.common_edit),
onClick = state.onAccountEditClick,
),
)
}
}
// todo account make reusable
@Composable
private fun AccountIcon(accountName: String, accountIcon: CryptoPortfolioIconUM, modifier: Modifier = Modifier) {
Box(
contentAlignment = Alignment.Center,
modifier = modifier.background(accountIcon.color.getUiColor()),
) {
val icon = accountIcon.value
val letter = accountName.first()
when {
icon == CryptoPortfolioIcon.Icon.Letter -> Text(
text = letter.uppercase(),
style = TangemTheme.typography.body2,
color = TangemTheme.colors.text.constantWhite,
)
else -> Icon(
modifier = Modifier.size(20.dp),
tint = TangemTheme.colors.text.constantWhite,
imageVector = ImageVector.vectorResource(id = icon.getResId()),
contentDescription = null,
)
}
}
}
@Preview(showBackground = true, widthDp = 360)
@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES)
@Composable
private fun WcConnectionsContentPreview(@PreviewParameter(PreviewStateProvider::class) params: AccountDetailsUM) {
TangemThemePreview {
AccountDetailsContent(state = params)
}
}
private class PreviewStateProvider : CollectionPreviewParameterProvider<AccountDetailsUM>(
buildList {
var portfolioIcon = CryptoPortfolioIcon.ofDefaultCustomAccount().toUM()
val first = AccountDetailsUM(
onCloseClick = {},
onAccountEditClick = {},
onManageTokensClick = {},
onArchiveAccountClick = {},
accountName = "Main",
accountIcon = portfolioIcon,
)
add(first)
portfolioIcon = portfolioIcon.copy(
value = CryptoPortfolioIcon.Icon.Letter,
color = CryptoPortfolioIcon.Color.entries.random(),
)
add(first.copy(accountIcon = portfolioIcon))
},
)