Updated on 2026-08-14

This commit is contained in:
Tangem 2023-04-25 16:40:06 +08:00
parent 4690bf2e6c
commit 3718b9e42f
12 changed files with 876 additions and 42 deletions

View file

@ -3,6 +3,7 @@ package com.tangem.tap.features.customtoken.impl.presentation.models
import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.ui.text.input.ImeAction import androidx.compose.ui.text.input.ImeAction
import androidx.compose.ui.text.input.KeyboardType import androidx.compose.ui.text.input.KeyboardType
import com.tangem.blockchain.common.Blockchain
import com.tangem.tap.features.details.ui.cardsettings.TextReference import com.tangem.tap.features.details.ui.cardsettings.TextReference
import com.tangem.wallet.R import com.tangem.wallet.R
@ -243,20 +244,29 @@ internal sealed interface AddCustomTokenSelectorField {
/** Title */ /** Title */
val title: TextReference val title: TextReference
/** Blockchain */
val blockchain: Blockchain
/** /**
* Menu item with title * Menu item with title
* *
* @property title title text * @property title title text
* @property blockchain blockchain
*/ */
data class Title(override val title: TextReference) : SelectorItem data class Title(override val title: TextReference, override val blockchain: Blockchain) : SelectorItem
/** /**
* Menu item with title ans subtitle * Menu item with title ans subtitle
* *
* @property title title text * @property title title text
* @property subtitle subtitle text * @property subtitle subtitle text
* @property blockchain blockchain
*/ */
data class TitleWithSubtitle(override val title: TextReference, val subtitle: TextReference) : SelectorItem data class TitleWithSubtitle(
override val title: TextReference,
val subtitle: TextReference,
override val blockchain: Blockchain,
) : SelectorItem
} }
} }

View file

@ -0,0 +1,121 @@
package com.tangem.tap.features.customtoken.impl.presentation.ui
import androidx.activity.compose.BackHandler
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.FabPosition
import androidx.compose.material.Scaffold
import androidx.compose.runtime.Composable
import androidx.compose.runtime.key
import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.Preview
import com.tangem.blockchain.common.Blockchain
import com.tangem.core.ui.res.TangemTheme
import com.tangem.tap.features.customtoken.impl.presentation.models.AddCustomTokenFloatingButton
import com.tangem.tap.features.customtoken.impl.presentation.models.AddCustomTokenInputField
import com.tangem.tap.features.customtoken.impl.presentation.models.AddCustomTokenSelectorField
import com.tangem.tap.features.customtoken.impl.presentation.models.AddCustomTokensToolbar
import com.tangem.tap.features.customtoken.impl.presentation.states.AddCustomTokenStateHolder
import com.tangem.tap.features.customtoken.impl.presentation.ui.components.AddCustomTokenFloatingButton
import com.tangem.tap.features.customtoken.impl.presentation.ui.components.AddCustomTokenForm
import com.tangem.tap.features.customtoken.impl.presentation.ui.components.AddCustomTokenToolbar
import com.tangem.tap.features.customtoken.impl.presentation.ui.components.AddCustomTokenWarning
import com.tangem.tap.features.details.ui.cardsettings.TextReference
import com.tangem.wallet.R
/**
* Add custom token content
*
* @param state screen state
*
[REDACTED_AUTHOR]
*/
@Composable
internal fun AddCustomTokenContent(state: AddCustomTokenStateHolder.Content) {
BackHandler(onBack = state.onBackButtonClick)
Scaffold(
topBar = {
AddCustomTokenToolbar(
title = state.toolbar.title,
onBackButtonClick = state.toolbar.onBackButtonClick,
)
},
floatingActionButton = { AddCustomTokenFloatingButton(model = state.floatingButton) },
floatingActionButtonPosition = FabPosition.Center,
) {
Column(
modifier = Modifier
.padding(paddingValues = it)
.fillMaxSize()
.verticalScroll(rememberScrollState()),
) {
AddCustomTokenForm(model = state.form)
state.warnings.forEach { description ->
key(description) {
AddCustomTokenWarning(description)
}
}
}
}
}
@Preview(showSystemUi = true)
@Composable
private fun Preview_AddCustomTokenContent() {
TangemTheme {
AddCustomTokenContent(
state = AddCustomTokenStateHolder.Content(
onBackButtonClick = {},
toolbar = AddCustomTokensToolbar(
title = TextReference.Res(R.string.add_custom_token_title),
onBackButtonClick = {},
),
form = com.tangem.tap.features.customtoken.impl.presentation.models.AddCustomTokenForm(
contractAddressInputField = AddCustomTokenInputField.ContactAddress(
value = "",
onValueChange = {},
isError = false,
isLoading = false,
),
networkSelectorField = AddCustomTokenSelectorField.Network(
selectedItem = AddCustomTokenSelectorField.SelectorItem.Title(
title = TextReference.Str("Avalanche"),
blockchain = Blockchain.Avalanche,
),
items = listOf(),
onMenuItemClick = {},
),
tokenNameInputField = AddCustomTokenInputField.TokenName(
value = "",
onValueChange = {},
isEnabled = false,
isError = false,
),
tokenSymbolInputField = AddCustomTokenInputField.TokenSymbol(
value = "",
onValueChange = {},
isEnabled = false,
isError = false,
),
decimalsInputField = AddCustomTokenInputField.Decimals(
value = "",
onValueChange = {},
isEnabled = false,
isError = false,
),
derivationPathSelectorField = null,
),
warnings = listOf(),
floatingButton = AddCustomTokenFloatingButton(
isEnabled = false,
onClick = {},
),
),
)
}
}

View file

@ -0,0 +1,20 @@
package com.tangem.tap.features.customtoken.impl.presentation.ui
import androidx.compose.runtime.Composable
import com.tangem.tap.features.customtoken.impl.presentation.states.AddCustomTokenStateHolder
/**
* Add custom token screen
*
* @param stateHolder state holder
*
[REDACTED_AUTHOR]
*/
@Suppress("UnusedPrivateMember")
@Composable
internal fun AddCustomTokenScreen(stateHolder: AddCustomTokenStateHolder) {
when (stateHolder) {
is AddCustomTokenStateHolder.Content -> AddCustomTokenContent(state = stateHolder)
is AddCustomTokenStateHolder.TestContent -> AddCustomTokenTestContent(state = stateHolder)
}
}

View file

@ -0,0 +1,311 @@
package com.tangem.tap.features.customtoken.impl.presentation.ui
import androidx.activity.compose.BackHandler
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.BottomSheetScaffold
import androidx.compose.material.BottomSheetScaffoldState
import androidx.compose.material.BottomSheetState
import androidx.compose.material.BottomSheetValue
import androidx.compose.material.Divider
import androidx.compose.material.ExperimentalMaterialApi
import androidx.compose.material.FabPosition
import androidx.compose.material.Text
import androidx.compose.material.rememberBottomSheetScaffoldState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.key
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Alignment
import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalConfiguration
import androidx.compose.ui.platform.LocalSoftwareKeyboardController
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import com.tangem.blockchain.common.Blockchain
import com.tangem.core.ui.components.PrimaryButton
import com.tangem.core.ui.components.SpacerH8
import com.tangem.core.ui.components.atoms.Hand
import com.tangem.core.ui.res.TangemTheme
import com.tangem.tap.features.customtoken.impl.presentation.models.AddCustomTokenChooseTokenBottomSheet
import com.tangem.tap.features.customtoken.impl.presentation.models.AddCustomTokenChooseTokenBottomSheet.TestTokenItem
import com.tangem.tap.features.customtoken.impl.presentation.models.AddCustomTokenFloatingButton
import com.tangem.tap.features.customtoken.impl.presentation.models.AddCustomTokenInputField
import com.tangem.tap.features.customtoken.impl.presentation.models.AddCustomTokenSelectorField
import com.tangem.tap.features.customtoken.impl.presentation.models.AddCustomTokenTestBlock
import com.tangem.tap.features.customtoken.impl.presentation.models.AddCustomTokensToolbar
import com.tangem.tap.features.customtoken.impl.presentation.states.AddCustomTokenStateHolder
import com.tangem.tap.features.customtoken.impl.presentation.ui.components.AddCustomTokenFloatingButton
import com.tangem.tap.features.customtoken.impl.presentation.ui.components.AddCustomTokenForm
import com.tangem.tap.features.customtoken.impl.presentation.ui.components.AddCustomTokenToolbar
import com.tangem.tap.features.details.ui.cardsettings.TextReference
import com.tangem.wallet.R
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
/**
* Add custom token content for testing
*
* @param state screen state
*
[REDACTED_AUTHOR]
*/
@OptIn(ExperimentalMaterialApi::class)
@Composable
internal fun AddCustomTokenTestContent(state: AddCustomTokenStateHolder.TestContent) {
val coroutineScope = rememberCoroutineScope()
val bottomSheetScaffoldState = rememberBottomSheetScaffoldState(
bottomSheetState = BottomSheetState(initialValue = BottomSheetValue.Collapsed),
)
BackHandler(
onBack = {
onBackButtonClicked(
coroutineScope = coroutineScope,
bottomSheetScaffoldState = bottomSheetScaffoldState,
defaultAction = state.onBackButtonClick,
)
},
)
BottomSheetScaffold(
sheetContent = {
SheetContent(
coroutineScope = coroutineScope,
bottomSheetScaffoldState = bottomSheetScaffoldState,
model = state.bottomSheet,
)
},
scaffoldState = bottomSheetScaffoldState,
topBar = {
AddCustomTokenToolbar(
title = state.toolbar.title,
onBackButtonClick = {
onBackButtonClicked(
coroutineScope,
bottomSheetScaffoldState,
defaultAction = state.toolbar.onBackButtonClick,
)
},
)
},
floatingActionButton = { AddCustomTokenFloatingButton(model = state.floatingButton) },
floatingActionButtonPosition = FabPosition.Center,
sheetBackgroundColor = TangemTheme.colors.background.secondary,
sheetPeekHeight = TangemTheme.dimens.size0,
backgroundColor = TangemTheme.colors.background.secondary,
) {
Column(
modifier = Modifier
.verticalScroll(rememberScrollState())
.padding(it),
) {
TestBlock(
model = state.testBlock,
coroutineScope,
bottomSheetScaffoldState,
)
AddCustomTokenForm(model = state.form)
}
}
}
@OptIn(ExperimentalMaterialApi::class)
private fun onBackButtonClicked(
coroutineScope: CoroutineScope,
bottomSheetScaffoldState: BottomSheetScaffoldState,
defaultAction: () -> Unit,
) {
coroutineScope.launch {
if (bottomSheetScaffoldState.bottomSheetState.isExpanded) {
bottomSheetScaffoldState.bottomSheetState.collapse()
} else {
defaultAction()
}
}
}
@OptIn(ExperimentalMaterialApi::class)
@Composable
private fun SheetContent(
model: AddCustomTokenChooseTokenBottomSheet,
coroutineScope: CoroutineScope,
bottomSheetScaffoldState: BottomSheetScaffoldState,
) {
Column(
modifier = Modifier
.fillMaxWidth()
.height(LocalConfiguration.current.screenHeightDp.dp - TangemTheme.dimens.spacing16),
) {
Hand()
Column(
modifier = Modifier
.fillMaxWidth()
.verticalScroll(rememberScrollState()),
) {
model.categoriesBlocks.forEachIndexed { index, categoryBlock ->
key(categoryBlock) {
Column {
TokensList(
title = categoryBlock.name,
tokens = categoryBlock.items,
onTestTokenClick = { address ->
model.onTestTokenClick(address)
coroutineScope.launch { bottomSheetScaffoldState.bottomSheetState.collapse() }
},
)
if (model.categoriesBlocks.lastIndex != index) {
Divider()
SpacerH8()
}
}
}
}
}
}
}
@Composable
private fun TokensList(title: String, tokens: List<TestTokenItem>, onTestTokenClick: (String) -> Unit) {
Text(
text = title,
modifier = Modifier.padding(
horizontal = TangemTheme.dimens.spacing24,
vertical = TangemTheme.dimens.spacing8,
),
maxLines = 1,
style = TangemTheme.typography.h3,
)
tokens.forEach { token ->
key(token) {
PrimaryButton(
text = token.name,
onClick = { onTestTokenClick(token.address) },
modifier = Modifier
.padding(horizontal = TangemTheme.dimens.spacing16)
.padding(bottom = TangemTheme.dimens.spacing8)
.fillMaxWidth(),
)
}
}
}
@OptIn(ExperimentalMaterialApi::class, ExperimentalComposeUiApi::class)
@Composable
private fun TestBlock(
model: AddCustomTokenTestBlock,
coroutineScope: CoroutineScope,
bottomSheetScaffoldState: BottomSheetScaffoldState,
) {
Column(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = TangemTheme.dimens.spacing16)
.padding(top = TangemTheme.dimens.spacing16),
verticalArrangement = Arrangement.spacedBy(space = TangemTheme.dimens.spacing8),
) {
val softwareKeyboardController = LocalSoftwareKeyboardController.current
PrimaryButton(
text = model.chooseTokenButtonText,
onClick = {
softwareKeyboardController?.hide()
coroutineScope.launch { bottomSheetScaffoldState.bottomSheetState.expand() }
},
modifier = Modifier.fillMaxWidth(),
)
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing8),
verticalAlignment = Alignment.CenterVertically,
) {
PrimaryButton(
text = model.clearButtonText,
onClick = model.onClearAddressButtonClick,
modifier = Modifier.weight(1f),
)
PrimaryButton(
text = model.resetButtonText,
onClick = model.onResetButtonClick,
modifier = Modifier.weight(1f),
)
}
}
}
@Preview(showSystemUi = true)
@Composable
private fun Preview_AddCustomTokenTestContent() {
TangemTheme {
AddCustomTokenTestContent(
state = AddCustomTokenStateHolder.TestContent(
onBackButtonClick = {},
toolbar = AddCustomTokensToolbar(
title = TextReference.Res(R.string.add_custom_token_title),
onBackButtonClick = {},
),
form = com.tangem.tap.features.customtoken.impl.presentation.models.AddCustomTokenForm(
contractAddressInputField = AddCustomTokenInputField.ContactAddress(
value = "",
onValueChange = {},
isError = false,
isLoading = false,
),
networkSelectorField = AddCustomTokenSelectorField.Network(
selectedItem = AddCustomTokenSelectorField.SelectorItem.Title(
title = TextReference.Str(value = "Avalanche"),
blockchain = Blockchain.Avalanche,
),
items = listOf(),
onMenuItemClick = {},
),
tokenNameInputField = AddCustomTokenInputField.TokenName(
value = "",
onValueChange = {},
isEnabled = false,
isError = false,
),
tokenSymbolInputField = AddCustomTokenInputField.TokenSymbol(
value = "",
onValueChange = {},
isEnabled = false,
isError = false,
),
decimalsInputField = AddCustomTokenInputField.Decimals(
value = "",
onValueChange = {},
isEnabled = false,
isError = false,
),
derivationPathSelectorField = null,
),
warnings = listOf(),
floatingButton = AddCustomTokenFloatingButton(
isEnabled = false,
onClick = {},
),
testBlock = AddCustomTokenTestBlock(
chooseTokenButtonText = "Choose token",
clearButtonText = "Clear address",
resetButtonText = "Reset",
onClearAddressButtonClick = {},
onResetButtonClick = {},
),
bottomSheet = AddCustomTokenChooseTokenBottomSheet(
categoriesBlocks = listOf(),
onTestTokenClick = {},
),
),
)
}
}

View file

@ -0,0 +1,51 @@
package com.tangem.tap.features.customtoken.impl.presentation.ui.components
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.imePadding
import androidx.compose.foundation.layout.padding
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview
import com.tangem.core.ui.components.PrimaryButtonIconLeft
import com.tangem.core.ui.res.TangemTheme
import com.tangem.tap.features.customtoken.impl.presentation.models.AddCustomTokenFloatingButton
import com.tangem.wallet.R
/**
* Add custom token floating button. Attached above the keyboard.
*
* @param model button model
*
[REDACTED_AUTHOR]
*/
@Composable
internal fun AddCustomTokenFloatingButton(model: AddCustomTokenFloatingButton) {
PrimaryButtonIconLeft(
modifier = Modifier
.imePadding()
.padding(horizontal = TangemTheme.dimens.spacing16)
.fillMaxWidth(),
text = stringResource(id = R.string.common_add),
icon = painterResource(id = R.drawable.ic_plus_24),
enabled = model.isEnabled,
onClick = model.onClick,
)
}
@Preview
@Composable
private fun Preview_AddCustomTokenFloatingButton_Enabled() {
TangemTheme {
AddCustomTokenFloatingButton(model = AddCustomTokenFloatingButton(isEnabled = true, onClick = {}))
}
}
@Preview
@Composable
private fun Preview_AddCustomTokenFloatingButton_Disabled() {
TangemTheme {
AddCustomTokenFloatingButton(model = AddCustomTokenFloatingButton(isEnabled = false, onClick = {}))
}
}

View file

@ -0,0 +1,216 @@
package com.tangem.tap.features.customtoken.impl.presentation.ui.components
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.Card
import androidx.compose.material.DropdownMenuItem
import androidx.compose.material.ExperimentalMaterialApi
import androidx.compose.material.ExposedDropdownMenuBox
import androidx.compose.material.ExposedDropdownMenuDefaults
import androidx.compose.material.LinearProgressIndicator
import androidx.compose.material.OutlinedTextField
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.tooling.preview.Preview
import com.tangem.blockchain.common.Blockchain
import com.tangem.core.ui.res.TangemTheme
import com.tangem.tap.common.compose.TangemTextFieldsDefault
import com.tangem.tap.features.customtoken.impl.presentation.models.AddCustomTokenForm
import com.tangem.tap.features.customtoken.impl.presentation.models.AddCustomTokenInputField
import com.tangem.tap.features.customtoken.impl.presentation.models.AddCustomTokenSelectorField
import com.tangem.tap.features.details.ui.cardsettings.TextReference
import com.tangem.tap.features.details.ui.cardsettings.resolveReference
/**
* Add custom token form
*
* @param model component model
*
[REDACTED_AUTHOR]
*/
@Composable
internal fun AddCustomTokenForm(model: AddCustomTokenForm) {
Card(
modifier = Modifier
.fillMaxWidth()
.padding(TangemTheme.dimens.spacing16),
shape = RoundedCornerShape(TangemTheme.dimens.radius8),
backgroundColor = TangemTheme.colors.background.primary,
elevation = TangemTheme.dimens.elevation4,
) {
Column(
modifier = Modifier.padding(TangemTheme.dimens.spacing16),
verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing8),
) {
InputField(model = model.contractAddressInputField)
SelectorField(model = model.networkSelectorField)
InputField(model = model.tokenNameInputField)
InputField(model = model.tokenSymbolInputField)
InputField(model = model.decimalsInputField)
model.derivationPathSelectorField?.let { SelectorField(model = it) }
}
}
}
@Composable
private fun InputField(model: AddCustomTokenInputField) {
Box {
OutlinedTextField(
modifier = Modifier.fillMaxWidth(),
value = model.value,
onValueChange = model.onValueChange,
keyboardOptions = model.keyboardOptions,
label = {
Text(
text = model.label.resolveReference(),
style = TangemTheme.typography.caption,
color = TangemTextFieldsDefault.defaultTextFieldColors.labelColor(
enabled = model.isEnabled,
error = model.isError,
interactionSource = remember { MutableInteractionSource() },
).value,
)
},
placeholder = {
Text(
text = model.placeholder.resolveReference(),
style = TangemTheme.typography.body1,
color = TangemTextFieldsDefault.defaultTextFieldColors
.placeholderColor(enabled = model.isEnabled)
.value,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
)
},
singleLine = true,
enabled = model.isEnabled,
isError = model.isError,
colors = TangemTextFieldsDefault.defaultTextFieldColors,
)
AnimatedVisibility(
visible = model.isLoading,
modifier = Modifier
.fillMaxWidth()
.align(Alignment.BottomCenter)
.padding(horizontal = TangemTheme.dimens.spacing6)
.padding(bottom = TangemTheme.dimens.spacing6),
) {
LinearProgressIndicator(color = TangemTheme.colors.icon.primary1)
}
}
}
@OptIn(ExperimentalMaterialApi::class)
@Composable
private fun SelectorField(model: AddCustomTokenSelectorField) {
var isExpanded by remember { mutableStateOf(value = false) }
ExposedDropdownMenuBox(
expanded = isExpanded,
onExpandedChange = { isExpanded = !isExpanded },
) {
OutlinedTextField(
value = when (val item = model.selectedItem) {
is AddCustomTokenSelectorField.SelectorItem.Title -> item.title
is AddCustomTokenSelectorField.SelectorItem.TitleWithSubtitle -> item.subtitle
}.resolveReference(),
modifier = Modifier.fillMaxWidth(),
onValueChange = {},
readOnly = true,
enabled = model.isEnabled,
label = { Text(text = model.label.resolveReference()) },
trailingIcon = { ExposedDropdownMenuDefaults.TrailingIcon(expanded = isExpanded) },
colors = TangemTextFieldsDefault.defaultTextFieldColors,
)
ExposedDropdownMenu(
expanded = isExpanded && model.isEnabled,
onDismissRequest = { isExpanded = false },
) {
FocusRequester
model.items.forEachIndexed { index, item ->
DropdownMenuItem(
onClick = {
model.onMenuItemClick(index)
isExpanded = false
},
) {
Column {
Text(text = item.title.resolveReference())
val subtitle = (item as? AddCustomTokenSelectorField.SelectorItem.TitleWithSubtitle)
?.subtitle?.resolveReference()
if (!subtitle.isNullOrBlank()) {
Text(
text = subtitle,
color = TangemTheme.colors.text.secondary,
maxLines = 1,
style = TangemTheme.typography.caption,
)
}
}
}
}
}
}
}
@Preview
@Composable
private fun Preview_AddCustomTokenForm() {
TangemTheme {
AddCustomTokenForm(
AddCustomTokenForm(
contractAddressInputField = AddCustomTokenInputField.ContactAddress(
value = "",
onValueChange = {},
isError = false,
isLoading = false,
),
networkSelectorField = AddCustomTokenSelectorField.Network(
selectedItem = AddCustomTokenSelectorField.SelectorItem.Title(
title = TextReference.Str(value = "Avalanche"),
blockchain = Blockchain.Avalanche,
),
items = listOf(),
onMenuItemClick = {},
),
tokenNameInputField = AddCustomTokenInputField.TokenName(
value = "",
onValueChange = {},
isEnabled = false,
isError = false,
),
tokenSymbolInputField = AddCustomTokenInputField.TokenSymbol(
value = "",
onValueChange = {},
isEnabled = false,
isError = false,
),
decimalsInputField = AddCustomTokenInputField.Decimals(
value = "",
onValueChange = {},
isEnabled = false,
isError = false,
),
derivationPathSelectorField = null,
),
)
}
}

View file

@ -0,0 +1,54 @@
package com.tangem.tap.features.customtoken.impl.presentation.ui.components
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.width
import androidx.compose.material.Icon
import androidx.compose.material.IconButton
import androidx.compose.material.Text
import androidx.compose.material.TopAppBar
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.tooling.preview.Preview
import com.tangem.core.ui.R
import com.tangem.core.ui.res.TangemTheme
import com.tangem.tap.features.details.ui.cardsettings.TextReference
import com.tangem.tap.features.details.ui.cardsettings.resolveReference
/**
* Add custom token toolbar
*
* @param title title
* @param onBackButtonClick lambda be invoked when BackButton is been pressed
*
[REDACTED_AUTHOR]
*/
@Composable
internal fun AddCustomTokenToolbar(title: TextReference, onBackButtonClick: () -> Unit) {
TopAppBar(backgroundColor = TangemTheme.colors.background.secondary) {
IconButton(onClick = onBackButtonClick) {
Icon(
painter = painterResource(id = R.drawable.ic_back_24),
contentDescription = null,
tint = TangemTheme.colors.icon.secondary,
)
}
Spacer(modifier = Modifier.width(TangemTheme.dimens.spacing26))
Text(
text = title.resolveReference(),
color = TangemTheme.colors.text.primary1,
maxLines = 1,
style = TangemTheme.typography.h3,
)
}
}
@Preview
@Composable
internal fun Preview_AddCustomTokenToolbar() {
TangemTheme {
AddCustomTokenToolbar(title = TextReference.Res(R.string.add_custom_token_title), onBackButtonClick = {})
}
}

View file

@ -0,0 +1,54 @@
package com.tangem.tap.features.customtoken.impl.presentation.ui.components
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.Card
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.sp
import com.tangem.core.ui.res.TangemColorPalette
import com.tangem.core.ui.res.TangemTheme
import com.tangem.tap.features.details.ui.cardsettings.TextReference
import com.tangem.tap.features.details.ui.cardsettings.resolveReference
import com.tangem.wallet.R
/**
* Add custom token warning component
* FIXME("Incorrect typography. Replace with typography from design system")
*
* @param description warning description
* @param modifier modifier
*
[REDACTED_AUTHOR]
*/
@Composable
internal fun AddCustomTokenWarning(description: TextReference, modifier: Modifier = Modifier) {
Card(
modifier = modifier,
shape = RoundedCornerShape(TangemTheme.dimens.radius4),
backgroundColor = TangemColorPalette.Tangerine,
contentColor = TangemColorPalette.White,
elevation = TangemTheme.dimens.elevation4,
) {
Column(
modifier = Modifier.padding(all = TangemTheme.dimens.spacing16),
verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing8),
) {
Text(
text = stringResource(id = R.string.common_warning),
maxLines = 1,
style = TangemTheme.typography.body2.copy(fontWeight = FontWeight.Bold),
)
Text(
text = description.resolveReference(),
fontSize = 13.sp,
lineHeight = 18.sp,
)
}
}
}

View file

@ -13,7 +13,7 @@ interface DataConverterVisitor<Data, Result> {
interface FieldDataConverter<Result> : DataConverterVisitor<FieldData, Result> interface FieldDataConverter<Result> : DataConverterVisitor<FieldData, Result>
abstract class BaseFieldDataConverter<Result> : FieldDataConverter<Result> { abstract class BaseFieldDataConverter<Result> : FieldDataConverter<Result> {
protected val collectIds: List<FieldId> private val collectIds: List<FieldId>
get() = getIdToCollect() get() = getIdToCollect()
protected val collectedData: MutableMap<FieldId, Any?> = mutableMapOf() protected val collectedData: MutableMap<FieldId, Any?> = mutableMapOf()
@ -31,7 +31,7 @@ abstract class BaseFieldDataConverter<Result> : FieldDataConverter<Result> {
class FieldToJsonConverter( class FieldToJsonConverter(
private val fieldsToConvert: List<FieldId> = listOf(), private val fieldsToConvert: List<FieldId> = listOf(),
protected val jsonConverter: MoshiJsonConverter, private val jsonConverter: MoshiJsonConverter,
) : BaseFieldDataConverter<String>() { ) : BaseFieldDataConverter<String>() {
override fun getConvertedData(): String = jsonConverter.toJson(collectedData, " ") override fun getConvertedData(): String = jsonConverter.toJson(collectedData, " ")

View file

@ -16,16 +16,14 @@ import timber.log.Timber
interface CustomTokenValidator<T> : Validator<T, AddCustomTokenError> interface CustomTokenValidator<T> : Validator<T, AddCustomTokenError>
class StringIsEmptyValidator : CustomTokenValidator<String> { class StringIsEmptyValidator : CustomTokenValidator<String> {
override fun validate(data: String?): AddCustomTokenError? = when { override fun validate(data: String?): AddCustomTokenError? {
data == null || data.isEmpty() -> null return if (data.isNullOrEmpty()) null else AddCustomTokenError.FieldIsNotEmpty
else -> AddCustomTokenError.FieldIsNotEmpty
} }
} }
class StringIsNotEmptyValidator : CustomTokenValidator<String> { class StringIsNotEmptyValidator : CustomTokenValidator<String> {
override fun validate(data: String?): AddCustomTokenError? = when { override fun validate(data: String?): AddCustomTokenError? {
data == null || data.isEmpty() -> AddCustomTokenError.FieldIsEmpty return if (data.isNullOrEmpty()) AddCustomTokenError.FieldIsEmpty else null
else -> null
} }
} }
@ -46,12 +44,10 @@ class TokenContractAddressValidator : CustomTokenValidator<String> {
} }
override fun validate(data: String?): AddCustomTokenError? { override fun validate(data: String?): AddCustomTokenError? {
if (data == null || data.isEmpty()) return AddCustomTokenError.FieldIsEmpty return when {
data.isNullOrEmpty() -> AddCustomTokenError.FieldIsEmpty
return if (getAddressService().validate(data)) { getAddressService().validate(data) -> null
null else -> AddCustomTokenError.InvalidContractAddress
} else {
AddCustomTokenError.InvalidContractAddress
} }
} }
@ -74,9 +70,11 @@ class TokenContractAddressValidator : CustomTokenValidator<String> {
} }
class TokenNetworkValidator : CustomTokenValidator<Blockchain> { class TokenNetworkValidator : CustomTokenValidator<Blockchain> {
override fun validate(data: Blockchain?): AddCustomTokenError? = when (data) { override fun validate(data: Blockchain?): AddCustomTokenError? {
null, Blockchain.Unknown -> AddCustomTokenError.NetworkIsNotSelected return when (data) {
else -> null null, Blockchain.Unknown -> AddCustomTokenError.NetworkIsNotSelected
else -> null
}
} }
} }
@ -88,14 +86,14 @@ class TokenSymbolValidator : CustomTokenValidator<String> {
override fun validate(data: String?): AddCustomTokenError? = StringIsNotEmptyValidator().validate(data) override fun validate(data: String?): AddCustomTokenError? = StringIsNotEmptyValidator().validate(data)
} }
@Suppress("MagicNumber")
class TokenDecimalsValidator : CustomTokenValidator<String> { class TokenDecimalsValidator : CustomTokenValidator<String> {
override fun validate(data: String?): AddCustomTokenError? { override fun validate(data: String?): AddCustomTokenError? {
val decimal = data?.toIntOrNull() ?: return AddCustomTokenError.FieldIsEmpty val decimal = data?.toIntOrNull() ?: return AddCustomTokenError.FieldIsEmpty
return when { return if (decimal > INVALID_DECIMALS_COUNT) AddCustomTokenError.InvalidDecimalsCount else null
decimal > 30 -> AddCustomTokenError.InvalidDecimalsCount }
else -> null
} private companion object {
const val INVALID_DECIMALS_COUNT = 30
} }
} }

View file

@ -125,7 +125,7 @@ internal class AddCustomTokenHub : BaseStoreHub<AddCustomTokenState>("AddCustomT
} }
} }
} }
else -> {} else -> Unit
} }
} }
@ -532,21 +532,24 @@ internal class AddCustomTokenHub : BaseStoreHub<AddCustomTokenState>("AddCustomT
} }
private fun CustomTokenFieldId.validateValue(value: Any): AddCustomTokenError? { private fun CustomTokenFieldId.validateValue(value: Any): AddCustomTokenError? {
val state = hubState
val contractAddressValidator: TokenContractAddressValidator = state.getValidator(ContractAddress)
val nameValidator: TokenNameValidator = state.getValidator(Name)
val symbolValidator: TokenSymbolValidator = state.getValidator(Symbol)
val decimalsValidator: TokenDecimalsValidator = state.getValidator(Decimals)
val networkValidator: TokenNetworkValidator = state.getValidator(Network)
return when (this) { return when (this) {
ContractAddress -> { ContractAddress -> {
val contractAddressValidator: TokenContractAddressValidator = hubState.getValidator(ContractAddress)
contractAddressValidator.nextValidationFor(Network.getFieldValue()) contractAddressValidator.nextValidationFor(Network.getFieldValue())
contractAddressValidator.validate(value as String) contractAddressValidator.validate(value as String)
} }
Network, DerivationPath -> networkValidator.validate(value as Blockchain) Network, DerivationPath -> {
Name -> nameValidator.validate(value as String) hubState.getValidator<TokenNetworkValidator>(Network).validate(value as Blockchain)
Symbol -> symbolValidator.validate(value as String) }
Decimals -> decimalsValidator.validate(value as String) Name -> {
hubState.getValidator<TokenNameValidator>(Name).validate(value as String)
}
Symbol -> {
hubState.getValidator<TokenSymbolValidator>(Symbol).validate(value as String)
}
Decimals -> {
hubState.getValidator<TokenDecimalsValidator>(Decimals).validate(value as String)
}
} }
} }

View file

@ -159,10 +159,6 @@ data class AddCustomTokenState(
.getConvertedData() .getConvertedData()
} }
fun getNetworks(card: CardDTO, type: CustomTokenType): List<Blockchain> {
return getNetworksList(card, type)
}
companion object { companion object {
/** /**