Updated on 2026-08-14

This commit is contained in:
Tangem 2025-08-21 15:26:54 +03:00
commit fe4971962d
23 changed files with 559 additions and 251 deletions

View file

@ -0,0 +1,134 @@
package com.tangem.common.ui.account
import android.content.res.Configuration
import androidx.compose.foundation.background
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.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import com.tangem.common.ui.account.AccountIconPreviewData.randomAccountIcon
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.resolveReference
import com.tangem.core.ui.extensions.stringReference
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreview
import com.tangem.domain.models.account.CryptoPortfolioIcon
import com.tangem.domain.models.account.CryptoPortfolioIcon.Color
enum class AccountIconSize {
Default, Large, Medium, Small, ExtraSmall
}
/**
* Displays an account icon that can either show a letter (derived from [name])
* or a predefined vector resource (from [icon]).
*
* The background color is determined by the icon's [CryptoPortfolioIconUM.color],
* and the icon size, text style, and box modifier are adapted based on the given [size].
*
* @param name The text reference used to resolve and display the first letter
* when [icon] is set to [CryptoPortfolioIcon.Icon.Letter].
* @param icon The account icon definition, which can be a letter or a drawable resource.
* @param size The size of the icon, defined by [AccountIconSize].
*/
@Composable
fun AccountIcon(
name: TextReference,
icon: CryptoPortfolioIconUM,
size: AccountIconSize,
modifier: Modifier = Modifier,
) {
val boxModifier = modifier.selectBoxModifier(size)
val iconSize = Modifier.selectIconSize(size)
val textStyle = when (size) {
AccountIconSize.Default -> TangemTheme.typography.h3
AccountIconSize.Large -> TangemTheme.typography.h1
AccountIconSize.Medium -> TangemTheme.typography.subtitle1
AccountIconSize.Small -> TangemTheme.typography.subtitle2
AccountIconSize.ExtraSmall -> TangemTheme.typography.caption1
}
Box(
contentAlignment = Alignment.Center,
modifier = boxModifier.background(icon.color.getUiColor()),
) {
val icon = icon.value
val letter = name.resolveReference().firstOrNull()
when {
icon == CryptoPortfolioIcon.Icon.Letter -> Text(
text = letter?.uppercase() ?: "",
style = textStyle,
color = TangemTheme.colors.text.constantWhite,
)
else -> Icon(
modifier = iconSize,
tint = TangemTheme.colors.text.constantWhite,
imageVector = ImageVector.vectorResource(id = icon.getResId()),
contentDescription = null,
)
}
}
}
private fun Modifier.selectIconSize(size: AccountIconSize): Modifier = when (size) {
AccountIconSize.Default -> this.size(20.dp)
AccountIconSize.Large -> this.size(40.dp)
AccountIconSize.Medium -> this.size(16.dp)
AccountIconSize.Small -> this.size(12.dp)
AccountIconSize.ExtraSmall -> this.size(8.dp)
}
private fun Modifier.selectBoxModifier(size: AccountIconSize): Modifier = when (size) {
AccountIconSize.Default -> size(36.dp).clip(RoundedCornerShape(10.dp))
AccountIconSize.Large -> size(88.dp).clip(RoundedCornerShape(24.dp))
AccountIconSize.Medium -> size(28.dp).clip(RoundedCornerShape(8.dp))
AccountIconSize.Small -> size(20.dp).clip(RoundedCornerShape(6.dp))
AccountIconSize.ExtraSmall -> size(14.dp).clip(RoundedCornerShape(4.dp))
}
@Preview(showBackground = true)
@Preview(showBackground = true, uiMode = Configuration.UI_MODE_NIGHT_YES)
@Composable
private fun Preview_AccountIcon() {
TangemThemePreview {
Sample()
}
}
@Composable
private fun Sample() {
val name = stringReference("Account Name")
Row(
modifier = Modifier.background(TangemTheme.colors.background.primary),
) {
Column(verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing8)) {
AccountIcon(name = name, randomAccountIcon(), size = AccountIconSize.Default)
AccountIcon(name = name, randomAccountIcon(), size = AccountIconSize.Large)
AccountIcon(name = name, randomAccountIcon(), size = AccountIconSize.Medium)
AccountIcon(name = name, randomAccountIcon(), size = AccountIconSize.Small)
AccountIcon(name = name, randomAccountIcon(), size = AccountIconSize.ExtraSmall)
}
Column(verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing8)) {
AccountIcon(name = name, randomAccountIcon(letter = true), size = AccountIconSize.Default)
AccountIcon(name = name, randomAccountIcon(letter = true), size = AccountIconSize.Large)
AccountIcon(name = name, randomAccountIcon(letter = true), size = AccountIconSize.Medium)
AccountIcon(name = name, randomAccountIcon(letter = true), size = AccountIconSize.Small)
AccountIcon(name = name, randomAccountIcon(letter = true), size = AccountIconSize.ExtraSmall)
}
}
}
object AccountIconPreviewData {
fun randomAccountIcon(letter: Boolean = false) = CryptoPortfolioIconUM(
value = if (letter) CryptoPortfolioIcon.Icon.Letter else CryptoPortfolioIcon.Icon.entries.random(),
color = Color.entries.random(),
)
}

View file

@ -0,0 +1,110 @@
package com.tangem.common.ui.account
import android.content.res.Configuration
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.Preview
import com.tangem.common.ui.R
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.resolveReference
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.extensions.stringReference
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreview
/**
* Displays a row representing an account with an icon, title, and subtitle.
*
* The row consists of:
* - An [AccountIcon] on the left.
* - A column with the [title] and [subtitle] texts, which can be displayed in normal
* or reversed order depending on [isReverse].
*
* The layout uses horizontal spacing between the icon and text, and vertical spacing
* between the title and subtitle.
*
* @param title The main text shown in the row, usually representing the account name.
* @param subtitle The secondary text, typically providing additional details about the account.
* @param icon The account icon definition, displayed using [AccountIcon].
* @param isReverse If `true`, the [subtitle] is displayed above the [title].
* Otherwise, the [title] is displayed above the [subtitle].
*/
@Composable
fun AccountRow(
title: TextReference,
subtitle: TextReference,
icon: CryptoPortfolioIconUM,
modifier: Modifier = Modifier,
isReverse: Boolean = false,
) {
Row(
modifier = modifier,
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing12),
) {
AccountIcon(
name = title,
icon = icon,
size = AccountIconSize.Default,
)
Column(
verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing2),
) {
if (isReverse) {
Subtitle(subtitle)
Title(title)
} else {
Title(title)
Subtitle(subtitle)
}
}
}
}
@Composable
private fun Title(title: TextReference) {
Text(
text = title.resolveReference(),
style = TangemTheme.typography.subtitle2,
color = TangemTheme.colors.text.primary1,
)
}
@Composable
private fun Subtitle(subtitle: TextReference) {
Text(
color = TangemTheme.colors.text.tertiary,
style = TangemTheme.typography.caption2,
text = subtitle.resolveReference(),
)
}
@Preview(showBackground = true)
@Preview(showBackground = true, uiMode = Configuration.UI_MODE_NIGHT_YES)
@Composable
private fun Preview() {
TangemThemePreview {
Sample()
}
}
@Composable
private fun Sample() {
val name = stringReference("Main account")
val info = stringReference("10 tokens in 2 networks")
val subtitle = resourceReference(R.string.account_form_name)
fun icon(letter: Boolean = false) = AccountIconPreviewData.randomAccountIcon(letter)
Column(
modifier = Modifier.background(TangemTheme.colors.background.primary),
verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing8),
) {
AccountRow(title = name, subtitle = info, icon = icon())
AccountRow(title = name, subtitle = subtitle, icon = icon(), isReverse = true)
}
}

View file

@ -47,4 +47,7 @@ fun CryptoPortfolioIcon.Icon.getResId(): Int {
CryptoPortfolioIcon.Icon.Package -> R.drawable.ic_package_24
CryptoPortfolioIcon.Icon.Gift -> R.drawable.ic_gift_24
}
}
}
fun CryptoPortfolioIcon.toUM() = CryptoPortfolioIconUM(domainModel = this)
fun CryptoPortfolioIconUM.toDomain() = CryptoPortfolioIcon.ofCustomAccount(value = this.value, color = this.color)

View file

@ -1,4 +1,4 @@
package com.tangem.features.account.common
package com.tangem.common.ui.account
import com.tangem.domain.models.account.CryptoPortfolioIcon
import com.tangem.domain.models.account.CryptoPortfolioIcon.Color
@ -12,7 +12,4 @@ data class CryptoPortfolioIconUM(
value = domainModel.value,
color = domainModel.color,
)
}
fun CryptoPortfolioIcon.toUM() = CryptoPortfolioIconUM(this)
fun CryptoPortfolioIconUM.toDomain() = CryptoPortfolioIcon.ofCustomAccount(this.value, this.color)
}

View file

@ -0,0 +1,99 @@
package com.tangem.core.ui.components.bottomsheets
import android.content.res.Configuration
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.padding
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.Preview
import com.tangem.core.ui.components.bottomsheets.sheet.TangemBottomSheet
import com.tangem.core.ui.components.inputrow.InputRowDefault
import com.tangem.core.ui.decorations.roundedShapeItemDecoration
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreview
import kotlinx.collections.immutable.persistentListOf
/**
* Generic options bottom sheet component
*
* @param config Bottom sheet configuration containing OptionsBottomSheetContent
* @param title Title text for the bottom sheet
* @param containerColor Background color of the bottom sheet
*/
@Composable
fun OptionsBottomSheet(
config: TangemBottomSheetConfig,
title: TextReference,
containerColor: androidx.compose.ui.graphics.Color = TangemTheme.colors.background.tertiary,
) {
TangemBottomSheet<OptionsBottomSheetContent>(
config = config,
titleText = title,
containerColor = containerColor,
content = { content ->
OptionsBottomSheetContent(content = content)
},
)
}
@Composable
private fun OptionsBottomSheetContent(content: OptionsBottomSheetContent) {
Column(
modifier = Modifier
.padding(
start = TangemTheme.dimens.spacing16,
end = TangemTheme.dimens.spacing16,
bottom = TangemTheme.dimens.spacing16,
),
) {
content.options.forEachIndexed { index, option ->
InputRowDefault(
text = option.label,
showDivider = index < content.options.size - 1,
modifier = Modifier
.roundedShapeItemDecoration(
currentIndex = index,
lastIndex = content.options.size - 1,
addDefaultPadding = false,
)
.background(TangemTheme.colors.background.action)
.clickable { content.onOptionClick(option.key) },
)
}
}
}
@Preview(showBackground = true, widthDp = 360)
@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES)
@Composable
private fun OptionsBottomSheetPreview() {
TangemThemePreview {
OptionsBottomSheet(
config = TangemBottomSheetConfig(
isShown = true,
onDismissRequest = {},
content = OptionsBottomSheetContent(
options = persistentListOf(
BottomSheetOption(
key = "option1",
label = TextReference.Str("First Option"),
),
BottomSheetOption(
key = "option2",
label = TextReference.Str("Second Option"),
),
BottomSheetOption(
key = "option3",
label = TextReference.Str("Third Option"),
),
),
onOptionClick = {},
),
),
title = TextReference.Str("Select Option"),
)
}
}

View file

@ -0,0 +1,23 @@
package com.tangem.core.ui.components.bottomsheets
import com.tangem.core.ui.extensions.TextReference
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.persistentListOf
/**
* @param key Unique identifier for the option
* @param label Display text for the option
*/
data class BottomSheetOption(
val key: String,
val label: TextReference,
)
/**
* @param options List of options to display
* @param onOptionClick Callback when an option is clicked, receives the option key
*/
data class OptionsBottomSheetContent(
val options: ImmutableList<BottomSheetOption> = persistentListOf(),
val onOptionClick: (String) -> Unit = {},
) : TangemBottomSheetConfigContent

View file

@ -1,7 +1,7 @@
package com.tangem.features.account.archived.entity
import com.tangem.common.ui.account.CryptoPortfolioIconUM
import com.tangem.core.ui.extensions.TextReference
import com.tangem.features.account.common.CryptoPortfolioIconUM
import kotlinx.collections.immutable.ImmutableList
internal sealed interface AccountArchivedUM {
@ -20,8 +20,8 @@ internal sealed interface AccountArchivedUM {
internal data class ArchivedAccountUM(
val accountId: String,
val accountName: String,
val accountIcon: CryptoPortfolioIconUM,
val accountName: TextReference,
val accountIconUM: CryptoPortfolioIconUM,
val tokensInfo: TextReference,
val onClick: (accountId: String) -> Unit,
)

View file

@ -6,33 +6,28 @@ import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.itemsIndexed
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.CircularProgressIndicator
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.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.account.AccountIconPreviewData
import com.tangem.common.ui.account.AccountRow
import com.tangem.core.res.R
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.decorations.roundedShapeItemDecoration
import com.tangem.core.ui.extensions.resolveReference
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.extensions.stringReference
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.archived.entity.AccountArchivedUM
import com.tangem.features.account.archived.entity.ArchivedAccountUM
import com.tangem.features.account.common.toUM
import com.tangem.features.account.details.ui.AccountIcon
import kotlinx.collections.immutable.toImmutableList
@Composable
@ -135,28 +130,12 @@ private fun ArchivedAccountRow(item: ArchivedAccountUM, modifier: Modifier = Mod
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing12),
) {
AccountIcon(
modifier = Modifier
.size(36.dp)
.clip(RoundedCornerShape(9.dp)),
accountName = item.accountName,
accountIcon = item.accountIcon,
)
Column(
AccountRow(
title = item.accountName,
subtitle = item.tokensInfo,
icon = item.accountIconUM,
modifier = Modifier.weight(1f),
verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing2),
) {
Text(
text = item.accountName,
style = TangemTheme.typography.subtitle2,
color = TangemTheme.colors.text.primary1,
)
Text(
color = TangemTheme.colors.text.tertiary,
style = TangemTheme.typography.caption2,
text = item.tokensInfo.resolveReference(),
)
}
)
SecondarySmallButton(
config = SmallButtonConfig(
@ -179,13 +158,14 @@ private fun WcConnectionsContentPreview(@PreviewParameter(PreviewStateProvider::
@Suppress("MagicNumber")
private class PreviewStateProvider : CollectionPreviewParameterProvider<AccountArchivedUM>(
buildList {
fun portfolioIcon() = CryptoPortfolioIcon.ofDefaultCustomAccount().toUM()
fun portfolioIcon() = AccountIconPreviewData.randomAccountIcon()
val accountName = stringReference("Account name")
val firstList = List(10) {
ArchivedAccountUM(
accountId = it.toString(),
accountName = "Account name",
accountIcon = portfolioIcon(),
accountName = accountName,
accountIconUM = portfolioIcon(),
tokensInfo = stringReference("10 tokens in 2 networks"),
onClick = {},

View file

@ -2,6 +2,7 @@ package com.tangem.features.account.createedit
import com.tangem.core.analytics.api.AnalyticsExceptionHandler
import com.tangem.core.analytics.models.ExceptionAnalyticsEvent
import com.tangem.common.ui.account.toDomain
import com.tangem.core.decompose.di.ModelScoped
import com.tangem.core.decompose.model.Model
import com.tangem.core.decompose.model.ParamsContainer
@ -20,7 +21,6 @@ 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.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

View file

@ -1,8 +1,8 @@
package com.tangem.features.account.createedit.entity
import com.tangem.common.ui.account.CryptoPortfolioIconUM
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(

View file

@ -1,5 +1,6 @@
package com.tangem.features.account.createedit.entity
import com.tangem.common.ui.account.toUM
import com.tangem.core.res.R
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.resourceReference
@ -7,7 +8,6 @@ import com.tangem.core.ui.extensions.wrappedList
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
internal class AccountCreateEditUMBuilder(

View file

@ -24,6 +24,9 @@ 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.AccountIcon
import com.tangem.common.ui.account.AccountIconPreviewData
import com.tangem.common.ui.account.AccountIconSize
import com.tangem.common.ui.account.getResId
import com.tangem.common.ui.account.getUiColor
import com.tangem.core.ui.components.PrimaryButton
@ -36,7 +39,6 @@ import com.tangem.core.ui.extensions.*
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
@ -69,7 +71,7 @@ internal fun AccountCreateEditContent(state: AccountCreateEditUM, modifier: Modi
SpacerH24()
AccountColor(state.colorsState)
SpacerH24()
AccountIcon(state.iconsState)
AccountIcons(state.iconsState)
SpacerH8()
Text(
modifier = Modifier.padding(horizontal = 8.dp),
@ -100,7 +102,11 @@ private fun AccountSummary(account: Account) {
) {
Spacer(modifier = Modifier.height(24.dp))
AccountIcon(account)
AccountIcon(
name = stringReference(account.name),
icon = account.portfolioIcon,
size = AccountIconSize.Large,
)
Spacer(modifier = Modifier.height(24.dp))
Text(
@ -122,34 +128,6 @@ private fun AccountSummary(account: Account) {
}
}
@Composable
private fun AccountIcon(account: Account) {
Box(
contentAlignment = Alignment.Center,
modifier = Modifier
.size(88.dp)
.clip(RoundedCornerShape(TangemTheme.dimens.radius24))
.background(account.portfolioIcon.color.getUiColor()),
) {
val icon = account.portfolioIcon.value
val letter = account.name.firstOrNull()
?: account.inputPlaceholder.resolveReference().first()
when {
icon == CryptoPortfolioIcon.Icon.Letter -> Text(
text = letter.uppercase(),
style = TangemTheme.typography.head,
color = TangemTheme.colors.text.constantWhite,
)
else -> Icon(
modifier = Modifier.size(44.dp),
tint = TangemTheme.colors.text.constantWhite,
imageVector = ImageVector.vectorResource(id = icon.getResId()),
contentDescription = null,
)
}
}
}
@Suppress("LongMethod", "MagicNumber")
@Composable
private fun AccountColor(colorsState: AccountCreateEditUM.Colors) {
@ -201,7 +179,7 @@ private fun AccountColor(colorsState: AccountCreateEditUM.Colors) {
@Suppress("LongMethod", "MagicNumber")
@Composable
private fun AccountIcon(iconsState: AccountCreateEditUM.Icons) {
private fun AccountIcons(iconsState: AccountCreateEditUM.Icons) {
Box(
Modifier
.clip(RoundedCornerShape(16.dp))
@ -296,7 +274,7 @@ private class PreviewStateProvider : CollectionPreviewParameterProvider<AccountC
buildList {
val colors = CryptoPortfolioIcon.Color.entries.toImmutableList()
val icons = CryptoPortfolioIcon.Icon.entries.toImmutableList()
var portfolioIcon = CryptoPortfolioIcon.ofDefaultCustomAccount().toUM()
var portfolioIcon = AccountIconPreviewData.randomAccountIcon()
val first = AccountCreateEditUM(
title = stringReference("Add account"),
onCloseClick = {},
@ -328,16 +306,14 @@ private class PreviewStateProvider : CollectionPreviewParameterProvider<AccountC
)
add(first)
portfolioIcon = portfolioIcon.copy(
value = CryptoPortfolioIcon.Icon.Letter,
color = CryptoPortfolioIcon.Color.entries.random(),
)
portfolioIcon = AccountIconPreviewData.randomAccountIcon(letter = true)
val accountName = "Main account"
val second = AccountCreateEditUM(
title = stringReference("Edit account"),
onCloseClick = {},
account = Account(
portfolioIcon = portfolioIcon,
name = "Main account",
name = accountName,
inputPlaceholder = resourceReference(R.string.account_form_placeholder_edit_account),
onNameChange = {},
derivationInfo = AccountCreateEditUM.DerivationInfo.Content(

View file

@ -1,6 +1,7 @@
package com.tangem.features.account.details
import com.tangem.common.routing.AppRoute
import com.tangem.common.ui.account.toUM
import com.tangem.core.decompose.di.ModelScoped
import com.tangem.core.decompose.model.Model
import com.tangem.core.decompose.model.ParamsContainer
@ -12,7 +13,6 @@ 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

View file

@ -1,6 +1,6 @@
package com.tangem.features.account.details.entity
import com.tangem.features.account.common.CryptoPortfolioIconUM
import com.tangem.common.ui.account.CryptoPortfolioIconUM
data class AccountDetailsUM(
val accountName: String,

View file

@ -19,21 +19,18 @@ 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.common.ui.account.AccountIconPreviewData
import com.tangem.common.ui.account.AccountRow
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.stringReference
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
@ -145,32 +142,13 @@ private fun AccountRow(state: AccountDetailsUM) {
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,
AccountRow(
title = stringReference(state.accountName),
subtitle = resourceReference(R.string.account_form_name),
icon = state.accountIcon,
modifier = Modifier.weight(1f),
isReverse = true,
)
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(
@ -181,31 +159,6 @@ private fun AccountRow(state: AccountDetailsUM) {
}
}
// todo account make reusable
@Composable
internal 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
@ -217,20 +170,18 @@ private fun WcConnectionsContentPreview(@PreviewParameter(PreviewStateProvider::
private class PreviewStateProvider : CollectionPreviewParameterProvider<AccountDetailsUM>(
buildList {
var portfolioIcon = CryptoPortfolioIcon.ofDefaultCustomAccount().toUM()
val accountName = "Main"
var portfolioIcon = AccountIconPreviewData.randomAccountIcon()
val first = AccountDetailsUM(
onCloseClick = {},
onAccountEditClick = {},
onManageTokensClick = {},
onArchiveAccountClick = {},
accountName = "Main",
accountName = accountName,
accountIcon = portfolioIcon,
)
add(first)
portfolioIcon = portfolioIcon.copy(
value = CryptoPortfolioIcon.Icon.Letter,
color = CryptoPortfolioIcon.Color.entries.random(),
)
portfolioIcon = AccountIconPreviewData.randomAccountIcon(letter = true)
add(first.copy(accountIcon = portfolioIcon))
},
)

View file

@ -18,6 +18,7 @@ dependencies {
implementation(projects.features.wallet.api)
implementation(projects.features.disclaimer.api)
implementation(projects.features.tester.api)
implementation(projects.features.createWalletSelection.api)
/* Project - Core */
implementation(projects.core.decompose)

View file

@ -2,6 +2,7 @@ package com.tangem.features.details.entity
import androidx.compose.runtime.Immutable
import com.tangem.common.ui.userwallet.state.UserWalletItemUM
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig
import com.tangem.core.ui.extensions.TextReference
import kotlinx.collections.immutable.ImmutableList
@ -11,4 +12,5 @@ internal data class UserWalletListUM(
val isWalletSavingInProgress: Boolean,
val addNewWalletText: TextReference,
val onAddNewWalletClick: () -> Unit,
val addWalletBottomSheet: TangemBottomSheetConfig = TangemBottomSheetConfig.Empty,
)

View file

@ -6,12 +6,17 @@ import com.tangem.core.decompose.di.ModelScoped
import com.tangem.core.decompose.model.Model
import com.tangem.core.decompose.navigation.Router
import com.tangem.core.decompose.ui.UiMessageSender
import com.tangem.core.navigation.url.UrlOpener
import com.tangem.core.ui.R.*
import com.tangem.core.ui.components.bottomsheets.BottomSheetOption
import com.tangem.core.ui.components.bottomsheets.OptionsBottomSheetContent
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.domain.wallets.usecase.GenerateBuyTangemCardLinkUseCase
import com.tangem.domain.wallets.usecase.ShouldSaveUserWalletsUseCase
import com.tangem.features.details.entity.UserWalletListUM
import com.tangem.features.details.impl.R
import com.tangem.features.details.utils.UserWalletSaver
import com.tangem.features.wallet.utils.UserWalletsFetcher
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import kotlinx.collections.immutable.ImmutableList
@ -20,16 +25,19 @@ import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import javax.inject.Inject
@Suppress("LongParameterList")
@ModelScoped
internal class UserWalletListModel @Inject constructor(
userWalletsFetcherFactory: UserWalletsFetcher.Factory,
shouldSaveUserWalletsUseCase: ShouldSaveUserWalletsUseCase,
private val router: Router,
private val messageSender: UiMessageSender,
private val userWalletSaver: UserWalletSaver,
override val dispatchers: CoroutineDispatcherProvider,
private val generateBuyTangemCardLinkUseCase: GenerateBuyTangemCardLinkUseCase,
private val urlOpener: UrlOpener,
) : Model() {
private val isWalletSavingInProgress: MutableStateFlow<Boolean> = MutableStateFlow(value = false)
@ -45,7 +53,8 @@ internal class UserWalletListModel @Inject constructor(
userWallets = persistentListOf(),
isWalletSavingInProgress = false,
addNewWalletText = TextReference.EMPTY,
onAddNewWalletClick = ::addUserWallet,
onAddNewWalletClick = ::showAddWalletBottomSheet,
addWalletBottomSheet = TangemBottomSheetConfig.Empty,
),
)
@ -54,8 +63,9 @@ internal class UserWalletListModel @Inject constructor(
flow = userWalletsFetcher.userWallets,
flow2 = shouldSaveUserWalletsUseCase(),
flow3 = isWalletSavingInProgress,
transform = ::updateState,
).launchIn(modelScope)
) { userWallets, shouldSaveUserWallets, isWalletSavingInProgress ->
updateState(userWallets, shouldSaveUserWallets, isWalletSavingInProgress)
}.launchIn(modelScope)
}
private fun updateState(
@ -74,7 +84,58 @@ internal class UserWalletListModel @Inject constructor(
)
}
private fun addUserWallet() = withProgress(isWalletSavingInProgress) {
userWalletSaver.scanAndSaveUserWallet(modelScope)
private fun showAddWalletBottomSheet() {
state.update { currentState ->
currentState.copy(
addWalletBottomSheet = TangemBottomSheetConfig(
isShown = true,
onDismissRequest = ::dismissAddWalletBottomSheet,
content = createAddWalletBottomSheetContent(),
),
)
}
}
private fun dismissAddWalletBottomSheet() {
state.update { currentState ->
currentState.copy(
addWalletBottomSheet = currentState.addWalletBottomSheet.copy(isShown = false),
)
}
}
private fun createAddWalletBottomSheetContent(): OptionsBottomSheetContent {
return OptionsBottomSheetContent(
options = persistentListOf(
BottomSheetOption(
key = ADD_WALLET_KEY_CREATE,
label = resourceReference(string.home_button_create_new_wallet),
),
BottomSheetOption(
key = ADD_WALLET_KEY_ADD,
label = resourceReference(string.home_button_add_existing_wallet),
),
BottomSheetOption(
key = ADD_WALLET_KEY_BUY,
label = resourceReference(string.details_buy_wallet),
),
),
onOptionClick = { optionKey ->
dismissAddWalletBottomSheet()
when (optionKey) {
ADD_WALLET_KEY_CREATE -> router.push(AppRoute.CreateWalletSelection)
ADD_WALLET_KEY_ADD -> router.push(AppRoute.AddExistingWallet)
ADD_WALLET_KEY_BUY -> modelScope.launch {
generateBuyTangemCardLinkUseCase.invoke().let { urlOpener.openUrl(it) }
}
}
},
)
}
companion object {
private const val ADD_WALLET_KEY_CREATE = "create"
private const val ADD_WALLET_KEY_ADD = "add"
private const val ADD_WALLET_KEY_BUY = "buy"
}
}

View file

@ -15,9 +15,13 @@ import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
import com.tangem.common.ui.userwallet.UserWalletItem
import com.tangem.core.ui.R.*
import com.tangem.core.ui.components.block.BlockCard
import com.tangem.core.ui.components.bottomsheets.OptionsBottomSheet
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.resolveReference
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreview
import com.tangem.features.details.component.UserWalletListComponent
@ -44,6 +48,8 @@ internal fun UserWalletListBlock(state: UserWalletListUM, modifier: Modifier = M
onClick = state.onAddNewWalletClick,
)
}
AddWalletBottomSheet(state.addWalletBottomSheet)
}
@Composable
@ -94,6 +100,15 @@ private fun AddWalletButton(
}
}
@Composable
private fun AddWalletBottomSheet(config: TangemBottomSheetConfig) {
OptionsBottomSheet(
config = config,
title = resourceReference(string.auth_info_add_wallet_title),
containerColor = TangemTheme.colors.background.tertiary,
)
}
// region Preview
@Composable
@Preview(showBackground = true, widthDp = 360)

View file

@ -18,6 +18,7 @@ dependencies {
/** Core */
implementation(projects.core.configToggles)
implementation(projects.core.decompose)
implementation(projects.core.navigation)
implementation(projects.core.ui)
implementation(projects.core.analytics)
implementation(projects.common.routing)

View file

@ -6,8 +6,12 @@ import com.tangem.core.decompose.di.ModelScoped
import com.tangem.core.decompose.model.Model
import com.tangem.core.decompose.navigation.Router
import com.tangem.core.decompose.ui.UiMessageSender
import com.tangem.core.navigation.url.UrlOpener
import com.tangem.core.ui.components.bottomsheets.BottomSheetOption
import com.tangem.core.ui.components.bottomsheets.OptionsBottomSheetContent
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.message.SnackbarMessage
import com.tangem.domain.core.wallets.UserWalletsListRepository
import com.tangem.domain.core.wallets.error.UnlockWalletError
@ -16,10 +20,9 @@ import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.domain.models.wallet.isLocked
import com.tangem.domain.settings.CanUseBiometryUseCase
import com.tangem.domain.wallets.repository.WalletsRepository
import com.tangem.domain.wallets.usecase.GenerateBuyTangemCardLinkUseCase
import com.tangem.features.wallet.utils.UserWalletsFetcher
import com.tangem.features.welcome.impl.R
import com.tangem.features.welcome.impl.ui.state.AddWalletBottomSheetContentUM
import com.tangem.features.welcome.impl.ui.state.AddWalletBottomSheetContentUM.Option.*
import com.tangem.features.welcome.impl.ui.state.WelcomeUM
import com.tangem.hot.sdk.model.HotWalletId
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
@ -41,6 +44,8 @@ internal class WelcomeModel @Inject constructor(
private val userWalletsListRepository: UserWalletsListRepository,
private val canUseBiometryUseCase: CanUseBiometryUseCase,
private val walletsRepository: WalletsRepository,
private val generateBuyTangemCardLinkUseCase: GenerateBuyTangemCardLinkUseCase,
private val urlOpener: UrlOpener,
userWalletsFetcherFactory: UserWalletsFetcher.Factory,
) : Model() {
@ -145,8 +150,27 @@ internal class WelcomeModel @Inject constructor(
currentState.copy(
addWalletBottomSheet = TangemBottomSheetConfig(
isShown = true,
content = AddWalletBottomSheetContentUM(
onOptionClick = ::onAddWalletOptionClick,
content = OptionsBottomSheetContent(
options = persistentListOf(
BottomSheetOption(
key = ADD_WALLET_KEY_CREATE,
label = resourceReference(R.string.home_button_create_new_wallet),
),
BottomSheetOption(
key = ADD_WALLET_KEY_ADD,
label = resourceReference(R.string.home_button_add_existing_wallet),
),
BottomSheetOption(
key = ADD_WALLET_KEY_BUY,
label = resourceReference(R.string.details_buy_wallet),
),
),
onOptionClick = { optionKey ->
updateSelectState {
it.copy(addWalletBottomSheet = it.addWalletBottomSheet.copy(isShown = false))
}
onAddWalletOptionClick(optionKey)
},
),
onDismissRequest = {
updateSelectState {
@ -158,11 +182,13 @@ internal class WelcomeModel @Inject constructor(
}
}
private fun onAddWalletOptionClick(option: AddWalletBottomSheetContentUM.Option) {
when (option) {
Create -> router.push(AppRoute.CreateWalletSelection)
Add -> router.push(AppRoute.AddExistingWallet)
Buy -> Unit // TODO
private fun onAddWalletOptionClick(optionKey: String) {
when (optionKey) {
ADD_WALLET_KEY_CREATE -> router.push(AppRoute.CreateWalletSelection)
ADD_WALLET_KEY_ADD -> router.push(AppRoute.AddExistingWallet)
ADD_WALLET_KEY_BUY -> modelScope.launch {
generateBuyTangemCardLinkUseCase.invoke().let { urlOpener.openUrl(it) }
}
}
}
@ -242,4 +268,10 @@ internal class WelcomeModel @Inject constructor(
}
}
}
companion object {
private const val ADD_WALLET_KEY_CREATE = "create"
private const val ADD_WALLET_KEY_ADD = "add"
private const val ADD_WALLET_KEY_BUY = "buy"
}
}

View file

@ -1,90 +0,0 @@
package com.tangem.features.welcome.impl.ui
import android.content.res.Configuration
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.padding
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.Preview
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig
import com.tangem.core.ui.components.bottomsheets.sheet.TangemBottomSheet
import com.tangem.core.ui.components.inputrow.InputRowDefault
import com.tangem.core.ui.decorations.roundedShapeItemDecoration
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreview
import com.tangem.features.welcome.impl.R
import com.tangem.features.welcome.impl.ui.state.AddWalletBottomSheetContentUM
@Composable
fun AddWalletBottomSheet(config: TangemBottomSheetConfig) {
TangemBottomSheet<AddWalletBottomSheetContentUM>(
config = config,
titleText = resourceReference(R.string.auth_info_add_wallet_title),
containerColor = TangemTheme.colors.background.tertiary,
content = { Content(it) },
)
}
@Composable
private fun Content(content: AddWalletBottomSheetContentUM) {
Column(
modifier = Modifier
.padding(
start = TangemTheme.dimens.spacing16,
end = TangemTheme.dimens.spacing16,
bottom = TangemTheme.dimens.spacing16,
),
) {
InputRowDefault(
text = resourceReference(R.string.home_button_create_new_wallet),
modifier = Modifier
.roundedShapeItemDecoration(
currentIndex = 0,
lastIndex = 3,
addDefaultPadding = false,
)
.background(TangemTheme.colors.background.action)
.clickable { content.onOptionClick(AddWalletBottomSheetContentUM.Option.Create) },
)
InputRowDefault(
text = resourceReference(R.string.home_button_add_existing_wallet),
modifier = Modifier
.roundedShapeItemDecoration(
currentIndex = 1,
lastIndex = 2,
addDefaultPadding = false,
)
.background(TangemTheme.colors.background.action)
.clickable { content.onOptionClick(AddWalletBottomSheetContentUM.Option.Add) },
)
InputRowDefault(
text = resourceReference(R.string.details_buy_wallet),
modifier = Modifier
.roundedShapeItemDecoration(
currentIndex = 2,
lastIndex = 2,
addDefaultPadding = false,
)
.background(TangemTheme.colors.background.action)
.clickable { content.onOptionClick(AddWalletBottomSheetContentUM.Option.Buy) },
)
}
}
@Preview(showBackground = true, widthDp = 360)
@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES)
@Composable
private fun SecurityScoreBottomSheetPreview() {
TangemThemePreview {
AddWalletBottomSheet(
config = TangemBottomSheetConfig(
isShown = true,
onDismissRequest = {},
content = AddWalletBottomSheetContentUM(),
),
)
}
}

View file

@ -19,9 +19,13 @@ import androidx.compose.ui.res.vectorResource
import androidx.compose.ui.unit.dp
import com.tangem.common.ui.userwallet.UserWalletItem
import com.tangem.common.ui.userwallet.state.UserWalletItemUM
import com.tangem.core.ui.R.*
import com.tangem.core.ui.components.*
import com.tangem.core.ui.components.block.TangemBlockCardColors
import com.tangem.core.ui.components.bottomsheets.OptionsBottomSheet
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig
import com.tangem.core.ui.components.buttons.common.TangemButtonsDefaults
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.extensions.stringResourceSafe
import com.tangem.core.ui.res.TangemTheme
import com.tangem.features.welcome.impl.R
@ -170,4 +174,13 @@ private fun AnimatedContentScope.TitleText(modifier: Modifier = Modifier) {
color = TangemTheme.colors.text.secondary,
)
}
}
@Composable
fun AddWalletBottomSheet(config: TangemBottomSheetConfig) {
OptionsBottomSheet(
config = config,
title = resourceReference(string.auth_info_add_wallet_title),
containerColor = TangemTheme.colors.background.tertiary,
)
}