diff --git a/app/src/main/java/com/tangem/tap/features/customtoken/impl/presentation/models/AddCustomTokenViewsModels.kt b/app/src/main/java/com/tangem/tap/features/customtoken/impl/presentation/models/AddCustomTokenViewsModels.kt index 65a253714a..90dcb8a583 100644 --- a/app/src/main/java/com/tangem/tap/features/customtoken/impl/presentation/models/AddCustomTokenViewsModels.kt +++ b/app/src/main/java/com/tangem/tap/features/customtoken/impl/presentation/models/AddCustomTokenViewsModels.kt @@ -3,6 +3,7 @@ package com.tangem.tap.features.customtoken.impl.presentation.models import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.ui.text.input.ImeAction 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.wallet.R @@ -243,20 +244,29 @@ internal sealed interface AddCustomTokenSelectorField { /** Title */ val title: TextReference + /** Blockchain */ + val blockchain: Blockchain + /** * 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 * - * @property title title text - * @property subtitle subtitle text + * @property title title 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 } } diff --git a/app/src/main/java/com/tangem/tap/features/customtoken/impl/presentation/ui/AddCustomTokenContent.kt b/app/src/main/java/com/tangem/tap/features/customtoken/impl/presentation/ui/AddCustomTokenContent.kt new file mode 100644 index 0000000000..7b746c3ec4 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/features/customtoken/impl/presentation/ui/AddCustomTokenContent.kt @@ -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 = {}, + ), + ), + ) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/customtoken/impl/presentation/ui/AddCustomTokenScreen.kt b/app/src/main/java/com/tangem/tap/features/customtoken/impl/presentation/ui/AddCustomTokenScreen.kt new file mode 100644 index 0000000000..82f284fb98 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/features/customtoken/impl/presentation/ui/AddCustomTokenScreen.kt @@ -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) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/customtoken/impl/presentation/ui/AddCustomTokenTestContent.kt b/app/src/main/java/com/tangem/tap/features/customtoken/impl/presentation/ui/AddCustomTokenTestContent.kt new file mode 100644 index 0000000000..dce924a655 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/features/customtoken/impl/presentation/ui/AddCustomTokenTestContent.kt @@ -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, 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 = {}, + ), + ), + ) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/customtoken/impl/presentation/ui/components/AddCustomTokenFloatingButton.kt b/app/src/main/java/com/tangem/tap/features/customtoken/impl/presentation/ui/components/AddCustomTokenFloatingButton.kt new file mode 100644 index 0000000000..082bfe4939 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/features/customtoken/impl/presentation/ui/components/AddCustomTokenFloatingButton.kt @@ -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 = {})) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/customtoken/impl/presentation/ui/components/AddCustomTokenForm.kt b/app/src/main/java/com/tangem/tap/features/customtoken/impl/presentation/ui/components/AddCustomTokenForm.kt new file mode 100644 index 0000000000..44904ac319 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/features/customtoken/impl/presentation/ui/components/AddCustomTokenForm.kt @@ -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, + ), + ) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/customtoken/impl/presentation/ui/components/AddCustomTokenToolbar.kt b/app/src/main/java/com/tangem/tap/features/customtoken/impl/presentation/ui/components/AddCustomTokenToolbar.kt new file mode 100644 index 0000000000..8587b990bf --- /dev/null +++ b/app/src/main/java/com/tangem/tap/features/customtoken/impl/presentation/ui/components/AddCustomTokenToolbar.kt @@ -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 = {}) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/customtoken/impl/presentation/ui/components/AddCustomTokenWarning.kt b/app/src/main/java/com/tangem/tap/features/customtoken/impl/presentation/ui/components/AddCustomTokenWarning.kt new file mode 100644 index 0000000000..5eb76c6e66 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/features/customtoken/impl/presentation/ui/components/AddCustomTokenWarning.kt @@ -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, + ) + } + } +} \ No newline at end of file diff --git a/domain/legacy/src/main/java/com/tangem/domain/common/form/FieldDataConverters.kt b/domain/legacy/src/main/java/com/tangem/domain/common/form/FieldDataConverters.kt index f4800c7223..b406d02f7c 100644 --- a/domain/legacy/src/main/java/com/tangem/domain/common/form/FieldDataConverters.kt +++ b/domain/legacy/src/main/java/com/tangem/domain/common/form/FieldDataConverters.kt @@ -13,7 +13,7 @@ interface DataConverterVisitor { interface FieldDataConverter : DataConverterVisitor abstract class BaseFieldDataConverter : FieldDataConverter { - protected val collectIds: List + private val collectIds: List get() = getIdToCollect() protected val collectedData: MutableMap = mutableMapOf() @@ -31,7 +31,7 @@ abstract class BaseFieldDataConverter : FieldDataConverter { class FieldToJsonConverter( private val fieldsToConvert: List = listOf(), - protected val jsonConverter: MoshiJsonConverter, + private val jsonConverter: MoshiJsonConverter, ) : BaseFieldDataConverter() { override fun getConvertedData(): String = jsonConverter.toJson(collectedData, " ") diff --git a/domain/legacy/src/main/java/com/tangem/domain/common/form/FieldsValidators.kt b/domain/legacy/src/main/java/com/tangem/domain/common/form/FieldsValidators.kt index 8f5e7dfd00..067feb900f 100644 --- a/domain/legacy/src/main/java/com/tangem/domain/common/form/FieldsValidators.kt +++ b/domain/legacy/src/main/java/com/tangem/domain/common/form/FieldsValidators.kt @@ -16,16 +16,14 @@ import timber.log.Timber interface CustomTokenValidator : Validator class StringIsEmptyValidator : CustomTokenValidator { - override fun validate(data: String?): AddCustomTokenError? = when { - data == null || data.isEmpty() -> null - else -> AddCustomTokenError.FieldIsNotEmpty + override fun validate(data: String?): AddCustomTokenError? { + return if (data.isNullOrEmpty()) null else AddCustomTokenError.FieldIsNotEmpty } } class StringIsNotEmptyValidator : CustomTokenValidator { - override fun validate(data: String?): AddCustomTokenError? = when { - data == null || data.isEmpty() -> AddCustomTokenError.FieldIsEmpty - else -> null + override fun validate(data: String?): AddCustomTokenError? { + return if (data.isNullOrEmpty()) AddCustomTokenError.FieldIsEmpty else null } } @@ -46,12 +44,10 @@ class TokenContractAddressValidator : CustomTokenValidator { } override fun validate(data: String?): AddCustomTokenError? { - if (data == null || data.isEmpty()) return AddCustomTokenError.FieldIsEmpty - - return if (getAddressService().validate(data)) { - null - } else { - AddCustomTokenError.InvalidContractAddress + return when { + data.isNullOrEmpty() -> AddCustomTokenError.FieldIsEmpty + getAddressService().validate(data) -> null + else -> AddCustomTokenError.InvalidContractAddress } } @@ -74,9 +70,11 @@ class TokenContractAddressValidator : CustomTokenValidator { } class TokenNetworkValidator : CustomTokenValidator { - override fun validate(data: Blockchain?): AddCustomTokenError? = when (data) { - null, Blockchain.Unknown -> AddCustomTokenError.NetworkIsNotSelected - else -> null + override fun validate(data: Blockchain?): AddCustomTokenError? { + return when (data) { + null, Blockchain.Unknown -> AddCustomTokenError.NetworkIsNotSelected + else -> null + } } } @@ -88,14 +86,14 @@ class TokenSymbolValidator : CustomTokenValidator { override fun validate(data: String?): AddCustomTokenError? = StringIsNotEmptyValidator().validate(data) } -@Suppress("MagicNumber") class TokenDecimalsValidator : CustomTokenValidator { override fun validate(data: String?): AddCustomTokenError? { val decimal = data?.toIntOrNull() ?: return AddCustomTokenError.FieldIsEmpty - return when { - decimal > 30 -> AddCustomTokenError.InvalidDecimalsCount - else -> null - } + return if (decimal > INVALID_DECIMALS_COUNT) AddCustomTokenError.InvalidDecimalsCount else null + } + + private companion object { + const val INVALID_DECIMALS_COUNT = 30 } } \ No newline at end of file diff --git a/domain/legacy/src/main/java/com/tangem/domain/features/addCustomToken/redux/AddCustomTokenHub.kt b/domain/legacy/src/main/java/com/tangem/domain/features/addCustomToken/redux/AddCustomTokenHub.kt index 08315dc850..72eeb341bc 100644 --- a/domain/legacy/src/main/java/com/tangem/domain/features/addCustomToken/redux/AddCustomTokenHub.kt +++ b/domain/legacy/src/main/java/com/tangem/domain/features/addCustomToken/redux/AddCustomTokenHub.kt @@ -125,7 +125,7 @@ internal class AddCustomTokenHub : BaseStoreHub("AddCustomT } } } - else -> {} + else -> Unit } } @@ -532,21 +532,24 @@ internal class AddCustomTokenHub : BaseStoreHub("AddCustomT } 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) { ContractAddress -> { + val contractAddressValidator: TokenContractAddressValidator = hubState.getValidator(ContractAddress) contractAddressValidator.nextValidationFor(Network.getFieldValue()) contractAddressValidator.validate(value as String) } - Network, DerivationPath -> networkValidator.validate(value as Blockchain) - Name -> nameValidator.validate(value as String) - Symbol -> symbolValidator.validate(value as String) - Decimals -> decimalsValidator.validate(value as String) + Network, DerivationPath -> { + hubState.getValidator(Network).validate(value as Blockchain) + } + Name -> { + hubState.getValidator(Name).validate(value as String) + } + Symbol -> { + hubState.getValidator(Symbol).validate(value as String) + } + Decimals -> { + hubState.getValidator(Decimals).validate(value as String) + } } } diff --git a/domain/legacy/src/main/java/com/tangem/domain/features/addCustomToken/redux/AddCustomTokenState.kt b/domain/legacy/src/main/java/com/tangem/domain/features/addCustomToken/redux/AddCustomTokenState.kt index 8d32aa4f6b..545339f567 100644 --- a/domain/legacy/src/main/java/com/tangem/domain/features/addCustomToken/redux/AddCustomTokenState.kt +++ b/domain/legacy/src/main/java/com/tangem/domain/features/addCustomToken/redux/AddCustomTokenState.kt @@ -159,10 +159,6 @@ data class AddCustomTokenState( .getConvertedData() } - fun getNetworks(card: CardDTO, type: CustomTokenType): List { - return getNetworksList(card, type) - } - companion object { /**