Updated on 2026-08-14

This commit is contained in:
Tangem 2023-06-21 14:54:43 +08:00
parent 95d45c5a19
commit 53100fe46a
23 changed files with 495 additions and 400 deletions

View file

@ -1,12 +1,16 @@
package com.tangem.tap.features.tokens.impl.presentation.states
import androidx.compose.runtime.Immutable
import kotlinx.collections.immutable.ImmutableList
/**
* Network item state
* Token item state.
* All subclasses is stable, but @Immutable annotation is required to use this sealed class like as
* field of TokensListStateHolder.
*
[REDACTED_AUTHOR]
*/
@Immutable
sealed interface TokenItemState {
/** Token id */

View file

@ -1,5 +1,6 @@
package com.tangem.tap.features.tokens.impl.presentation.states
import androidx.compose.runtime.Immutable
import androidx.paging.LoadState
import androidx.paging.PagingData
import kotlinx.coroutines.flow.Flow
@ -9,6 +10,7 @@ import kotlinx.coroutines.flow.Flow
*
[REDACTED_AUTHOR]
*/
@Immutable
internal sealed interface TokensListStateHolder {
/** Toolbar state */

View file

@ -1,6 +1,13 @@
package com.tangem.tap.features.tokens.impl.presentation.states
/** Toolbar state */
import androidx.compose.runtime.Immutable
/**
* Toolbar state.
* All subclasses is stable, but @Immutable annotation is required to use this sealed class like as
* field of TokensListStateHolder.
*/
@Immutable
sealed interface TokensListToolbarState {
/** Callback to be invoked when BackButton is being clicked */

View file

@ -312,14 +312,8 @@ private fun TangemButton(
textStyle: TextStyle = TangemTheme.typography.button,
) {
Button(
modifier = modifier
.width(IntrinsicSize.Min)
.heightIn(min = size.toHeightDp()),
onClick = {
if (!showProgress) {
onClick()
}
},
modifier = modifier.heightIn(min = size.toHeightDp()),
onClick = { if (!showProgress) onClick() },
enabled = enabled,
elevation = elevation,
shape = size.toShape(),
@ -588,59 +582,44 @@ private open class TangemButtonColors(
// region Preview
@Composable
private fun PrimaryButtonSample(modifier: Modifier = Modifier) {
private fun PrimaryButtonSample() {
Column(
modifier = modifier
.background(TangemTheme.colors.background.primary),
modifier = Modifier.background(TangemTheme.colors.background.primary),
verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing8),
) {
PrimaryButton(
modifier = Modifier.fillMaxWidth(),
text = "Manage tokens",
onClick = { /* no-op */ },
)
Divider(modifier = Modifier.padding(vertical = TangemTheme.dimens.spacing8))
PrimaryButton(
modifier = Modifier.fillMaxWidth(),
showProgress = true,
text = "Manage tokens",
onClick = { /* no-op */ },
)
Divider(modifier = Modifier.padding(vertical = TangemTheme.dimens.spacing8))
PrimaryButton(modifier = Modifier.fillMaxWidth(), text = "Manage tokens", onClick = { })
PrimaryButton(modifier = Modifier.fillMaxWidth(), showProgress = true, text = "Manage tokens", onClick = { })
PrimaryButtonIconEnd(
modifier = Modifier.fillMaxWidth(),
text = "Manage tokens",
iconResId = R.drawable.ic_tangem_24,
onClick = { /* no-op */ },
onClick = { },
)
Divider(modifier = Modifier.padding(vertical = TangemTheme.dimens.spacing8))
PrimaryButtonIconStart(
modifier = Modifier.fillMaxWidth(),
text = "Manage tokens",
iconResId = R.drawable.ic_tangem_24,
onClick = { /* no-op */ },
onClick = { },
)
Divider(modifier = Modifier.padding(vertical = TangemTheme.dimens.spacing8))
PrimaryButton(
modifier = Modifier.fillMaxWidth(),
text = "Manage tokens",
enabled = false,
onClick = { /* no-op */ },
onClick = { },
)
Divider(modifier = Modifier.padding(vertical = TangemTheme.dimens.spacing8))
PrimaryButtonIconEnd(
modifier = Modifier.fillMaxWidth(),
text = "Manage tokens",
iconResId = R.drawable.ic_tangem_24,
enabled = false,
onClick = { /* no-op */ },
onClick = { },
)
Divider(modifier = Modifier.padding(vertical = TangemTheme.dimens.spacing8))
PrimaryButtonIconStart(
modifier = Modifier.fillMaxWidth(),
text = "Manage tokens",
iconResId = R.drawable.ic_tangem_24,
enabled = false,
onClick = { /* no-op */ },
onClick = { },
)
}
}
@ -662,59 +641,44 @@ private fun PrimaryButtonPreview_Dark() {
}
@Composable
private fun SecondaryButtonSample(modifier: Modifier = Modifier) {
private fun SecondaryButtonSample() {
Column(
modifier = modifier
.background(TangemTheme.colors.background.primary),
modifier = Modifier.background(TangemTheme.colors.background.primary),
verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing8),
) {
SecondaryButton(
modifier = Modifier.fillMaxWidth(),
text = "Manage tokens",
onClick = { /* no-op */ },
)
Divider(modifier = Modifier.padding(vertical = TangemTheme.dimens.spacing8))
SecondaryButton(
modifier = Modifier.fillMaxWidth(),
showProgress = true,
text = "Manage tokens",
onClick = { /* no-op */ },
)
Divider(modifier = Modifier.padding(vertical = TangemTheme.dimens.spacing8))
SecondaryButton(modifier = Modifier.fillMaxWidth(), text = "Manage tokens", onClick = { })
SecondaryButton(modifier = Modifier.fillMaxWidth(), showProgress = true, text = "Manage tokens", onClick = { })
SecondaryButtonIconEnd(
modifier = Modifier.fillMaxWidth(),
text = "Manage tokens",
iconResId = R.drawable.ic_tangem_24,
onClick = { /* no-op */ },
onClick = { },
)
Divider(modifier = Modifier.padding(vertical = TangemTheme.dimens.spacing8))
SecondaryButtonIconStart(
modifier = Modifier.fillMaxWidth(),
text = "Manage tokens",
iconResId = R.drawable.ic_tangem_24,
onClick = { /* no-op */ },
onClick = { },
)
Divider(modifier = Modifier.padding(vertical = TangemTheme.dimens.spacing8))
SecondaryButton(
modifier = Modifier.fillMaxWidth(),
text = "Manage tokens",
enabled = false,
onClick = { /* no-op */ },
onClick = { },
)
Divider(modifier = Modifier.padding(vertical = TangemTheme.dimens.spacing8))
SecondaryButtonIconEnd(
modifier = Modifier.fillMaxWidth(),
text = "Manage tokens",
iconResId = R.drawable.ic_tangem_24,
enabled = false,
onClick = { /* no-op */ },
onClick = { },
)
Divider(modifier = Modifier.padding(vertical = TangemTheme.dimens.spacing8))
SecondaryButtonIconStart(
modifier = Modifier.fillMaxWidth(),
text = "Manage tokens",
iconResId = R.drawable.ic_tangem_24,
enabled = false,
onClick = { /* no-op */ },
onClick = { },
)
}
}
@ -736,50 +700,23 @@ private fun SecondaryButtonPreview_Dark() {
}
@Composable
private fun TextButtonSample(modifier: Modifier = Modifier) {
private fun TextButtonSample() {
Column(
modifier = modifier
.background(TangemTheme.colors.background.primary),
modifier = Modifier.background(TangemTheme.colors.background.primary),
verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing8),
) {
TextButton(
text = "Enabled",
onClick = { /* no-op */ },
)
Divider(modifier = Modifier.padding(vertical = TangemTheme.dimens.spacing8))
TextButtonIconStart(
text = "Enabled",
iconResId = R.drawable.ic_plus_24,
onClick = { /* no-op */ },
)
Divider(modifier = Modifier.padding(vertical = TangemTheme.dimens.spacing8))
TextButton(
text = "Enabled",
enabled = false,
onClick = { /* no-op */ },
)
Divider(modifier = Modifier.padding(vertical = TangemTheme.dimens.spacing8))
TextButtonIconStart(
text = "Enabled",
iconResId = R.drawable.ic_plus_24,
enabled = false,
onClick = { /* no-op */ },
)
Divider(modifier = Modifier.padding(vertical = TangemTheme.dimens.spacing8))
WarningTextButton(
text = "Delete",
onClick = { /* no-op */ },
)
Divider(modifier = Modifier.padding(vertical = TangemTheme.dimens.spacing8))
SelectorButton(
text = "USD",
onClick = { /* no-op */ },
)
TextButton(text = "Enabled", onClick = { })
TextButtonIconStart(text = "Enabled", iconResId = R.drawable.ic_plus_24, onClick = { })
TextButton(text = "Enabled", enabled = false, onClick = { })
TextButtonIconStart(text = "Enabled", iconResId = R.drawable.ic_plus_24, enabled = false, onClick = { })
WarningTextButton(text = "Delete", onClick = { })
SelectorButton(text = "USD", onClick = { })
}
}
@Preview(showBackground = true, widthDp = 360)
@Composable
private fun TextButtonPreview_Light() {
private fun TextButtonPreview_LightTheme() {
TangemTheme {
TextButtonSample()
}
@ -787,62 +724,30 @@ private fun TextButtonPreview_Light() {
@Preview(showBackground = true, widthDp = 360)
@Composable
private fun TextButtonPreview_Dark() {
private fun TextButtonPreview_DarkTheme() {
TangemTheme(isDark = true) {
TextButtonSample()
}
}
@Composable
private fun ActionButtonSample(modifier: Modifier = Modifier) {
private fun ActionButtonSample() {
Column(
modifier = modifier
.background(TangemTheme.colors.background.primary),
modifier = Modifier.background(TangemTheme.colors.background.primary),
verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing8),
) {
RoundedActionButton(
text = "Send",
iconResId = R.drawable.ic_arrow_up_24,
onClick = { /* [REDACTED_TODO_COMMENT]*/ },
)
Divider(modifier = Modifier.padding(vertical = TangemTheme.dimens.spacing8))
ActionButton(
text = "Send",
iconResId = R.drawable.ic_arrow_up_24,
onClick = { /* [REDACTED_TODO_COMMENT]*/ },
)
Divider(modifier = Modifier.padding(vertical = TangemTheme.dimens.spacing8))
BackgroundActionButton(
text = "Send",
iconResId = R.drawable.ic_arrow_up_24,
onClick = { /* [REDACTED_TODO_COMMENT]*/ },
)
Divider(modifier = Modifier.padding(vertical = TangemTheme.dimens.spacing8))
RoundedActionButton(
text = "Send",
iconResId = R.drawable.ic_arrow_up_24,
enabled = false,
onClick = { /* [REDACTED_TODO_COMMENT]*/ },
)
Divider(modifier = Modifier.padding(vertical = TangemTheme.dimens.spacing8))
ActionButton(
text = "Send",
iconResId = R.drawable.ic_arrow_up_24,
enabled = false,
onClick = { /* [REDACTED_TODO_COMMENT]*/ },
)
Divider(modifier = Modifier.padding(vertical = TangemTheme.dimens.spacing8))
BackgroundActionButton(
text = "Send",
iconResId = R.drawable.ic_arrow_up_24,
enabled = false,
onClick = { /* [REDACTED_TODO_COMMENT]*/ },
)
RoundedActionButton(text = "Send", iconResId = R.drawable.ic_arrow_up_24, onClick = { })
ActionButton(text = "Send", iconResId = R.drawable.ic_arrow_up_24, onClick = { })
BackgroundActionButton(text = "Send", iconResId = R.drawable.ic_arrow_up_24, onClick = { })
RoundedActionButton(text = "Send", iconResId = R.drawable.ic_arrow_up_24, enabled = false, onClick = { })
ActionButton(text = "Send", iconResId = R.drawable.ic_arrow_up_24, enabled = false, onClick = { })
BackgroundActionButton(text = "Send", iconResId = R.drawable.ic_arrow_up_24, enabled = false, onClick = { })
}
}
@Preview(showBackground = true, widthDp = 360)
@Composable
private fun ActionButtonPreview_Light() {
private fun ActionButtonPreview_LightTheme() {
TangemTheme {
ActionButtonSample()
}
@ -850,7 +755,7 @@ private fun ActionButtonPreview_Light() {
@Preview(showBackground = true, widthDp = 360)
@Composable
private fun ActionButtonPreview_Dark() {
private fun ActionButtonPreview_DarkTheme() {
TangemTheme(isDark = true) {
ActionButtonSample()
}

View file

@ -2,22 +2,42 @@ package com.tangem.core.ui.extensions
import androidx.annotation.StringRes
import androidx.compose.runtime.Composable
import androidx.compose.runtime.Immutable
import androidx.compose.runtime.ReadOnlyComposable
import androidx.compose.ui.res.stringResource
/**
* Utility class for creating text as [String] or [StringRes].
* It necessary to use [Immutable] annotation because all sealed interface has runtime stability.
* All subclasses are stable.
*/
@Immutable
sealed interface TextReference {
class Res(@StringRes val id: Int, val formatArgs: List<Any> = emptyList()) : TextReference {
constructor(@StringRes id: Int, vararg formatArgs: Any) : this(id, formatArgs.toList())
}
class Str(val value: String) : TextReference
/**
* Text resource id
*
* @property id resource id
* @property formatArgs arguments
*
* Impossible to use [kotlinx.collections.immutable.ImmutableList] because [Any] is unstable.
*/
data class Res(@StringRes val id: Int, val formatArgs: WrappedList<Any> = WrappedList(emptyList())) : TextReference
/**
* Text string
*
* @property value value
*/
data class Str(val value: String) : TextReference
}
/** Get text */
@Composable
@ReadOnlyComposable
fun TextReference.resolveReference(): String {
return when (this) {
is TextReference.Res -> stringResource(this.id, *this.formatArgs.toTypedArray())
is TextReference.Str -> this.value
is TextReference.Res -> stringResource(id, *formatArgs.toTypedArray())
is TextReference.Str -> value
}
}

View file

@ -0,0 +1,10 @@
package com.tangem.core.ui.extensions
import androidx.compose.runtime.Immutable
/**
[REDACTED_AUTHOR]
*/
@JvmInline
@Immutable
value class WrappedList<T>(val data: List<T>) : List<T> by data

View file

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:pathData="M6,13H18V11H6M3,6V8H21V6M10,18H14V16H10V18Z"
android:fillColor="#000000" />
</vector>

View file

@ -22,6 +22,8 @@ dependencies {
implementation(deps.compose.ui.tooling)
implementation(deps.compose.shimmer)
implementation(deps.compose.accompanist.systemUiController)
/** Other libraries */
implementation(deps.kotlin.immutable.collections)
/** DI */

View file

@ -1,13 +1,13 @@
package com.tangem.feature.wallet.presentation.common
import com.tangem.core.ui.R
import com.tangem.feature.wallet.presentation.common.state.NetworkGroupState
import com.tangem.feature.wallet.presentation.common.state.TokenItemState
import com.tangem.feature.wallet.presentation.common.state.TokenItemState.TokenOptionsState
import com.tangem.feature.wallet.presentation.organizetokens.DraggableItem
import com.tangem.feature.wallet.presentation.organizetokens.OrganizeTokensListState
import com.tangem.feature.wallet.presentation.organizetokens.OrganizeTokensStateHolder
import com.tangem.feature.wallet.presentation.wallet.state.WalletCardState
import com.tangem.feature.wallet.presentation.wallet.state.WalletContentItemState
import com.tangem.feature.wallet.presentation.wallet.state.WalletStateHolder
import kotlinx.collections.immutable.persistentListOf
import kotlinx.collections.immutable.toPersistentList
@ -15,7 +15,9 @@ import java.util.UUID
internal object WalletPreviewData {
val walletCardContent = WalletCardState.Content(
val walletTopBarConfig = WalletStateHolder.TopBarConfig(onScanCardClick = {}, onMoreClick = {})
val walletCardContentState = WalletCardState.Content(
id = UUID.randomUUID().toString(),
title = "Wallet 1",
balance = "8923,05 $",
@ -24,7 +26,7 @@ internal object WalletPreviewData {
onClick = {},
)
val walletCardLoading = WalletCardState.Loading(
val walletCardLoadingState = WalletCardState.Loading(
id = UUID.randomUUID().toString(),
title = "Wallet 1",
additionalInfo = "3 cards • Seed enabled",
@ -32,7 +34,7 @@ internal object WalletPreviewData {
onClick = {},
)
val walletCardHiddenContent = WalletCardState.HiddenContent(
val walletCardHiddenContentState = WalletCardState.HiddenContent(
id = UUID.randomUUID().toString(),
title = "Wallet 1",
additionalInfo = "3 cards • Seed enabled",
@ -40,7 +42,7 @@ internal object WalletPreviewData {
onClick = {},
)
val walletCardError = WalletCardState.Error(
val walletCardErrorState = WalletCardState.Error(
id = UUID.randomUUID().toString(),
title = "Wallet 1",
additionalInfo = "3 cards • Seed enabled",
@ -48,15 +50,6 @@ internal object WalletPreviewData {
onClick = {},
)
val walletScreenState = WalletStateHolder(
onBackClick = {},
headerConfig = WalletStateHolder.HeaderConfig(
wallets = persistentListOf(walletCardContent, walletCardLoading, walletCardHiddenContent, walletCardError),
onScanCardClick = {},
onMoreClick = {},
),
)
val tokenItemVisibleState = TokenItemState.Content(
id = UUID.randomUUID().toString(),
tokenIconUrl = null,
@ -109,37 +102,6 @@ internal object WalletPreviewData {
val loadingTokenItemState = TokenItemState.Loading(id = UUID.randomUUID().toString())
val networkGroup = NetworkGroupState.Content(
id = UUID.randomUUID().toString(),
networkName = "Ethereum",
tokens = persistentListOf(
tokenItemVisibleState.copy(
id = "token_1",
name = "Ethereum",
tokenIconResId = R.drawable.img_eth_22,
networkIconResId = null,
amount = "1,89340821 ETH",
),
tokenItemVisibleState.copy(
id = "token_2",
networkIconResId = R.drawable.img_eth_22,
amount = "733,71097 MATIC",
),
tokenItemVisibleState.copy(
id = "token_3",
name = "USDT",
tokenIconResId = R.drawable.img_arbitrum_22,
networkIconResId = R.drawable.img_eth_22,
amount = "0,25404523 ARB",
),
),
)
val draggableNetworkGroup = NetworkGroupState.Draggable(
id = "group_1",
networkName = "Ethereum",
)
private const val networksSize = 10
private const val tokensSize = 3
val draggableItems = List(networksSize) { it }
@ -147,10 +109,8 @@ internal object WalletPreviewData {
val n = index + 1
val group = DraggableItem.GroupHeader(
groupState = NetworkGroupState.Draggable(
id = "group_$n",
networkName = "$n",
),
id = "group_$n",
networkName = "$n",
)
val tokens: MutableList<DraggableItem.Token> = mutableListOf()
@ -198,4 +158,66 @@ internal object WalletPreviewData {
items = draggableTokens,
),
)
val groupedWalletScreenState = WalletStateHolder(
onBackClick = {},
topBarConfig = walletTopBarConfig,
selectedWallet = walletCardContentState,
wallets = persistentListOf(
walletCardContentState,
walletCardLoadingState,
walletCardHiddenContentState,
walletCardErrorState,
),
contentItems = persistentListOf(
WalletContentItemState.NetworkGroupTitle("Bitcoin"),
WalletContentItemState.Token(
tokenItemVisibleState.copy(
id = "token_1",
name = "Ethereum",
tokenIconResId = R.drawable.img_eth_22,
networkIconResId = null,
amount = "1,89340821 ETH",
),
),
WalletContentItemState.Token(
tokenItemVisibleState.copy(
id = "token_2",
name = "Ethereum",
tokenIconResId = R.drawable.img_eth_22,
networkIconResId = null,
amount = "1,89340821 ETH",
),
),
WalletContentItemState.Token(
tokenItemVisibleState.copy(
id = "token_3",
name = "Ethereum",
tokenIconResId = R.drawable.img_eth_22,
networkIconResId = null,
amount = "1,89340821 ETH",
),
),
WalletContentItemState.Token(
tokenItemVisibleState.copy(
id = "token_4",
name = "Ethereum",
tokenIconResId = R.drawable.img_eth_22,
networkIconResId = null,
amount = "1,89340821 ETH",
),
),
WalletContentItemState.NetworkGroupTitle("Ethereum"),
WalletContentItemState.Token(
tokenItemVisibleState.copy(
id = "token_5",
name = "Ethereum",
tokenIconResId = R.drawable.img_eth_22,
networkIconResId = null,
amount = "1,89340821 ETH",
),
),
),
onOrganizeTokensClick = {},
)
}

View file

@ -5,7 +5,6 @@ import androidx.compose.foundation.layout.*
import androidx.compose.material3.Icon
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.key
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.painterResource
@ -15,29 +14,20 @@ import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider
import com.tangem.core.ui.res.TangemTheme
import com.tangem.feature.wallet.impl.R
import com.tangem.feature.wallet.presentation.common.WalletPreviewData
import com.tangem.feature.wallet.presentation.common.state.NetworkGroupState
@Composable
internal fun NetworkGroupItem(state: NetworkGroupState.Content, modifier: Modifier = Modifier) {
internal fun NetworkGroupItem(networkName: String, modifier: Modifier = Modifier) {
InternalNetworkGroupItem(
modifier = modifier,
networkName = state.networkName,
tokens = {
state.tokens.forEach { token ->
key(token.id) {
TokenItem(state = token)
}
}
},
networkName = networkName,
)
}
@Composable
internal fun DraggableNetworkGroupItem(state: NetworkGroupState.Draggable, modifier: Modifier = Modifier) {
internal fun DraggableNetworkGroupItem(networkName: String, modifier: Modifier = Modifier) {
InternalNetworkGroupItem(
modifier = modifier,
networkName = state.networkName,
networkName = networkName,
endIcon = {
Icon(
painter = painterResource(id = R.drawable.ic_group_drop_24),
@ -52,14 +42,13 @@ internal fun DraggableNetworkGroupItem(state: NetworkGroupState.Draggable, modif
private fun InternalNetworkGroupItem(
networkName: String,
modifier: Modifier = Modifier,
tokens: @Composable ColumnScope.() -> Unit = {},
endIcon: @Composable RowScope.() -> Unit = {},
) {
Column(modifier = modifier) {
Row(
modifier = Modifier
.background(TangemTheme.colors.background.primary)
.padding(horizontal = TangemTheme.dimens.spacing12)
.padding(horizontal = TangemTheme.dimens.spacing14)
.fillMaxWidth()
.heightIn(min = TangemTheme.dimens.size48),
verticalAlignment = Alignment.CenterVertically,
@ -72,39 +61,36 @@ private fun InternalNetworkGroupItem(
)
endIcon()
}
tokens()
}
}
// region Preview
@Composable
private fun NetworkGroupItemSample(group: NetworkGroupState) {
when (group) {
is NetworkGroupState.Content -> NetworkGroupItem(group)
is NetworkGroupState.Draggable -> DraggableNetworkGroupItem(group)
private fun NetworkGroupItemSample(isDraggable: Boolean) {
if (isDraggable) {
DraggableNetworkGroupItem(networkName = "Ethereum")
} else {
NetworkGroupItem(networkName = "Ethereum")
}
}
@Preview(showBackground = true, widthDp = 360)
@Composable
private fun NetworkGroupItemPreview_Light(@PreviewParameter(NetworkGroupProvider::class) group: NetworkGroupState) {
private fun NetworkGroupItemPreview_Light(@PreviewParameter(NetworkGroupProvider::class) isDraggable: Boolean) {
TangemTheme {
NetworkGroupItemSample(group)
NetworkGroupItemSample(isDraggable)
}
}
@Preview(showBackground = true, widthDp = 360)
@Composable
private fun NetworkGroupItemPreview_Dark(@PreviewParameter(NetworkGroupProvider::class) group: NetworkGroupState) {
private fun NetworkGroupItemPreview_Dark(@PreviewParameter(NetworkGroupProvider::class) isDraggable: Boolean) {
TangemTheme(isDark = true) {
NetworkGroupItemSample(group)
NetworkGroupItemSample(isDraggable)
}
}
private class NetworkGroupProvider : CollectionPreviewParameterProvider<NetworkGroupState>(
collection = listOf(
WalletPreviewData.networkGroup,
WalletPreviewData.draggableNetworkGroup,
),
private class NetworkGroupProvider : CollectionPreviewParameterProvider<Boolean>(
collection = listOf(true, false),
)
// endregion Preview

View file

@ -206,7 +206,7 @@ private fun InternalTokenItem(
modifier = Modifier
.fillMaxWidth()
.padding(
horizontal = TangemTheme.dimens.spacing12,
horizontal = TangemTheme.dimens.spacing14,
vertical = TangemTheme.dimens.spacing4,
),
) {

View file

@ -1,21 +0,0 @@
package com.tangem.feature.wallet.presentation.common.state
import androidx.compose.runtime.Immutable
import kotlinx.collections.immutable.ImmutableList
@Immutable
internal sealed interface NetworkGroupState {
val id: String
val networkName: String
data class Draggable(
override val id: String,
override val networkName: String,
) : NetworkGroupState
data class Content(
override val id: String,
override val networkName: String,
val tokens: ImmutableList<TokenItemState>,
) : NetworkGroupState
}

View file

@ -93,7 +93,7 @@ private fun DraggableItem(index: Int, item: DraggableItem, lastItemIndex: Int) {
when (item) {
is DraggableItem.GroupHeader -> DraggableNetworkGroupItem(
modifier = itemModifier,
state = item.groupState,
networkName = item.networkName,
)
is DraggableItem.Token -> DraggableTokenItem(
modifier = itemModifier,

View file

@ -1,7 +1,6 @@
package com.tangem.feature.wallet.presentation.organizetokens
import androidx.compose.runtime.Immutable
import com.tangem.feature.wallet.presentation.common.state.NetworkGroupState
import com.tangem.feature.wallet.presentation.common.state.TokenItemState
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.toPersistentList
@ -51,10 +50,9 @@ internal sealed interface DraggableItem {
val id: String
data class GroupHeader(
val groupState: NetworkGroupState.Draggable,
) : DraggableItem {
override val id: String = groupState.id
}
override val id: String,
val networkName: String,
) : DraggableItem
data class Token(
val tokenItemState: TokenItemState.Draggable,

View file

@ -1,9 +1,12 @@
package com.tangem.feature.wallet.presentation.wallet.state
import androidx.annotation.DrawableRes
import androidx.compose.runtime.Immutable
/** Wallet card state */
@Immutable
internal sealed interface WalletCardState {
/** Id */
val id: String

View file

@ -0,0 +1,13 @@
package com.tangem.feature.wallet.presentation.wallet.state
import com.tangem.feature.wallet.presentation.common.state.TokenItemState
/**
[REDACTED_AUTHOR]
*/
internal sealed interface WalletContentItemState {
data class NetworkGroupTitle(val networkName: String) : WalletContentItemState
data class Token(val tokenItemState: TokenItemState) : WalletContentItemState
}

View file

@ -5,26 +5,28 @@ import kotlinx.collections.immutable.ImmutableList
/**
* Wallet state holder
*
* @property onBackClick lambda be invoked when back button is clicked
* @property headerConfig header config
* @property onBackClick lambda be invoked when back button is clicked
* @property topBarConfig top bar config
* @property selectedWallet selected wallet
* @property wallets list of wallets states
* @property contentItems content items
*
[REDACTED_AUTHOR]
*/
internal data class WalletStateHolder(
val onBackClick: () -> Unit,
val headerConfig: HeaderConfig,
val topBarConfig: TopBarConfig,
val selectedWallet: WalletCardState,
val wallets: ImmutableList<WalletCardState>,
val contentItems: ImmutableList<WalletContentItemState>,
val onOrganizeTokensClick: () -> Unit,
) {
/**
* Header config
* Top bar config
*
* @property wallets list of wallets states
* @property onScanCardClick lambda be invoked when scan card button is clicked
* @property onMoreClick lambda be invoked when more button is clicked
*/
data class HeaderConfig(
val wallets: ImmutableList<WalletCardState>,
val onScanCardClick: () -> Unit,
val onMoreClick: () -> Unit,
)
data class TopBarConfig(val onScanCardClick: () -> Unit, val onMoreClick: () -> Unit)
}

View file

@ -1,10 +1,32 @@
package com.tangem.feature.wallet.presentation.wallet.ui
import androidx.activity.compose.BackHandler
import androidx.compose.material.Scaffold
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.itemsIndexed
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.Scaffold
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.composed
import androidx.compose.ui.draw.clip
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider
import com.tangem.core.ui.components.RoundedActionButton
import com.tangem.core.ui.res.TangemTheme
import com.tangem.feature.wallet.impl.R
import com.tangem.feature.wallet.presentation.common.WalletPreviewData
import com.tangem.feature.wallet.presentation.common.component.NetworkGroupItem
import com.tangem.feature.wallet.presentation.common.component.TokenItem
import com.tangem.feature.wallet.presentation.wallet.state.WalletContentItemState
import com.tangem.feature.wallet.presentation.wallet.state.WalletStateHolder
import com.tangem.feature.wallet.presentation.wallet.ui.components.WalletHeader
import com.tangem.feature.wallet.presentation.wallet.ui.components.WalletCardsList
import com.tangem.feature.wallet.presentation.wallet.ui.components.WalletTopBar
/**
* Wallet screen
@ -18,8 +40,109 @@ internal fun WalletScreen(state: WalletStateHolder) {
BackHandler(onBack = state.onBackClick)
Scaffold(
topBar = { WalletHeader(config = state.headerConfig) },
) {
// TODO: [REDACTED_TASK_KEY] Design a body with tokens and transactions
topBar = { WalletTopBar(config = state.topBarConfig) },
containerColor = TangemTheme.colors.background.secondary,
) { scaffoldPaddings ->
val lastContentItemIndex = remember(state.contentItems) {
state.contentItems.lastIndex
}
LazyColumn(
modifier = Modifier
.padding(scaffoldPaddings)
.fillMaxSize(),
horizontalAlignment = Alignment.CenterHorizontally,
) {
item { WalletCardsList(wallets = state.wallets) }
itemsIndexed(
items = state.contentItems,
key = { _, item ->
when (item) {
is WalletContentItemState.NetworkGroupTitle -> item.networkName
is WalletContentItemState.Token -> item.tokenItemState.id
}
},
) { index, item ->
val itemModifier = Modifier
.padding(horizontal = TangemTheme.dimens.spacing16)
.clipFirstAndLastItems(index, lastContentItemIndex)
when (item) {
is WalletContentItemState.NetworkGroupTitle -> {
NetworkGroupItem(
networkName = item.networkName,
modifier = itemModifier,
)
}
is WalletContentItemState.Token -> {
TokenItem(
state = item.tokenItemState,
modifier = itemModifier,
)
}
}
}
item {
RoundedActionButton(
text = stringResource(id = R.string.organize_tokens_title),
iconResId = R.drawable.ic_filter_24,
onClick = state.onOrganizeTokensClick,
modifier = Modifier.padding(top = TangemTheme.dimens.spacing14),
)
}
}
}
}
}
private fun Modifier.clipFirstAndLastItems(index: Int, lastItemIndex: Int): Modifier = composed {
when (index) {
0 -> {
this
.padding(top = TangemTheme.dimens.spacing14)
.clip(
RoundedCornerShape(
topStart = TangemTheme.dimens.radius16,
topEnd = TangemTheme.dimens.radius16,
),
)
}
lastItemIndex -> {
this
.clip(
RoundedCornerShape(
bottomStart = TangemTheme.dimens.radius16,
bottomEnd = TangemTheme.dimens.radius16,
),
)
}
else -> this
}
}
// region Preview
@Preview(showBackground = true, widthDp = 360)
@Composable
private fun WalletScreenPreview_Light(
@PreviewParameter(WalletScreenParameterProvider::class) state: WalletStateHolder,
) {
TangemTheme {
WalletScreen(state)
}
}
@Preview(showBackground = true, widthDp = 360)
@Composable
private fun WalletScreenPreview_Dark(
@PreviewParameter(WalletScreenParameterProvider::class) state: WalletStateHolder,
) {
TangemTheme(isDark = true) {
WalletScreen(state)
}
}
private class WalletScreenParameterProvider : CollectionPreviewParameterProvider<WalletStateHolder>(
collection = listOf(WalletPreviewData.groupedWalletScreenState),
)
// endregion Preview

View file

@ -44,7 +44,7 @@ internal fun WalletCard(state: WalletCardState, modifier: Modifier = Modifier) {
ConstraintLayout(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = TangemTheme.dimens.spacing12),
.padding(horizontal = TangemTheme.dimens.spacing14),
) {
val (balanceBlock, imageItem) = createRefs()
Column(
@ -182,10 +182,10 @@ private fun Preview_WalletCard_DarkTheme(@PreviewParameter(WalletCardStateProvid
private class WalletCardStateProvider : CollectionPreviewParameterProvider<WalletCardState>(
collection = listOf(
WalletPreviewData.walletCardContent,
WalletPreviewData.walletCardLoading,
WalletPreviewData.walletCardHiddenContent,
WalletPreviewData.walletCardError,
WalletPreviewData.walletCardContentState,
WalletPreviewData.walletCardLoadingState,
WalletPreviewData.walletCardHiddenContentState,
WalletPreviewData.walletCardErrorState,
),
)

View file

@ -0,0 +1,78 @@
package com.tangem.feature.wallet.presentation.wallet.ui.components
import androidx.compose.animation.core.*
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.background
import androidx.compose.foundation.gestures.snapping.rememberSnapFlingBehavior
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyRow
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.material3.*
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalConfiguration
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import com.tangem.core.ui.res.TangemTheme
import com.tangem.feature.wallet.presentation.common.WalletPreviewData
import com.tangem.feature.wallet.presentation.wallet.state.WalletCardState
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.persistentListOf
/**
* Wallets list
*
* @param wallets list of wallet state
*
[REDACTED_AUTHOR]
*/
@OptIn(ExperimentalFoundationApi::class)
@Composable
internal fun WalletCardsList(wallets: ImmutableList<WalletCardState>) {
val horizontalCardPadding = TangemTheme.dimens.spacing16
val itemWidth = LocalConfiguration.current.screenWidthDp.dp - horizontalCardPadding * 2
val lazyListState = rememberLazyListState()
LazyRow(
modifier = Modifier.background(color = TangemTheme.colors.background.secondary),
state = lazyListState,
contentPadding = PaddingValues(horizontal = TangemTheme.dimens.spacing16),
horizontalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing8),
flingBehavior = rememberSnapFlingBehavior(lazyListState = lazyListState),
) {
items(items = wallets, key = WalletCardState::id) { state ->
WalletCard(state = state, modifier = Modifier.width(itemWidth))
}
}
}
@Preview
@Composable
private fun Preview_WalletHeader_LightTheme() {
TangemTheme(isDark = false) {
WalletCardsList(
wallets = persistentListOf(
WalletPreviewData.walletCardContentState,
WalletPreviewData.walletCardLoadingState,
WalletPreviewData.walletCardHiddenContentState,
WalletPreviewData.walletCardErrorState,
),
)
}
}
@Preview
@Composable
private fun Preview_WalletHeader_DarkTheme() {
TangemTheme(isDark = true) {
WalletCardsList(
wallets = persistentListOf(
WalletPreviewData.walletCardContentState,
WalletPreviewData.walletCardLoadingState,
WalletPreviewData.walletCardHiddenContentState,
WalletPreviewData.walletCardErrorState,
),
)
}
}

View file

@ -1,123 +0,0 @@
package com.tangem.feature.wallet.presentation.wallet.ui.components
import androidx.compose.animation.core.*
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.LocalOverscrollConfiguration
import androidx.compose.foundation.background
import androidx.compose.foundation.gestures.snapping.rememberSnapFlingBehavior
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyRow
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.material3.*
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalConfiguration
import androidx.compose.ui.res.painterResource
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.core.ui.res.TangemTheme
import com.tangem.feature.wallet.impl.R
import com.tangem.feature.wallet.presentation.common.WalletPreviewData
import com.tangem.feature.wallet.presentation.wallet.state.WalletCardState
import com.tangem.feature.wallet.presentation.wallet.state.WalletStateHolder
import kotlinx.collections.immutable.persistentListOf
/**
* Wallet screen header
*
* @param config config
*
[REDACTED_AUTHOR]
*/
@OptIn(ExperimentalFoundationApi::class)
@Composable
internal fun WalletHeader(config: WalletStateHolder.HeaderConfig) {
Column(
modifier = Modifier
.background(color = TangemTheme.colors.background.secondary)
.padding(bottom = TangemTheme.dimens.spacing14),
) {
TopBar(onScanCardClick = config.onScanCardClick, onMoreClick = config.onMoreClick)
CompositionLocalProvider(LocalOverscrollConfiguration provides null) {
val lazyListState = rememberLazyListState()
LazyRow(
state = lazyListState,
contentPadding = PaddingValues(horizontal = TangemTheme.dimens.spacing16),
horizontalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing8),
flingBehavior = rememberSnapFlingBehavior(lazyListState = lazyListState),
) {
items(items = config.wallets, key = WalletCardState::id) { state ->
WalletCard(
state = state,
modifier = Modifier.width(
LocalConfiguration.current.screenWidthDp.dp - TangemTheme.dimens.size32,
),
)
}
}
}
}
}
@OptIn(ExperimentalMaterial3Api::class)
@Composable
private fun TopBar(onScanCardClick: () -> Unit, onMoreClick: () -> Unit) {
TopAppBar(
title = {
Icon(painter = painterResource(id = R.drawable.img_tangem_logo_90_24), contentDescription = null)
},
actions = {
IconButton(onClick = onScanCardClick) {
Icon(painter = painterResource(id = R.drawable.ic_tap_card_24), contentDescription = "Scan card")
}
IconButton(onClick = onMoreClick) {
Icon(painter = painterResource(id = R.drawable.ic_more_vertical_24), contentDescription = "More")
}
},
colors = TopAppBarDefaults.topAppBarColors(
containerColor = TangemTheme.colors.background.secondary,
titleContentColor = TangemTheme.colors.icon.primary1,
actionIconContentColor = TangemTheme.colors.icon.primary1,
),
)
}
@Preview
@Composable
private fun Preview_WalletHeader_LightTheme(
@PreviewParameter(WalletHeaderProvider::class) state: WalletStateHolder.HeaderConfig,
) {
TangemTheme(isDark = false) {
WalletHeader(state)
}
}
@Preview
@Composable
private fun Preview_WalletHeader_DarkTheme(
@PreviewParameter(WalletHeaderProvider::class) state: WalletStateHolder.HeaderConfig,
) {
TangemTheme(isDark = true) {
WalletHeader(state)
}
}
private class WalletHeaderProvider : CollectionPreviewParameterProvider<WalletStateHolder.HeaderConfig>(
collection = listOf(
WalletStateHolder.HeaderConfig(
wallets = persistentListOf(
WalletPreviewData.walletCardContent,
WalletPreviewData.walletCardLoading,
WalletPreviewData.walletCardHiddenContent,
WalletPreviewData.walletCardError,
),
onScanCardClick = {},
onMoreClick = {},
),
),
)

View file

@ -0,0 +1,55 @@
package com.tangem.feature.wallet.presentation.wallet.ui.components
import androidx.compose.material3.*
import androidx.compose.runtime.Composable
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.tooling.preview.Preview
import com.tangem.core.ui.res.TangemTheme
import com.tangem.feature.wallet.impl.R
import com.tangem.feature.wallet.presentation.common.WalletPreviewData
import com.tangem.feature.wallet.presentation.wallet.state.WalletStateHolder
/**
* Wallet screen top bar
*
* @param config top bar config
*/
@OptIn(ExperimentalMaterial3Api::class)
@Composable
internal fun WalletTopBar(config: WalletStateHolder.TopBarConfig) {
TopAppBar(
title = {
Icon(painter = painterResource(id = R.drawable.img_tangem_logo_90_24), contentDescription = null)
},
actions = {
IconButton(onClick = config.onScanCardClick) {
Icon(painter = painterResource(id = R.drawable.ic_tap_card_24), contentDescription = "Scan card")
}
IconButton(onClick = config.onMoreClick) {
Icon(painter = painterResource(id = R.drawable.ic_more_vertical_24), contentDescription = "More")
}
},
colors = TopAppBarDefaults.topAppBarColors(
containerColor = TangemTheme.colors.background.secondary,
titleContentColor = TangemTheme.colors.icon.primary1,
actionIconContentColor = TangemTheme.colors.icon.primary1,
),
scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior(),
)
}
@Preview
@Composable
private fun Preview_WalletTopBar_LightTheme() {
TangemTheme(isDark = false) {
WalletTopBar(config = WalletPreviewData.walletTopBarConfig)
}
}
@Preview
@Composable
private fun Preview_WalletTopBar_DarkTheme() {
TangemTheme(isDark = true) {
WalletTopBar(config = WalletPreviewData.walletTopBarConfig)
}
}

View file

@ -27,9 +27,9 @@ internal class WalletViewModel @Inject constructor() : ViewModel() {
private set
// TODO: [REDACTED_TASK_KEY] Use production data instead of WalletPreviewData
private fun getInitialState(): WalletStateHolder = WalletPreviewData.walletScreenState.copy(
private fun getInitialState(): WalletStateHolder = WalletPreviewData.groupedWalletScreenState.copy(
onBackClick = { router.popBackStack() },
headerConfig = WalletPreviewData.walletScreenState.headerConfig.copy(
topBarConfig = WalletPreviewData.groupedWalletScreenState.topBarConfig.copy(
onScanCardClick = { router.openOrganizeTokensScreen() },
),
)