Updated on 2026-08-14

This commit is contained in:
Tangem 2024-08-05 15:02:40 +04:00
commit 6a360fe95b
24 changed files with 1018 additions and 81 deletions

View file

@ -3,6 +3,7 @@ package com.tangem.core.ui.components
import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.ime
import androidx.compose.runtime.Composable
import androidx.compose.runtime.Stable
import androidx.compose.runtime.State
import androidx.compose.runtime.rememberUpdatedState
import androidx.compose.ui.platform.LocalDensity
@ -14,11 +15,15 @@ sealed interface Keyboard {
data class Opened(override val height: Dp) : Keyboard
object Closed : Keyboard {
data object Closed : Keyboard {
override val height: Dp = 0.dp
}
}
val Keyboard.isOpened: Boolean
@Stable
get() = this is Keyboard.Opened
/**
* Allows to subscribe to a soft keyboard to detect when it's open/closed
*/

View file

@ -10,6 +10,7 @@ import androidx.compose.runtime.*
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.unit.dp
import com.tangem.core.ui.components.appbar.models.TopAppBarButtonUM
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.res.LocalBottomSheetAlwaysVisible
@ -114,6 +115,7 @@ inline fun <reified T : TangemBottomSheetConfigContent> PreviewBottomSheet(
crossinline content: @Composable (ColumnScope.(T) -> Unit),
) {
BasicBottomSheet<T>(
modifier = Modifier.width(360.dp),
config = config,
sheetState = SheetState(
skipPartiallyExpanded = true,
@ -137,6 +139,7 @@ inline fun <reified T : TangemBottomSheetConfigContent> BasicBottomSheet(
addBottomInsets: Boolean,
crossinline title: @Composable (BoxScope.(T) -> Unit),
crossinline content: @Composable (ColumnScope.(T) -> Unit),
modifier: Modifier = Modifier,
) {
val model = config.content as? T ?: return
@ -145,7 +148,7 @@ inline fun <reified T : TangemBottomSheetConfigContent> BasicBottomSheet(
ModalBottomSheet(
// FIXME temporary solution to fix height of the bottom sheet
modifier = Modifier.sizeIn(maxHeight = LocalWindowSize.current.height - statusBarHeight),
modifier = modifier.heightIn(max = LocalWindowSize.current.height - statusBarHeight),
onDismissRequest = config.onDismissRequest,
sheetState = sheetState,
containerColor = containerColor,

View file

@ -1,12 +1,11 @@
package com.tangem.core.ui.components.fields
import androidx.compose.animation.AnimatedContent
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.text.BasicTextField
import androidx.compose.foundation.text.KeyboardActions
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.foundation.text.selection.LocalTextSelectionColors
import androidx.compose.foundation.text.selection.TextSelectionColors
import androidx.compose.material3.Text
import androidx.compose.runtime.*
import androidx.compose.ui.Modifier
@ -40,6 +39,7 @@ fun SimpleTextField(
textStyle: TextStyle = TangemTheme.typography.body2.copy(color = color),
placeholderColor: Color = TangemTheme.colors.text.disabled,
readOnly: Boolean = false,
interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
isValuePasted: Boolean = false,
onValuePastedTriggerDismiss: () -> Unit = {},
decorationBox: (@Composable (innerTextField: @Composable () -> Unit) -> Unit)? = null,
@ -54,10 +54,6 @@ fun SimpleTextField(
)
}
val focusRequester = remember { FocusRequester.Default }
val customTextSelectionColors = TextSelectionColors(
handleColor = TangemTheme.colors.text.accent,
backgroundColor = TangemTheme.colors.text.accent.copy(alpha = 0.3f),
)
val textFieldValue = textFieldValueState.copy(text = value)
var lastTextValue by remember(proxyValue, isValuePasted) {
textFieldValueState = textFieldValueState.copy(
@ -85,37 +81,36 @@ fun SimpleTextField(
}
}
CompositionLocalProvider(LocalTextSelectionColors provides customTextSelectionColors) {
BasicTextField(
value = textFieldValue,
onValueChange = { newTextFieldValueState ->
textFieldValueState = newTextFieldValueState
BasicTextField(
value = textFieldValue,
onValueChange = { newTextFieldValueState ->
textFieldValueState = newTextFieldValueState
val stringChangedSinceLastInvocation = lastTextValue != newTextFieldValueState.text
lastTextValue = newTextFieldValueState.text
val stringChangedSinceLastInvocation = lastTextValue != newTextFieldValueState.text
lastTextValue = newTextFieldValueState.text
if (stringChangedSinceLastInvocation) onValueChange(newTextFieldValueState.text)
},
textStyle = textStyle.copy(color = color),
cursorBrush = SolidColor(TangemTheme.colors.text.primary1),
singleLine = singleLine,
readOnly = readOnly,
visualTransformation = visualTransformation,
keyboardOptions = keyboardOptions,
keyboardActions = keyboardActions,
decorationBox = decorationBox ?: { textValue ->
SimpleTextPlaceholder(
placeholder = placeholder,
value = value,
textStyle = textStyle,
textValue = textValue,
color = placeholderColor,
)
},
modifier = modifier
.focusRequester(focusRequester),
)
}
if (stringChangedSinceLastInvocation) onValueChange(newTextFieldValueState.text)
},
textStyle = textStyle.copy(color = color),
cursorBrush = SolidColor(TangemTheme.colors.text.primary1),
singleLine = singleLine,
readOnly = readOnly,
visualTransformation = visualTransformation,
keyboardOptions = keyboardOptions,
keyboardActions = keyboardActions,
interactionSource = interactionSource,
decorationBox = decorationBox ?: { textValue ->
SimpleTextPlaceholder(
placeholder = placeholder,
value = value,
textStyle = textStyle,
textValue = textValue,
color = placeholderColor,
)
},
modifier = modifier
.focusRequester(focusRequester),
)
}
@Composable

View file

@ -52,11 +52,11 @@ fun TangemTheme(
LocalHapticManager provides hapticManager,
LocalSnackbarHostState provides snackbarHostState,
LocalWindowSize provides windowSize,
LocalTextSelectionColors provides TangemTextSelectionColors,
) {
CompositionLocalProvider(
LocalTangemShimmer provides TangemShimmer,
LocalMainBottomSheetColor provides remember { mutableStateOf(Color.Unspecified) },
LocalTextSelectionColors provides TangemTextSelectionColors,
) {
ProvideTextStyle(
value = TangemTheme.typography.body1,
@ -209,11 +209,13 @@ private fun darkThemeColors(): TangemColors {
)
}
@Stable
private val TangemTextSelectionColors = TextSelectionColors(
handleColor = TangemColorPalette.Azure,
backgroundColor = TangemColorPalette.Azure.copy(alpha = 0.4f),
)
private val TangemTextSelectionColors: TextSelectionColors
@Composable
@ReadOnlyComposable
get() = TextSelectionColors(
handleColor = TangemTheme.colors.text.accent,
backgroundColor = TangemTheme.colors.text.accent.copy(alpha = 0.3f),
)
private val LocalTangemColors = staticCompositionLocalOf<TangemColors> {
error("No TangemColors provided")

View file

@ -15,4 +15,7 @@ dependencies {
/* Project - Core */
implementation(projects.core.ui)
implementation(projects.core.decompose)
/* Compose */
implementation(deps.compose.runtime)
}

View file

@ -0,0 +1,18 @@
package com.tangem.features.managetokens.component
import androidx.compose.runtime.Composable
import com.tangem.domain.wallets.models.UserWalletId
interface AddCustomTokenComponent {
@Composable
fun BottomSheet(isVisible: Boolean, onDismiss: () -> Unit)
data class Params(
val userWalletId: UserWalletId,
)
interface Factory {
fun create(params: Params): AddCustomTokenComponent
}
}

View file

@ -21,6 +21,10 @@ dependencies {
implementation(projects.common.routing)
implementation(projects.core.featuretoggles)
/* Project - Domain */
implementation(projects.domain.wallets.models)
implementation(projects.domain.tokens.models)
/* AndroidX */
implementation(deps.androidx.activity.compose)
implementation(deps.lifecycle.compose)
@ -29,6 +33,7 @@ dependencies {
implementation(deps.compose.ui)
implementation(deps.compose.ui.tooling)
implementation(deps.compose.foundation)
implementation(deps.compose.material) // For button colors
implementation(deps.compose.material3)
implementation(deps.compose.shimmer)

View file

@ -0,0 +1,19 @@
package com.tangem.features.managetokens.component
import androidx.compose.foundation.lazy.LazyListScope
import com.tangem.domain.tokens.model.Network
import com.tangem.domain.wallets.models.UserWalletId
internal interface CustomTokenFormComponent {
fun content(scope: LazyListScope)
data class Params(
val userWalletId: UserWalletId,
val networkId: Network.ID,
)
interface Factory {
fun create(params: Params): CustomTokenFormComponent
}
}

View file

@ -0,0 +1,20 @@
package com.tangem.features.managetokens.component
import androidx.compose.foundation.lazy.LazyListScope
import com.tangem.domain.wallets.models.UserWalletId
import com.tangem.features.managetokens.entity.SelectedNetworkUM
internal interface CustomTokenNetworkSelectorComponent {
fun content(scope: LazyListScope)
data class Params(
val userWalletId: UserWalletId,
val selectedNetwork: SelectedNetworkUM?,
val onNetworkSelected: (SelectedNetworkUM) -> Unit,
)
interface Factory {
fun create(params: Params): CustomTokenNetworkSelectorComponent
}
}

View file

@ -0,0 +1,84 @@
package com.tangem.features.managetokens.component.preview
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.extensions.stringReference
import com.tangem.domain.wallets.models.UserWalletId
import com.tangem.features.managetokens.component.AddCustomTokenComponent
import com.tangem.features.managetokens.component.CustomTokenNetworkSelectorComponent
import com.tangem.features.managetokens.entity.AddCustomTokenButtonUM
import com.tangem.features.managetokens.entity.AddCustomTokenUM
import com.tangem.features.managetokens.entity.ClickableFieldUM
import com.tangem.features.managetokens.entity.SelectedNetworkUM
import com.tangem.features.managetokens.impl.R
import com.tangem.features.managetokens.ui.AddCustomTokenBottomSheet
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.update
internal class PreviewAddCustomTokenComponent(
initialState: AddCustomTokenUM = AddCustomTokenUM.NetworkSelector(popBack = {}),
) : AddCustomTokenComponent {
private val userWalletId = UserWalletId(stringValue = "321")
private val previewState: MutableStateFlow<AddCustomTokenUM> = MutableStateFlow(initialState)
@Composable
override fun BottomSheet(isVisible: Boolean, onDismiss: () -> Unit) {
val state by previewState.collectAsStateWithLifecycle()
val config = TangemBottomSheetConfig(
isShow = isVisible,
onDismissRequest = onDismiss,
content = state,
)
AddCustomTokenBottomSheet(
config = config,
content = {
when (val s = state) {
is AddCustomTokenUM.Form -> {
PreviewCustomTokenFormComponent(
networkName = ClickableFieldUM(
label = resourceReference(R.string.custom_token_network_input_title),
value = stringReference(s.selectedNetwork.name),
onClick = { showNetworkSelector(s.selectedNetwork) },
),
).content(this)
}
is AddCustomTokenUM.NetworkSelector -> {
PreviewCustomTokenNetworkSelectorComponent(
params = CustomTokenNetworkSelectorComponent.Params(
userWalletId = userWalletId,
selectedNetwork = s.selectedNetwork,
onNetworkSelected = ::showForm,
),
networksSize = 20,
).content(this)
}
}
},
)
}
private fun showNetworkSelector(selectedNetwork: SelectedNetworkUM) {
previewState.update {
AddCustomTokenUM.NetworkSelector(selectedNetwork, popBack = { showForm(selectedNetwork) })
}
}
private fun showForm(network: SelectedNetworkUM) {
previewState.update {
AddCustomTokenUM.Form(
popBack = {},
selectedNetwork = network,
addTokenButton = AddCustomTokenButtonUM.Visible(
isEnabled = false,
onClick = {},
),
)
}
}
}

View file

@ -0,0 +1,81 @@
package com.tangem.features.managetokens.component.preview
import androidx.compose.foundation.lazy.LazyListScope
import com.tangem.core.ui.components.notifications.NotificationConfig
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.extensions.stringReference
import com.tangem.features.managetokens.component.CustomTokenFormComponent
import com.tangem.features.managetokens.entity.ClickableFieldUM
import com.tangem.features.managetokens.entity.CustomTokenFormUM
import com.tangem.features.managetokens.entity.TextInputFieldUM
import com.tangem.features.managetokens.impl.R
import com.tangem.features.managetokens.ui.customTokenFormContent
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.persistentListOf
internal class PreviewCustomTokenFormComponent(
networkName: ClickableFieldUM = ClickableFieldUM(
label = resourceReference(R.string.custom_token_network_input_title),
value = stringReference(value = "Ethereum"),
onClick = {},
),
derivationPath: ClickableFieldUM = ClickableFieldUM(
label = resourceReference(R.string.custom_token_derivation_path),
value = stringReference(value = "Default"),
onClick = {},
),
canAddToken: Boolean = false,
contractAddress: TextInputFieldUM = TextInputFieldUM(
label = resourceReference(R.string.custom_token_contract_address_input_title),
placeholder = stringReference(value = "0x000000000000000000000000000"),
value = "",
onValueChange = {},
),
tokenName: TextInputFieldUM = TextInputFieldUM(
label = resourceReference(R.string.custom_token_name_input_title),
placeholder = stringReference(value = "E.g. USD Coin"),
value = "",
onValueChange = {},
),
tokenSymbol: TextInputFieldUM = TextInputFieldUM(
label = resourceReference(R.string.custom_token_token_symbol_input_title),
placeholder = stringReference(value = "E.g. USDC"),
value = "",
onValueChange = {},
),
tokenDecimals: TextInputFieldUM = TextInputFieldUM(
label = resourceReference(R.string.custom_token_decimals_input_title),
placeholder = stringReference(value = "8"),
value = "",
onValueChange = {},
),
notifications: ImmutableList<CustomTokenFormUM.NotificationUM> = persistentListOf(
CustomTokenFormUM.NotificationUM(
id = "1",
config = NotificationConfig(
title = stringReference(value = "Note that tokens can be created by anyone"),
subtitle = stringReference(value = "Be aware of adding scam tokens, they can cost nothing"),
iconResId = R.drawable.img_attention_20,
),
),
),
) : CustomTokenFormComponent {
private val previewState = CustomTokenFormUM(
networkName = networkName,
contractAddress = contractAddress,
tokenName = tokenName,
tokenSymbol = tokenSymbol,
tokenDecimals = tokenDecimals,
derivationPath = derivationPath,
notifications = notifications,
canAddToken = canAddToken,
onDerivationPathClick = {},
onNetworkClick = {},
onAddClick = {},
)
override fun content(scope: LazyListScope) {
scope.customTokenFormContent(model = previewState)
}
}

View file

@ -0,0 +1,50 @@
package com.tangem.features.managetokens.component.preview
import androidx.compose.foundation.lazy.LazyListScope
import com.tangem.domain.tokens.model.Network
import com.tangem.domain.wallets.models.UserWalletId
import com.tangem.features.managetokens.component.CustomTokenNetworkSelectorComponent
import com.tangem.features.managetokens.entity.CurrencyNetworkUM
import com.tangem.features.managetokens.entity.CustomTokenNetworkSelectorUM
import com.tangem.features.managetokens.entity.SelectedNetworkUM
import com.tangem.features.managetokens.impl.R
import com.tangem.features.managetokens.ui.customTokenNetworkSelectorContent
import kotlinx.collections.immutable.toImmutableList
internal class PreviewCustomTokenNetworkSelectorComponent(
private val params: CustomTokenNetworkSelectorComponent.Params = CustomTokenNetworkSelectorComponent.Params(
userWalletId = UserWalletId(stringValue = "321"),
selectedNetwork = null,
onNetworkSelected = {},
),
networksSize: Int = 5,
) : CustomTokenNetworkSelectorComponent {
private val previewNetworks = List(size = networksSize) { networkIndex ->
val n = SelectedNetworkUM(
id = Network.ID(networkIndex.toString()),
name = "Network $networkIndex",
)
CurrencyNetworkUM(
id = n.id,
name = n.name,
type = "N$networkIndex",
iconResId = R.drawable.ic_eth_16,
isMainNetwork = false,
isSelected = n.id == params.selectedNetwork?.id,
onSelectedStateChange = { params.onNetworkSelected(n) },
)
}.toImmutableList()
private val previewState = CustomTokenNetworkSelectorUM(
showTitle = params.selectedNetwork == null,
networks = previewNetworks,
)
override fun content(scope: LazyListScope) {
scope.customTokenNetworkSelectorContent(
model = previewState,
)
}
}

View file

@ -8,9 +8,9 @@ import androidx.compose.ui.graphics.Color
import androidx.compose.ui.util.fastForEachIndexed
import com.tangem.core.ui.components.currency.icon.CurrencyIconState
import com.tangem.core.ui.components.fields.entity.SearchBarUM
import com.tangem.core.ui.components.rows.model.BlockchainRowUM
import com.tangem.core.ui.components.rows.model.ChainRowUM
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.domain.tokens.model.Network
import com.tangem.features.managetokens.component.ManageTokensComponent
import com.tangem.features.managetokens.entity.CurrencyItemUM
import com.tangem.features.managetokens.entity.CurrencyNetworkUM
@ -129,14 +129,11 @@ internal class PreviewManageTokensComponent : ManageTokensComponent {
private fun getCurrencyNetworks(currencyIndex: Int) = List(size = 3) { networkIndex ->
CurrencyNetworkUM(
id = networkIndex.toString(),
model = BlockchainRowUM(
name = "NETWORK$networkIndex",
type = "N$networkIndex",
iconResId = R.drawable.ic_eth_16,
isMainNetwork = networkIndex == 0,
isSelected = false,
),
id = Network.ID(networkIndex.toString()),
name = "NETWORK$networkIndex",
type = "N$networkIndex",
iconResId = R.drawable.ic_eth_16,
isMainNetwork = networkIndex == 0,
isSelected = false,
onSelectedStateChange = { toggleNetwork(currencyIndex, networkIndex, isSelected = it) },
)
@ -171,14 +168,11 @@ internal class PreviewManageTokensComponent : ManageTokensComponent {
it.fastForEachIndexed { index, network ->
if (index == networkIndex) {
it[index] = network.copy(
model = network.model.copy(
iconResId = if (isSelected) {
R.drawable.img_eth_22
} else {
R.drawable.ic_eth_16
},
isSelected = isSelected,
),
iconResId = if (isSelected) {
R.drawable.img_eth_22
} else {
R.drawable.ic_eth_16
},
isSelected = isSelected,
)
}

View file

@ -0,0 +1,54 @@
package com.tangem.features.managetokens.entity
import androidx.compose.runtime.Immutable
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfigContent
import com.tangem.domain.tokens.model.Network
@Immutable
internal sealed class AddCustomTokenUM : TangemBottomSheetConfigContent {
abstract val selectedNetwork: SelectedNetworkUM?
abstract val addTokenButton: AddCustomTokenButtonUM
abstract val popBack: () -> Unit
data class NetworkSelector(
override val selectedNetwork: SelectedNetworkUM? = null,
override val popBack: () -> Unit,
) : AddCustomTokenUM() {
override val addTokenButton: AddCustomTokenButtonUM = AddCustomTokenButtonUM.Hidden
}
data class Form(
override val selectedNetwork: SelectedNetworkUM,
override val addTokenButton: AddCustomTokenButtonUM.Visible,
override val popBack: () -> Unit,
) : AddCustomTokenUM()
}
@Immutable
internal data class SelectedNetworkUM(
val id: Network.ID,
val name: String,
)
@Immutable
internal sealed class AddCustomTokenButtonUM {
open val onClick: () -> Unit = {}
open val isEnabled: Boolean = false
val isVisible: Boolean
get() = this is Visible
data object Hidden : AddCustomTokenButtonUM() {
override val onClick: () -> Unit = {}
}
data class Visible(
override val isEnabled: Boolean,
override val onClick: () -> Unit,
) : AddCustomTokenButtonUM()
}

View file

@ -1,12 +1,15 @@
package com.tangem.features.managetokens.entity
import androidx.compose.runtime.Immutable
import com.tangem.core.ui.components.rows.model.BlockchainRowUM
import com.tangem.domain.tokens.model.Network
@Immutable
internal data class CurrencyNetworkUM(
val id: String,
val model: BlockchainRowUM,
val id: Network.ID,
val name: String,
val type: String,
val iconResId: Int,
val isMainNetwork: Boolean,
val isSelected: Boolean,
val onSelectedStateChange: (Boolean) -> Unit,
)

View file

@ -0,0 +1,44 @@
package com.tangem.features.managetokens.entity
import androidx.compose.runtime.Immutable
import com.tangem.core.ui.components.notifications.NotificationConfig
import com.tangem.core.ui.extensions.TextReference
import kotlinx.collections.immutable.ImmutableList
@Immutable
internal data class CustomTokenFormUM(
val networkName: ClickableFieldUM,
val contractAddress: TextInputFieldUM,
val tokenName: TextInputFieldUM,
val tokenSymbol: TextInputFieldUM,
val tokenDecimals: TextInputFieldUM,
val derivationPath: ClickableFieldUM,
val notifications: ImmutableList<NotificationUM>,
val canAddToken: Boolean,
val onNetworkClick: () -> Unit,
val onDerivationPathClick: () -> Unit,
val onAddClick: () -> Unit,
) {
@Immutable
data class NotificationUM(
val id: String,
val config: NotificationConfig,
)
}
@Immutable
internal data class TextInputFieldUM(
val label: TextReference,
val placeholder: TextReference,
val value: String,
val onValueChange: (String) -> Unit,
val error: TextReference? = null,
)
@Immutable
internal data class ClickableFieldUM(
val label: TextReference,
val value: TextReference,
val onClick: () -> Unit,
)

View file

@ -0,0 +1,10 @@
package com.tangem.features.managetokens.entity
import androidx.compose.runtime.Immutable
import kotlinx.collections.immutable.ImmutableList
@Immutable
internal data class CustomTokenNetworkSelectorUM(
val showTitle: Boolean,
val networks: ImmutableList<CurrencyNetworkUM>,
)

View file

@ -7,9 +7,9 @@ import com.tangem.core.decompose.model.Model
import com.tangem.core.decompose.navigation.Router
import com.tangem.core.ui.components.currency.icon.CurrencyIconState
import com.tangem.core.ui.components.fields.entity.SearchBarUM
import com.tangem.core.ui.components.rows.model.BlockchainRowUM
import com.tangem.core.ui.components.rows.model.ChainRowUM
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.domain.tokens.model.Network
import com.tangem.features.managetokens.entity.CurrencyItemUM
import com.tangem.features.managetokens.entity.CurrencyNetworkUM
import com.tangem.features.managetokens.entity.ManageTokensUM
@ -109,14 +109,11 @@ internal class ManageTokensModel @Inject constructor(
private fun getCurrencyNetworks(currencyIndex: Int) = List(size = 3) { networkIndex ->
CurrencyNetworkUM(
id = networkIndex.toString(),
model = BlockchainRowUM(
name = "NETWORK$networkIndex",
type = "N$networkIndex",
iconResId = R.drawable.ic_eth_16,
isMainNetwork = networkIndex == 0,
isSelected = false,
),
id = Network.ID(networkIndex.toString()),
name = "NETWORK$networkIndex",
type = "N$networkIndex",
iconResId = R.drawable.ic_eth_16,
isMainNetwork = networkIndex == 0,
isSelected = false,
onSelectedStateChange = { toggleNetwork(currencyIndex, networkIndex, isSelected = it) },
)
@ -151,14 +148,11 @@ internal class ManageTokensModel @Inject constructor(
it.fastForEachIndexed { index, network ->
if (index == networkIndex) {
it[index] = network.copy(
model = network.model.copy(
iconResId = if (isSelected) {
R.drawable.img_eth_22
} else {
R.drawable.ic_eth_16
},
isSelected = isSelected,
),
iconResId = if (isSelected) {
R.drawable.img_eth_22
} else {
R.drawable.ic_eth_16
},
isSelected = isSelected,
)
}

View file

@ -0,0 +1,188 @@
package com.tangem.features.managetokens.ui
import android.content.res.Configuration
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.LazyListScope
import androidx.compose.material3.FabPosition
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.layout.onSizeChanged
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
import androidx.compose.ui.unit.dp
import com.tangem.core.ui.components.PrimaryButton
import com.tangem.core.ui.components.appbar.TangemTopAppBar
import com.tangem.core.ui.components.appbar.TangemTopAppBarHeight
import com.tangem.core.ui.components.appbar.models.TopAppBarButtonUM
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheet
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetTitle
import com.tangem.core.ui.components.isOpened
import com.tangem.core.ui.components.keyboardAsState
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreview
import com.tangem.domain.tokens.model.Network
import com.tangem.features.managetokens.component.AddCustomTokenComponent
import com.tangem.features.managetokens.component.preview.PreviewAddCustomTokenComponent
import com.tangem.features.managetokens.entity.AddCustomTokenButtonUM
import com.tangem.features.managetokens.entity.AddCustomTokenUM
import com.tangem.features.managetokens.entity.SelectedNetworkUM
import com.tangem.features.managetokens.impl.R
@Composable
internal fun AddCustomTokenBottomSheet(config: TangemBottomSheetConfig, content: LazyListScope.() -> Unit) {
TangemBottomSheet<AddCustomTokenUM>(
config = config,
title = { model ->
Title(model)
},
containerColor = TangemTheme.colors.background.secondary,
content = { model ->
Content(
model = model,
content = content,
)
},
)
}
@Composable
private fun Title(model: AddCustomTokenUM, modifier: Modifier = Modifier) {
val showTokenNetworkTitle = model is AddCustomTokenUM.NetworkSelector && model.selectedNetwork != null
if (showTokenNetworkTitle) {
TangemTopAppBar(
modifier = modifier,
title = resourceReference(R.string.custom_token_network_selector_title),
titleAlignment = Alignment.CenterHorizontally,
startButton = TopAppBarButtonUM.Back(model.popBack),
height = TangemTopAppBarHeight.BOTTOM_SHEET,
)
} else {
TangemBottomSheetTitle(
modifier = modifier,
title = resourceReference(R.string.add_custom_token_title),
)
}
}
@Composable
private fun Content(model: AddCustomTokenUM, content: LazyListScope.() -> Unit, modifier: Modifier = Modifier) {
val density = LocalDensity.current
val keyboardState by keyboardAsState()
var fabHeight by remember { mutableStateOf(0.dp) }
Scaffold(
modifier = modifier.imePadding(),
containerColor = TangemTheme.colors.background.secondary,
floatingActionButtonPosition = FabPosition.Center,
floatingActionButton = {
AnimatedVisibility(
modifier = Modifier.onSizeChanged {
fabHeight = with(density) { it.height.toDp() }
},
visible = model.addTokenButton.isVisible && !keyboardState.isOpened,
enter = fadeIn(),
exit = fadeOut(),
label = "Add button visibility",
) {
PrimaryButton(
modifier = Modifier
.padding(bottom = TangemTheme.dimens.spacing16)
.padding(horizontal = TangemTheme.dimens.spacing16)
.fillMaxWidth(),
text = stringResource(id = R.string.custom_token_add_token),
enabled = model.addTokenButton.isEnabled,
onClick = model.addTokenButton.onClick,
)
}
},
) { paddingValues ->
LazyColumn(
modifier = Modifier.padding(paddingValues),
contentPadding = PaddingValues(
start = TangemTheme.dimens.spacing16,
end = TangemTheme.dimens.spacing16,
bottom = TangemTheme.dimens.spacing32 + fabHeight,
),
) {
item {
if (model is AddCustomTokenUM.NetworkSelector && model.selectedNetwork != null) {
Spacer(modifier = Modifier.size(TangemTheme.dimens.spacing12))
} else {
Box(
modifier = Modifier
.fillMaxWidth()
.padding(bottom = TangemTheme.dimens.spacing16),
contentAlignment = Alignment.Center,
) {
Text(
modifier = Modifier.fillMaxWidth(fraction = 0.7f),
text = stringResource(id = R.string.custom_token_subtitle),
style = TangemTheme.typography.caption2,
color = TangemTheme.colors.text.secondary,
textAlign = TextAlign.Center,
)
}
}
}
content()
}
}
}
// region Preview
@Composable
@Preview(showBackground = true)
@Preview(showBackground = true, uiMode = Configuration.UI_MODE_NIGHT_YES)
private fun Preview_AddCustomTokenBottomSheet(
@PreviewParameter(AddCustomTokenComponentPreviewProvider::class) component: AddCustomTokenComponent,
) {
TangemThemePreview {
component.BottomSheet(isVisible = true, onDismiss = {})
}
}
private class AddCustomTokenComponentPreviewProvider : PreviewParameterProvider<AddCustomTokenComponent> {
override val values: Sequence<AddCustomTokenComponent>
get() = sequenceOf(
PreviewAddCustomTokenComponent(),
PreviewAddCustomTokenComponent(
initialState = AddCustomTokenUM.NetworkSelector(
popBack = {},
selectedNetwork = SelectedNetworkUM(
id = Network.ID(value = "0"),
name = "Ethereum",
),
),
),
PreviewAddCustomTokenComponent(
initialState = AddCustomTokenUM.Form(
popBack = {},
selectedNetwork = SelectedNetworkUM(
id = Network.ID(value = "1"),
name = "Ethereum",
),
addTokenButton = AddCustomTokenButtonUM.Visible(
isEnabled = false,
onClick = {},
),
),
),
)
}
// endregion Preview

View file

@ -0,0 +1,196 @@
package com.tangem.features.managetokens.ui
import android.content.res.Configuration
import androidx.compose.animation.animateColorAsState
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.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.LazyListScope
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.text.KeyboardActions
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.text.input.ImeAction
import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
import com.tangem.core.ui.components.block.information.InformationBlock
import com.tangem.core.ui.components.fields.SimpleTextField
import com.tangem.core.ui.components.notifications.Notification
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.features.managetokens.component.preview.PreviewCustomTokenFormComponent
import com.tangem.features.managetokens.entity.ClickableFieldUM
import com.tangem.features.managetokens.entity.CustomTokenFormUM
import com.tangem.features.managetokens.entity.TextInputFieldUM
internal fun LazyListScope.customTokenFormContent(model: CustomTokenFormUM) {
item {
ClickableField(
modifier = Modifier.padding(bottom = TangemTheme.dimens.spacing12),
model = model.networkName,
)
}
item {
Column(
modifier = Modifier
.padding(bottom = TangemTheme.dimens.spacing12)
.background(
color = TangemTheme.colors.background.action,
shape = TangemTheme.shapes.roundedCornersXMedium,
),
) {
TextField(
model = model.contractAddress,
keyboardOptions = KeyboardOptions.Default.copy(
imeAction = ImeAction.Next,
),
)
TextField(
model = model.tokenName,
keyboardOptions = KeyboardOptions.Default.copy(
imeAction = ImeAction.Next,
),
)
TextField(
model = model.tokenSymbol,
keyboardOptions = KeyboardOptions.Default.copy(
imeAction = ImeAction.Next,
),
)
TextField(
model = model.tokenDecimals,
keyboardOptions = KeyboardOptions.Default.copy(
keyboardType = KeyboardType.Decimal,
imeAction = ImeAction.Next,
),
)
}
}
item {
ClickableField(
modifier = Modifier.padding(bottom = TangemTheme.dimens.spacing12),
model = model.derivationPath,
)
}
items(
items = model.notifications,
key = { it.id },
) { notification ->
Notification(
modifier = Modifier.padding(bottom = TangemTheme.dimens.spacing12),
config = notification.config,
containerColor = TangemTheme.colors.button.disabled,
)
}
}
@Composable
private fun TextField(
model: TextInputFieldUM,
modifier: Modifier = Modifier,
keyboardOptions: KeyboardOptions = KeyboardOptions.Default,
keyboardActions: KeyboardActions = KeyboardActions.Default,
) {
InformationBlock(
modifier = modifier,
title = {
val color by animateColorAsState(
targetValue = if (model.error != null) {
TangemTheme.colors.text.warning
} else {
TangemTheme.colors.text.tertiary
},
label = "Field label color",
)
Text(
text = (model.error ?: model.label).resolveReference(),
style = TangemTheme.typography.subtitle2,
color = color,
)
},
content = {
SimpleTextField(
modifier = Modifier.padding(bottom = TangemTheme.dimens.spacing12),
value = model.value,
onValueChange = model.onValueChange,
readOnly = false,
placeholder = model.placeholder,
singleLine = true,
keyboardOptions = keyboardOptions,
keyboardActions = keyboardActions,
)
},
)
}
@Composable
private fun ClickableField(model: ClickableFieldUM, modifier: Modifier = Modifier) {
InformationBlock(
modifier = modifier
.clip(TangemTheme.shapes.roundedCornersXMedium)
.clickable(onClick = model.onClick),
title = {
Text(
text = model.label.resolveReference(),
style = TangemTheme.typography.subtitle2,
color = TangemTheme.colors.text.tertiary,
)
},
content = {
Text(
modifier = Modifier.padding(bottom = TangemTheme.dimens.spacing12),
text = model.value.resolveReference(),
style = TangemTheme.typography.body2,
color = TangemTheme.colors.text.primary1,
)
},
)
}
// region Preview
@Composable
@Preview(showBackground = true, widthDp = 360)
@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES)
private fun Preview_CustomTokenFormContent(
@PreviewParameter(PreviewCustomTokenFormComponentProvider::class)
component: PreviewCustomTokenFormComponent,
) {
TangemThemePreview {
LazyColumn(
modifier = Modifier.background(color = TangemTheme.colors.background.secondary),
) { component.content(scope = this) }
}
}
private class PreviewCustomTokenFormComponentProvider :
PreviewParameterProvider<PreviewCustomTokenFormComponent> {
override val values: Sequence<PreviewCustomTokenFormComponent>
get() = sequenceOf(
PreviewCustomTokenFormComponent(),
PreviewCustomTokenFormComponent(
contractAddress = TextInputFieldUM(
label = stringReference("Contract address"),
value = "0x1234567890",
error = stringReference("Contract address is invalid"),
placeholder = stringReference("0x1234567890"),
onValueChange = {},
),
),
)
}
// endregion Preview

View file

@ -0,0 +1,158 @@
package com.tangem.features.managetokens.ui
import android.content.res.Configuration
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.LazyListScope
import androidx.compose.foundation.lazy.itemsIndexed
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.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.RectangleShape
import androidx.compose.ui.res.painterResource
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.PreviewParameterProvider
import com.tangem.core.ui.components.currency.icon.CurrencyIconState
import com.tangem.core.ui.components.rows.ChainRow
import com.tangem.core.ui.components.rows.model.ChainRowUM
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreview
import com.tangem.domain.tokens.model.Network
import com.tangem.domain.wallets.models.UserWalletId
import com.tangem.features.managetokens.component.CustomTokenNetworkSelectorComponent
import com.tangem.features.managetokens.component.preview.PreviewCustomTokenNetworkSelectorComponent
import com.tangem.features.managetokens.entity.CurrencyNetworkUM
import com.tangem.features.managetokens.entity.CustomTokenNetworkSelectorUM
import com.tangem.features.managetokens.entity.SelectedNetworkUM
import com.tangem.features.managetokens.impl.R
internal fun LazyListScope.customTokenNetworkSelectorContent(model: CustomTokenNetworkSelectorUM) {
val lastIndex = model.networks.lastIndex
if (model.showTitle) {
item {
Box(
modifier = Modifier
.fillMaxWidth()
.heightIn(min = TangemTheme.dimens.size36)
.background(
color = TangemTheme.colors.background.primary,
shape = TangemTheme.shapes.bottomSheet,
),
) {
Text(
modifier = Modifier
.padding(
top = TangemTheme.dimens.spacing12,
bottom = TangemTheme.dimens.spacing6,
)
.padding(horizontal = TangemTheme.dimens.spacing12),
text = stringResource(R.string.add_custom_token_choose_network),
style = TangemTheme.typography.subtitle2,
color = TangemTheme.colors.text.tertiary,
)
}
}
}
itemsIndexed(
items = model.networks,
key = { _, item -> item.id.value },
) { index, item ->
NetworkItem(
modifier = Modifier
.fillMaxWidth()
.clip(
shape = when {
!model.showTitle && index == 0 -> RoundedCornerShape(
topStart = TangemTheme.dimens.radius16,
topEnd = TangemTheme.dimens.radius16,
)
index == lastIndex -> RoundedCornerShape(
bottomStart = TangemTheme.dimens.radius16,
bottomEnd = TangemTheme.dimens.radius16,
)
else -> RectangleShape
},
)
.background(color = TangemTheme.colors.background.primary)
.clickable(onClick = { item.onSelectedStateChange(true) })
.padding(horizontal = TangemTheme.dimens.spacing4),
model = item,
)
}
}
@Composable
private fun NetworkItem(model: CurrencyNetworkUM, modifier: Modifier = Modifier) {
ChainRow(
modifier = modifier,
model = with(model) {
ChainRowUM(
name = name,
type = type,
icon = CurrencyIconState.CoinIcon(
url = null,
fallbackResId = model.iconResId,
isGrayscale = false,
showCustomBadge = false,
),
showCustom = false,
)
},
action = {
AnimatedVisibility(
modifier = Modifier.size(TangemTheme.dimens.size24),
visible = model.isSelected,
) {
Icon(
painter = painterResource(id = R.drawable.ic_check_24),
tint = TangemTheme.colors.icon.accent,
contentDescription = null,
)
}
},
)
}
// region Preview
@Composable
@Preview(showBackground = true, widthDp = 360)
@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES)
private fun Preview_CustomTokenNetworkSelectorContent(
@PreviewParameter(CustomTokenNetworkSelectorComponentPreviewProvider::class)
component: CustomTokenNetworkSelectorComponent,
) {
TangemThemePreview {
LazyColumn {
component.content(this)
}
}
}
private class CustomTokenNetworkSelectorComponentPreviewProvider :
PreviewParameterProvider<CustomTokenNetworkSelectorComponent> {
override val values: Sequence<CustomTokenNetworkSelectorComponent>
get() = sequenceOf(
PreviewCustomTokenNetworkSelectorComponent(),
PreviewCustomTokenNetworkSelectorComponent(
params = CustomTokenNetworkSelectorComponent.Params(
userWalletId = UserWalletId(stringValue = "321"),
selectedNetwork = SelectedNetworkUM(
id = Network.ID(value = "0"),
name = "",
),
onNetworkSelected = {},
),
),
)
}
// endregion Preview

View file

@ -35,6 +35,7 @@ import com.tangem.core.ui.components.fields.entity.SearchBarUM
import com.tangem.core.ui.components.rows.ArrowRow
import com.tangem.core.ui.components.rows.BlockchainRow
import com.tangem.core.ui.components.rows.ChainRow
import com.tangem.core.ui.components.rows.model.BlockchainRowUM
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreview
@ -245,7 +246,15 @@ private fun NetworksList(networks: NetworksUM, currencyId: String, modifier: Mod
isLastItem = index == currentItems.lastIndex,
content = {
BlockchainRow(
model = network.model,
model = with(network) {
BlockchainRowUM(
name = name,
type = type,
iconResId = iconResId,
isMainNetwork = isMainNetwork,
isSelected = isSelected,
)
},
action = {
TangemSwitch(
checked = network.isSelected,

View file

@ -155,6 +155,7 @@ androidx-datastore = { module = "androidx.datastore:datastore-preferences", vers
# region AndroidX
# region Compose
compose-runtime = { module = "androidx.compose.runtime:runtime", version.ref = "compose-runtime" }
compose-ui = { module = "androidx.compose.ui:ui", version.ref = "compose-runtime" }
compose-ui-tooling = { module = "androidx.compose.ui:ui-tooling", version.ref = "compose-runtime" }
compose-ui-utils = { module = "androidx.compose.ui:ui-util", version.ref = "compose-runtime" }

View file

@ -25,6 +25,7 @@ internal fun BaseExtension.configureCompose(project: Project) {
contains(Regex(pattern = ":presentation\$")) ||
contains(Regex(pattern = ":app\$")) || // TODO: [REDACTED_JIRA]
contains(Regex(pattern = ":features:markets:api\$")) || // provides Composable function
contains(Regex(pattern = ":features:manage-tokens:api\$")) || // provides Composable function
contains(Regex(pattern = ":impl\$"))
}