Updated on 2026-08-14

This commit is contained in:
Tangem 2022-04-13 10:30:27 +03:00
parent 35d74021ac
commit 0a25bbedbb
17 changed files with 224 additions and 173 deletions

View file

@ -0,0 +1,31 @@
package com.tangem.tap.common.compose
import androidx.annotation.StringRes
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import com.tangem.blockchain.common.Blockchain
import com.tangem.domain.common.form.Field
@Composable
fun BlockchainSpinner(
@StringRes title: Int,
itemList: List<Blockchain>,
selectedItem: Field.Data<Blockchain>,
isEnabled: Boolean = true,
textFieldConverter: (Blockchain) -> String,
dropdownItemView: @Composable ((Blockchain) -> Unit)? = null,
onItemSelected: (Blockchain) -> Unit,
) {
OutlinedSpinner(
modifier = Modifier.fillMaxWidth(),
label = stringResource(id = title),
itemList = itemList,
selectedItem = selectedItem,
textFieldConverter = textFieldConverter,
dropdownItemView = dropdownItemView,
isEnabled = isEnabled,
onItemSelected = onItemSelected
)
}

View file

@ -4,8 +4,8 @@ import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.LocalTextStyle
import androidx.compose.material.MaterialTheme
import androidx.compose.material.Surface
import androidx.compose.material.Text
import androidx.compose.runtime.*
@ -22,6 +22,7 @@ import com.tangem.domain.DomainStateDialog
import com.tangem.domain.redux.domainStore
import com.tangem.domain.redux.global.DomainGlobalAction
import com.tangem.domain.redux.global.DomainGlobalState
import com.tangem.tap.features.tokens.addCustomToken.compose.SelectTokenNetworkDialog
import org.rekotlin.StoreSubscriber
@Composable
@ -56,15 +57,7 @@ fun ShowTheDialog(dialogState: MutableState<DomainStateDialog?>) {
val onDismissRequest = { domainStore.dispatch(DomainGlobalAction.ShowDialog(null)) }
when (val dialog = dialogState.value) {
is DomainDialog.SelectTokenDialog -> {
SimpleDialog(
title = "Select a token",
items = dialog.items,
itemNameConverter = dialog.itemNameConverter,
onSelect = dialog.onSelect,
onDismissRequest = onDismissRequest
)
}
is DomainDialog.SelectTokenDialog -> SelectTokenNetworkDialog(dialog, onDismissRequest)
}
}
@ -75,9 +68,9 @@ fun ShowTheDialog(dialogState: MutableState<DomainStateDialog?>) {
fun <T> SimpleDialog(
title: String,
items: List<T>,
itemNameConverter: (T) -> String,
onSelect: (T) -> Unit,
onDismissRequest: () -> Unit
onDismissRequest: () -> Unit,
itemContent: @Composable (T) -> Unit,
) {
Dialog(
properties = DialogProperties(false, false),
@ -85,7 +78,7 @@ fun <T> SimpleDialog(
) {
Surface(
modifier = Modifier.fillMaxWidth(),
shape = RoundedCornerShape(8.dp)
shape = MaterialTheme.shapes.medium
) {
Column(
modifier = Modifier.padding(22.dp)
@ -112,11 +105,7 @@ fun <T> SimpleDialog(
onDismissRequest()
},
verticalAlignment = Alignment.CenterVertically,
) {
Text(
text = itemNameConverter(item),
)
}
) { itemContent(item) }
}
}
}

View file

@ -5,6 +5,8 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.tooling.preview.Preview
import com.tangem.blockchain.common.Blockchain
import com.tangem.common.extensions.VoidCallback
@ -14,17 +16,16 @@ import com.tangem.tap.common.extensions.ValueCallback
/**
[REDACTED_AUTHOR]
*/
private class OutlinedSpinner
@OptIn(ExperimentalMaterialApi::class)
@Composable
fun <T> OutlinedSpinner(
modifier: Modifier = Modifier,
title: String,
label: String,
itemList: List<T>,
selectedItem: Field.Data<T>,
onItemSelected: ValueCallback<T>,
itemNameConverter: (T) -> String = { it.toString() },
textFieldConverter: (T) -> String = { it.toString() },
dropdownItemView: @Composable ((T) -> Unit)? = null,
isEnabled: Boolean = true,
onClose: VoidCallback = {}
) {
@ -48,24 +49,27 @@ fun <T> OutlinedSpinner(
expanded = rIsExpanded.value,
onExpandedChange = { rIsExpanded.value = !rIsExpanded.value },
) {
OutlinedTextField(
modifier = modifier,
readOnly = true,
enabled = isEnabled,
value = itemNameConverter(rSelectedItem.value),
onValueChange = {},
label = { Text(title) },
trailingIcon = { ExposedDropdownMenuDefaults.TrailingIcon(expanded = rIsExpanded.value) },
)
ProvideTextStyle(value = TextStyle(color = Color.Blue)) {
OutlinedTextField(
modifier = modifier,
readOnly = true,
enabled = isEnabled,
value = textFieldConverter(rSelectedItem.value),
onValueChange = {},
label = { Text(label) },
trailingIcon = { ExposedDropdownMenuDefaults.TrailingIcon(expanded = rIsExpanded.value) },
)
}
ExposedDropdownMenu(
expanded = rIsExpanded.value,
onDismissRequest = onDismissRequest,
) {
itemList.forEach { item ->
DropdownMenuItem(
onClick = { onItemSelectedInternal(item) }
) {
Text(itemNameConverter(item))
DropdownMenuItem(onClick = { onItemSelectedInternal(item) }) {
when (dropdownItemView) {
null -> Text(textFieldConverter(item))
else -> dropdownItemView(item)
}
}
}
}
@ -77,7 +81,7 @@ fun <T> OutlinedSpinner(
fun TestSpinnerPreview() {
Scaffold() {
OutlinedSpinner(
title = "Blockchain name",
label = "Blockchain name",
itemList = listOf(Blockchain.values()),
selectedItem = Field.Data(Blockchain.Avalanche),
onItemSelected = {},

View file

@ -28,8 +28,6 @@ import com.tangem.tap.common.compose.extensions.stringResourceDefault
/**
[REDACTED_AUTHOR]
*/
private class OutlinedTextFieldWidget
@Composable
fun OutlinedTextFieldWidget(
modifier: Modifier = Modifier,
@ -90,7 +88,7 @@ private fun OutlinedProgressTextField(
val rTextDebouncer = valueDebouncerAsState(debounce, onTextChanged)
val rText = remember { mutableStateOf(textFieldData.value) }
fun updateFieldValueAndEmmit(value: String){
fun updateFieldValueAndEmmit(value: String) {
rText.value = value
rTextDebouncer.emmit(value)
}

View file

@ -40,28 +40,28 @@ fun SpacerH24(modifier: Modifier = Modifier) {
// ***************************** Vertical
@Composable
fun SpacerV(width: Dp, modifier: Modifier = Modifier) {
fun SpacerW(width: Dp, modifier: Modifier = Modifier) {
Spacer(modifier = modifier.width(width))
}
@Composable
fun SpacerV4(modifier: Modifier = Modifier) {
SpacerV(4.dp, modifier)
fun SpacerW4(modifier: Modifier = Modifier) {
SpacerW(4.dp, modifier)
}
@Composable
fun SpacerV8(modifier: Modifier = Modifier) {
SpacerV(8.dp, modifier)
fun SpacerW8(modifier: Modifier = Modifier) {
SpacerW(8.dp, modifier)
}
@Composable
fun SpacerV16(modifier: Modifier = Modifier) {
SpacerV(16.dp, modifier)
fun SpacerW16(modifier: Modifier = Modifier) {
SpacerW(16.dp, modifier)
}
@Composable
fun SpacerV24(modifier: Modifier = Modifier) {
SpacerV(24.dp, modifier)
fun SpacerW24(modifier: Modifier = Modifier) {
SpacerW(24.dp, modifier)
}
// ***************************** Size

View file

@ -0,0 +1,27 @@
package com.tangem.tap.common.compose
import androidx.compose.foundation.layout.Column
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.unit.sp
/**
[REDACTED_AUTHOR]
* Compose views are not typically used as a main or base view.
*/
@Composable
fun TitleSubtitle(
title: String,
subtitle: String
) {
Column() {
Text(text = title)
Text(
text = subtitle,
fontSize = 12.sp,
color = Color.Gray
)
}
}

View file

@ -13,7 +13,7 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import com.tangem.tap.common.compose.SpacerV4
import com.tangem.tap.common.compose.SpacerW4
@Composable
fun StoriesProgressBar(
@ -68,7 +68,7 @@ fun StoriesProgressBar(
) {}
}
if (index != steps) {
SpacerV4()
SpacerW4()
}
}
}

View file

@ -35,10 +35,15 @@ class CustomTokenWarningConverter(
override fun convertError(error: DomainError): String {
val customTokenWarning = (error as? AddCustomTokenWarning) ?: throw UnsupportedOperationException()
val resId = when (customTokenWarning) {
val rawMessage = when (customTokenWarning) {
AddCustomTokenWarning.PotentialScamToken -> R.string.custom_token_validation_error_not_found
AddCustomTokenWarning.TokenAlreadyAdded -> R.string.custom_token_validation_error_already_added
AddCustomTokenWarning.Network.CheckAddressRequestError -> "CheckAddressRequestError"
}
return when (rawMessage) {
is Int -> context.getString(rawMessage)
is String -> rawMessage
else -> "Unknown error: ${customTokenWarning::class.java.simpleName}"
}
return context.getString(resId)
}
}

View file

@ -2,12 +2,10 @@ package com.tangem.tap.features.tokens.addCustomToken.compose
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material.Scaffold
import androidx.compose.material.Surface
import androidx.compose.material.Text
import androidx.compose.material.rememberScaffoldState
import androidx.compose.material.*
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Add
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
@ -29,10 +27,7 @@ import com.tangem.domain.features.addCustomToken.redux.AddCustomTokenState
import com.tangem.domain.features.addCustomToken.redux.ScreenState
import com.tangem.domain.features.addCustomToken.redux.ViewStates
import com.tangem.domain.redux.domainStore
import com.tangem.tap.common.compose.ComposeDialogManager
import com.tangem.tap.common.compose.OutlinedTextFieldWidget
import com.tangem.tap.common.compose.SpacerH8
import com.tangem.tap.common.compose.keyboardObserverAsState
import com.tangem.tap.common.compose.*
import com.tangem.tap.features.tokens.addCustomToken.CustomTokenErrorConverter
import com.tangem.tap.features.tokens.addCustomToken.CustomTokenWarningConverter
import com.tangem.wallet.R
@ -52,12 +47,12 @@ fun AddCustomTokenScreen(state: MutableState<AddCustomTokenState>) {
) {
Box(Modifier.fillMaxSize()) {
LazyColumn(
contentPadding = PaddingValues(bottom = 80.dp)
contentPadding = PaddingValues(bottom = 90.dp)
) {
item {
Surface(
modifier = Modifier.padding(16.dp),
shape = RoundedCornerShape(4.dp),
shape = MaterialTheme.shapes.small,
elevation = 4.dp,
) {
Column(
@ -143,7 +138,7 @@ private fun TokenNameView(screenFieldData: ScreenFieldData) {
error = screenFieldData.error,
errorConverter = screenFieldData.errorConverter,
) {
domainStore.dispatch(OnTokenFieldChanged(screenFieldData.field.id, Field.Data(it)))
domainStore.dispatch(OnTokenNameChanged(Field.Data(it)))
}
SpacerH8()
}
@ -153,14 +148,14 @@ private fun TokenNetworkView(screenFieldData: ScreenFieldData) {
if (!screenFieldData.viewState.isVisible) return
val notSelected = stringResource(id = R.string.custom_token_network_input_not_selected)
val networkField = screenFieldData.field as TokenNetworkField
val networkField = screenFieldData.field as TokenBlockchainField
TokenNetworkSpinner(
BlockchainSpinner(
title = R.string.custom_token_network_input_title,
itemList = networkField.itemList,
selectedItem = networkField.data,
isEnabled = screenFieldData.viewState.isEnabled,
itemNameConverter = { AddCustomTokenState.convertBlockchainName(it, notSelected) },
textFieldConverter = { AddCustomTokenState.convertBlockchainName(it, notSelected) },
) { domainStore.dispatch(OnTokenNetworkChanged(Field.Data(it))) }
SpacerH8()
}
@ -178,7 +173,7 @@ private fun TokenSymbolView(screenFieldData: ScreenFieldData) {
isEnabled = screenFieldData.viewState.isEnabled,
error = screenFieldData.error,
errorConverter = screenFieldData.errorConverter,
) { domainStore.dispatch(OnTokenFieldChanged(screenFieldData.field.id, Field.Data(it))) }
) { domainStore.dispatch(OnTokenSymbolChanged(Field.Data(it))) }
SpacerH8()
}
@ -207,12 +202,17 @@ private fun TokenDerivationPathView(screenFieldData: ScreenFieldData) {
val notSelected = stringResource(id = R.string.custom_token_derivation_path_default)
val networkField = screenFieldData.field as TokenDerivationPathField
TokenNetworkSpinner(
title = R.string.custom_token_network_input_title,
BlockchainSpinner(
title = R.string.custom_token_derivation_path_input_title,
itemList = networkField.itemList,
selectedItem = networkField.data,
isEnabled = screenFieldData.viewState.isEnabled,
itemNameConverter = { AddCustomTokenState.convertDerivationPathName(it, notSelected) },
textFieldConverter = { AddCustomTokenState.convertBlockchainName(it, notSelected) },
dropdownItemView = { blockchain ->
val derivationPathLabel = AddCustomTokenState.convertDerivationPathLabel(blockchain, notSelected)
val blockchainName = AddCustomTokenState.convertBlockchainName(blockchain, notSelected)
TitleSubtitle(derivationPathLabel, blockchainName)
}
) { domainStore.dispatch(OnTokenDerivationPathChanged(Field.Data(it))) }
SpacerH8()
}
@ -227,13 +227,13 @@ private fun Warnings(warnings: List<AddCustomTokenWarning>) {
Column {
warnings.forEachIndexed { index, item ->
val modifier = when (index) {
0 -> Modifier.padding(16.dp, 0.dp, 16.dp, 16.dp)
warnings.lastIndex -> Modifier.padding(16.dp, 16.dp, 16.dp, 16.dp)
else -> Modifier.padding(16.dp, 16.dp, 16.dp, 0.dp)
0 -> Modifier.padding(16.dp, 16.dp, 16.dp, 0.dp)
warnings.lastIndex -> Modifier.padding(16.dp, 8.dp, 16.dp, 16.dp)
else -> Modifier.padding(16.dp, 8.dp, 16.dp, 0.dp)
}
Surface(
modifier = modifier.fillMaxWidth(),
shape = RoundedCornerShape(4.dp),
shape = MaterialTheme.shapes.small,
color = colorResource(id = R.color.darkGray2),
contentColor = colorResource(id = R.color.darkGray3)
) {
@ -248,6 +248,30 @@ private fun Warnings(warnings: List<AddCustomTokenWarning>) {
}
}
@Composable
private fun AddButton(
modifier: Modifier = Modifier,
isEnabled: Boolean,
textId: Int = R.string.common_add,
onClick: () -> Unit,
) {
Button(
textId = textId,
isEnabled = isEnabled,
modifier = modifier
.height(52.dp)
.padding(horizontal = 16.dp)
.fillMaxWidth(),
leadingView = {
Icon(
imageVector = Icons.Filled.Add,
contentDescription = "Add",
)
},
onClick = onClick
)
}
private data class ScreenFieldData(
val field: DataField<*>,
val error: AddCustomTokenError?,

View file

@ -1,66 +0,0 @@
package com.tangem.tap.features.tokens.addCustomToken.compose
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.material.Icon
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Add
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import com.tangem.blockchain.common.Blockchain
import com.tangem.domain.common.form.Field
import com.tangem.tap.common.compose.Button
import com.tangem.tap.common.compose.OutlinedSpinner
import com.tangem.tap.common.extensions.ValueCallback
import com.tangem.wallet.R
/**
[REDACTED_AUTHOR]
*/
@Composable
fun TokenNetworkSpinner(
title: Int,
itemList: List<Blockchain>,
selectedItem: Field.Data<Blockchain>,
isEnabled: Boolean = true,
itemNameConverter: (Blockchain) -> String,
onItemSelected: ValueCallback<Blockchain>,
) {
OutlinedSpinner(
modifier = Modifier.fillMaxWidth(),
title = stringResource(id = title),
itemList = itemList,
selectedItem = selectedItem,
itemNameConverter = itemNameConverter,
isEnabled = isEnabled,
onItemSelected = onItemSelected
)
}
@Composable
fun AddButton(
modifier: Modifier = Modifier,
isEnabled: Boolean,
textId: Int = R.string.common_add,
onClick: () -> Unit,
) {
Button(
textId = textId,
isEnabled = isEnabled,
modifier = modifier
.height(52.dp)
.padding(horizontal = 16.dp)
.fillMaxWidth(),
leadingView = {
Icon(
imageVector = Icons.Filled.Add,
contentDescription = "Add",
)
},
onClick = onClick
)
}

View file

@ -0,0 +1,21 @@
package com.tangem.tap.features.tokens.addCustomToken.compose
import androidx.compose.runtime.Composable
import androidx.compose.ui.res.stringResource
import com.tangem.domain.DomainDialog
import com.tangem.tap.common.compose.SimpleDialog
import com.tangem.tap.common.compose.TitleSubtitle
import com.tangem.wallet.R
/**
[REDACTED_AUTHOR]
*/
@Composable
fun SelectTokenNetworkDialog(dialog: DomainDialog.SelectTokenDialog, onDismissRequest: () -> Unit) {
SimpleDialog(
title = stringResource(id = R.string.custom_token_type_network),
items = dialog.items,
onSelect = dialog.onSelect,
onDismissRequest = onDismissRequest
) { contract -> TitleSubtitle(dialog.networkIdConverter(contract.networkId), contract.address) }
}