Updated on 2026-08-14

This commit is contained in:
Tangem 2022-04-14 17:28:33 +03:00
parent 787d354d3d
commit a07f371456
36 changed files with 666 additions and 469 deletions

View file

@ -19,7 +19,7 @@ import com.tangem.wallet.R
[REDACTED_AUTHOR]
*/
@Composable
fun Button(
fun RectangleButton(
modifier: Modifier = Modifier,
text: String = "",
textId: Int? = null,
@ -90,7 +90,7 @@ fun ButtonTest() {
) {
Column(modifier = Modifier.padding(16.dp)) {
PreviewItem("Button") {
Button(text = "Some button") {}
RectangleButton(text = "Some button") {}
}
PreviewItem("PasteButton") {
PasteButton(onClick = {})

View file

@ -4,13 +4,12 @@ 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.material.LocalTextStyle
import androidx.compose.material.MaterialTheme
import androidx.compose.material.Surface
import androidx.compose.material.Text
import androidx.compose.material.*
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
@ -22,7 +21,9 @@ 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.DomainErrorConverter
import com.tangem.tap.features.tokens.addCustomToken.compose.SelectTokenNetworkDialog
import com.tangem.wallet.R
import org.rekotlin.StoreSubscriber
@Composable
@ -51,12 +52,19 @@ fun ComposeDialogManager() {
}
@Composable
fun ShowTheDialog(dialogState: MutableState<DomainStateDialog?>) {
private fun ShowTheDialog(dialogState: MutableState<DomainStateDialog?>) {
if (dialogState.value == null) return
val context = LocalContext.current
val errorConverter = remember { DomainErrorConverter(context) }
val onDismissRequest = { domainStore.dispatch(DomainGlobalAction.ShowDialog(null)) }
when (val dialog = dialogState.value) {
is DomainDialog.DialogError -> ErrorDialog(
title = stringResource(id = R.string.common_error),
body = errorConverter.convertError(dialog.error),
onDismissRequest
)
is DomainDialog.SelectTokenDialog -> SelectTokenNetworkDialog(dialog, onDismissRequest)
}
}
@ -83,17 +91,7 @@ fun <T> SimpleDialog(
Column(
modifier = Modifier.padding(22.dp)
) {
Text(
text = title,
style = LocalTextStyle.provides(
TextStyle(
fontWeight = FontWeight.Bold,
fontSize = 20.sp
)
).value
)
SpacerH16()
DialogTitle(title = title)
LazyColumn() {
items(items) { item ->
Row(
@ -111,4 +109,36 @@ fun <T> SimpleDialog(
}
}
}
}
}
@Composable
private fun DialogTitle(title: String) {
Text(
text = title,
style = LocalTextStyle.provides(
TextStyle(
fontWeight = FontWeight.Bold,
fontSize = 20.sp
)
).value
)
SpacerH16()
}
@Composable
fun ErrorDialog(
title: String,
body: String,
onDismissRequest: () -> Unit,
) {
AlertDialog(
title = { DialogTitle(title) },
text = { Text(body) },
onDismissRequest = onDismissRequest,
confirmButton = {
Button(onClick = onDismissRequest) {
Text(text = stringResource(id = R.string.common_ok))
}
}
)
}

View file

@ -6,7 +6,7 @@ import androidx.compose.runtime.*
import androidx.compose.ui.platform.LocalView
sealed class Keyboard {
data class Opened(val height: Int): Keyboard()
data class Opened(val height: Int) : Keyboard()
object Closed : Keyboard()
}
@ -14,12 +14,21 @@ sealed class Keyboard {
fun keyboardAsState(): State<Keyboard> {
val keyboardState: MutableState<Keyboard> = remember { mutableStateOf(Keyboard.Closed) }
val view = LocalView.current
val discrepancy = remember {
mutableStateOf(0)
}
DisposableEffect(view) {
val onGlobalListener = ViewTreeObserver.OnGlobalLayoutListener {
val rect = Rect()
view.getWindowVisibleDisplayFrame(rect)
val screenHeight = view.rootView.height
val keypadHeight = screenHeight - rect.bottom
val keypadHeight: Int = screenHeight - (rect.bottom + rect.top) - discrepancy.value
if (discrepancy.value == 0) {
discrepancy.value = keypadHeight;
if (keypadHeight == 0) discrepancy.value = 1
}
keyboardState.value = if (keypadHeight > screenHeight * 0.15) {
Keyboard.Opened(keypadHeight)
} else {

View file

@ -0,0 +1,19 @@
package com.tangem.tap.common.compose.extensions
import androidx.compose.runtime.Composable
import androidx.compose.ui.platform.LocalContext
import com.tangem.tap.common.extensions.copyToClipboard
import com.tangem.tap.common.extensions.getFromClipboard
/**
[REDACTED_AUTHOR]
*/
@Composable
fun copyToClipboard(value: Any, label: String = "") {
LocalContext.current.copyToClipboard(value, label)
}
@Composable
fun getFromClipboard(default: CharSequence? = null): CharSequence? {
return LocalContext.current.getFromClipboard(default)
}

View file

@ -20,6 +20,7 @@ import com.tangem.tap.features.onboarding.products.twins.ui.TwinsCardsFragment
import com.tangem.tap.features.onboarding.products.wallet.ui.OnboardingWalletFragment
import com.tangem.tap.features.send.ui.SendFragment
import com.tangem.tap.features.shop.ui.ShopFragment
import com.tangem.tap.features.tokens.addCustomToken.AddCustomTokenFragment
import com.tangem.tap.features.tokens.ui.AddTokensFragment
import com.tangem.tap.features.wallet.ui.WalletDetailsFragment
import com.tangem.tap.features.wallet.ui.WalletFragment
@ -82,6 +83,7 @@ private fun fragmentFactory(screen: AppScreen): Fragment {
AppScreen.DetailsSecurity -> DetailsSecurityFragment()
AppScreen.Disclaimer -> DisclaimerFragment()
AppScreen.AddTokens -> AddTokensFragment()
AppScreen.AddCustomToken -> AddCustomTokenFragment()
AppScreen.WalletDetails -> WalletDetailsFragment()
AppScreen.WalletConnectSessions -> WalletConnectSessionsFragment()
AppScreen.QrScan -> QrScanFragment()

View file

@ -9,9 +9,9 @@ import com.squareup.picasso.Transformation
import com.tangem.blockchain.common.Blockchain
import com.tangem.blockchain.common.IconsUtil
import com.tangem.blockchain.common.Token
import com.tangem.domain.common.extensions.toNetworkId
import com.tangem.tap.domain.extensions.getCustomIconUrl
import com.tangem.tap.domain.tokens.getIconUrl
import com.tangem.tap.domain.tokens.toNetworkId
import com.tangem.wallet.R
fun Picasso.loadCurrenciesIcon(

View file

@ -17,7 +17,7 @@ enum class AppScreen {
Wallet, WalletDetails,
Send,
Details, DetailsConfirm, DetailsSecurity,
AddTokens,
AddTokens, AddCustomToken,
WalletConnectSessions,
QrScan
}

View file

@ -2,6 +2,7 @@ package com.tangem.tap.domain.tokens
import com.squareup.moshi.JsonClass
import com.tangem.blockchain.common.Blockchain
import com.tangem.domain.common.extensions.fromNetworkId
@JsonClass(generateAdapter = true)
data class CurrencyFromJson(
@ -74,62 +75,4 @@ data class Contract(
fun getIconUrl(id: String): String {
return "https://s3.eu-central-1.amazonaws.com/tangem.api/coins/large/$id.png"
}
fun Blockchain.Companion.fromNetworkId(networkId: String): Blockchain? {
return when (networkId) {
"avalanche" -> Blockchain.Avalanche
"binancecoin" -> Blockchain.Binance
"binance-smart-chain" -> Blockchain.BSC
"ethereum" -> Blockchain.Ethereum
"polygon-pos" -> Blockchain.Polygon
"solana" -> Blockchain.Solana
"fantom" -> Blockchain.Fantom
"bitcoin" -> Blockchain.Bitcoin
"bitcoin-cash" -> Blockchain.BitcoinCash
"cardano" -> Blockchain.CardanoShelley
"dogecoin" -> Blockchain.Dogecoin
"ducatus" -> Blockchain.Ducatus
"litecoin" -> Blockchain.Litecoin
"rsk" -> Blockchain.RSK
"stellar" -> Blockchain.Stellar
"tezos" -> Blockchain.Tezos
"ripple" -> Blockchain.XRP
else -> null
}
}
fun Blockchain.toNetworkId(): String {
return when (this) {
Blockchain.Unknown -> "unknown"
Blockchain.Avalanche -> "avalanche"
Blockchain.AvalancheTestnet -> "avalaunche"
Blockchain.Binance -> "binancecoin"
Blockchain.BinanceTestnet -> "binancecoin"
Blockchain.BSC -> "binance-smart-chain"
Blockchain.BSCTestnet -> "binance-smart-chain"
Blockchain.Bitcoin -> "bitcoin"
Blockchain.BitcoinTestnet -> "bitcoin"
Blockchain.BitcoinCash -> "bitcoin-cash"
Blockchain.BitcoinCashTestnet -> "bitcoin-cash"
Blockchain.Cardano -> "cardano"
Blockchain.CardanoShelley -> "cardano"
Blockchain.Dogecoin -> "dogecoin"
Blockchain.Ducatus -> "ducatus"
Blockchain.Ethereum -> "ethereum"
Blockchain.EthereumTestnet -> "ethereum"
Blockchain.Fantom -> "fantom"
Blockchain.FantomTestnet -> "fantom"
Blockchain.Litecoin -> "litecoin"
Blockchain.Polygon -> "matic-network"
Blockchain.PolygonTestnet -> "matic-networks"
Blockchain.RSK -> "rootstock"
Blockchain.Stellar -> "stellar"
Blockchain.StellarTestnet -> "stellar"
Blockchain.Solana -> "solana"
Blockchain.SolanaTestnet -> "solana"
Blockchain.Tezos -> "tezos"
Blockchain.XRP -> "ripple"
}
}

View file

@ -10,40 +10,40 @@ import com.tangem.wallet.R
/**
[REDACTED_AUTHOR]
*/
class CustomTokenErrorConverter(
class DomainErrorConverter(
private val context: Context
) : ErrorConverter<String> {
override fun convertError(error: DomainError): String {
val errorMessage = when (error) {
is AddCustomTokenError -> AddCustomTokenConverter(context).convertError(error)
else -> null
}
return errorMessage?.let { it } ?: "Unknown error: ${error::class.java.simpleName}"
}
}
private class AddCustomTokenConverter(
private val context: Context
) : ErrorConverter<String> {
override fun convertError(error: DomainError): String {
val customTokenError = (error as? AddCustomTokenError) ?: throw UnsupportedOperationException()
val resId = when (customTokenError) {
val rawMessage = when (customTokenError) {
AddCustomTokenWarning.PotentialScamToken -> R.string.custom_token_validation_error_not_found
AddCustomTokenWarning.TokenAlreadyAdded -> R.string.custom_token_validation_error_already_added
AddCustomTokenError.InvalidContractAddress -> R.string.custom_token_creation_error_invalid_contract_address
AddCustomTokenError.NetworkIsNotSelected -> R.string.custom_token_creation_error_network_not_selected
AddCustomTokenError.InvalidDerivationPath -> R.string.custom_token_creation_error_invalid_derivation_path
AddCustomTokenError.InvalidDecimalsCount -> R.string.custom_token_creation_error_wrong_decimals
AddCustomTokenError.FieldIsEmpty -> R.string.custom_token_creation_error_empty_fields
else -> null
}
return resId?.let { context.getString(it) } ?: "Unknown error: ${customTokenError::class.java.simpleName}"
}
}
class CustomTokenWarningConverter(
private val context: Context
) : ErrorConverter<String> {
override fun convertError(error: DomainError): String {
val customTokenWarning = (error as? AddCustomTokenWarning) ?: throw UnsupportedOperationException()
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}"
// is String -> rawMessage
else -> "Unknown error: ${customTokenError::class.java.simpleName}"
}
}
}

View file

@ -2,34 +2,31 @@ package com.tangem.tap.features.tokens.addCustomToken.compose
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.text.KeyboardOptions
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
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.colorResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import com.tangem.domain.ErrorConverter
import com.tangem.domain.common.form.DataField
import com.tangem.domain.common.form.Field
import com.tangem.domain.common.form.FieldId
import com.tangem.domain.features.addCustomToken.*
import com.tangem.domain.features.addCustomToken.AddCustomTokenError
import com.tangem.domain.features.addCustomToken.AddCustomTokenWarning
import com.tangem.domain.features.addCustomToken.CustomTokenFieldId.*
import com.tangem.domain.features.addCustomToken.redux.AddCustomTokenAction
import com.tangem.domain.features.addCustomToken.redux.AddCustomTokenAction.*
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.*
import com.tangem.tap.features.tokens.addCustomToken.CustomTokenErrorConverter
import com.tangem.tap.features.tokens.addCustomToken.CustomTokenWarningConverter
import com.tangem.tap.common.compose.ComposeDialogManager
import com.tangem.tap.common.compose.keyboardAsState
import com.tangem.tap.features.tokens.addCustomToken.DomainErrorConverter
import com.tangem.wallet.R
/**
@ -44,12 +41,18 @@ fun AddCustomTokenScreen(state: MutableState<AddCustomTokenState>) {
Scaffold(
scaffoldState = scaffoldState,
backgroundColor = colorResource(id = R.color.backgroundLightGray),
floatingActionButton = {
HangingOverKeyboardView(keyboardState = keyboardAsState()) {
AddButton(state)
}
},
floatingActionButtonPosition = FabPosition.Center,
) {
Box(Modifier.fillMaxSize()) {
LazyColumn(
contentPadding = PaddingValues(bottom = 90.dp)
) {
item { AddCustomTokenDebugActions() }
// item { AddCustomTokenDebugActions() }
item {
Surface(
modifier = Modifier.padding(16.dp),
@ -67,18 +70,6 @@ fun AddCustomTokenScreen(state: MutableState<AddCustomTokenState>) {
}
item { Warnings(state.value.warnings.toList()) }
}
HangingOverKeyboardView(
modifier = Modifier
.align(Alignment.BottomCenter),
keyboardState = keyboardAsState(),
defaultBottomPadding = 30.dp,
spaceBetweenKeyboard = 20.dp,
) {
AddButton(
isEnabled = state.value.screenState.addButton.isEnabled
) {
}
}
}
ComposeDialogManager()
}
@ -90,7 +81,7 @@ fun AddCustomTokenScreen(state: MutableState<AddCustomTokenState>) {
@Composable
private fun FormFields(state: MutableState<AddCustomTokenState>) {
val context = LocalContext.current
val errorConverter = remember { CustomTokenErrorConverter(context) }
val errorConverter = remember { DomainErrorConverter(context) }
val stateValue = state.value
stateValue.form.fieldList.forEach { field ->
@ -107,124 +98,11 @@ private fun FormFields(state: MutableState<AddCustomTokenState>) {
}
@Composable
private fun TokenContractAddressView(screenFieldData: ScreenFieldData) {
if (!screenFieldData.viewState.isVisible) return
val tokenField = screenFieldData.field as TokenField
OutlinedTextFieldWidget(
textFieldData = tokenField.data,
labelId = R.string.custom_token_contract_address_input_title,
placeholder = "0x0000000000000000",
isEnabled = screenFieldData.viewState.isEnabled,
isLoading = screenFieldData.viewState.isLoading,
error = screenFieldData.error,
errorConverter = screenFieldData.errorConverter,
) {
domainStore.dispatch(OnTokenContractAddressChanged(Field.Data(it)))
}
SpacerH8()
}
@Composable
private fun TokenNameView(screenFieldData: ScreenFieldData) {
if (!screenFieldData.viewState.isVisible) return
val tokenField = screenFieldData.field as TokenField
OutlinedTextFieldWidget(
textFieldData = tokenField.data,
labelId = R.string.custom_token_name_input_title,
placeholderId = R.string.custom_token_name_input_placeholder,
isEnabled = screenFieldData.viewState.isEnabled,
error = screenFieldData.error,
errorConverter = screenFieldData.errorConverter,
) {
domainStore.dispatch(OnTokenNameChanged(Field.Data(it)))
}
SpacerH8()
}
@Composable
private fun TokenNetworkView(screenFieldData: ScreenFieldData, state: AddCustomTokenState) {
if (!screenFieldData.viewState.isVisible) return
val notSelected = stringResource(id = R.string.custom_token_network_input_not_selected)
val networkField = screenFieldData.field as TokenBlockchainField
BlockchainSpinner(
title = R.string.custom_token_network_input_title,
itemList = networkField.itemList,
selectedItem = networkField.data,
isEnabled = screenFieldData.viewState.isEnabled,
textFieldConverter = { state.convertBlockchainName(it, notSelected) },
) { domainStore.dispatch(OnTokenNetworkChanged(Field.Data(it))) }
SpacerH8()
}
@Composable
private fun TokenSymbolView(screenFieldData: ScreenFieldData) {
if (!screenFieldData.viewState.isVisible) return
val tokenField = screenFieldData.field as TokenField
OutlinedTextFieldWidget(
textFieldData = tokenField.data,
labelId = R.string.custom_token_token_symbol_input_title,
placeholderId = R.string.custom_token_token_symbol_input_placeholder,
isEnabled = screenFieldData.viewState.isEnabled,
error = screenFieldData.error,
errorConverter = screenFieldData.errorConverter,
) { domainStore.dispatch(OnTokenSymbolChanged(Field.Data(it))) }
SpacerH8()
}
@Composable
private fun TokenDecimalsView(screenFieldData: ScreenFieldData) {
if (!screenFieldData.viewState.isVisible) return
val tokenField = screenFieldData.field as TokenField
OutlinedTextFieldWidget(
textFieldData = tokenField.data,
labelId = R.string.custom_token_decimals_input_title,
placeholder = "8",
isEnabled = screenFieldData.viewState.isEnabled,
error = screenFieldData.error,
errorConverter = screenFieldData.errorConverter,
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number),
) { domainStore.dispatch(OnTokenDecimalsChanged(Field.Data(it))) }
SpacerH8()
}
@Composable
private fun TokenDerivationPathView(screenFieldData: ScreenFieldData, state: AddCustomTokenState) {
if (!screenFieldData.viewState.isVisible) return
val notSelected = stringResource(id = R.string.custom_token_derivation_path_default)
val networkField = screenFieldData.field as TokenDerivationPathField
BlockchainSpinner(
title = R.string.custom_token_derivation_path_input_title,
itemList = networkField.itemList,
selectedItem = networkField.data,
isEnabled = screenFieldData.viewState.isEnabled,
textFieldConverter = { state.convertBlockchainName(it, notSelected) },
dropdownItemView = { blockchain ->
val derivationPathLabel = state.convertDerivationPathLabel(blockchain, notSelected)
val blockchainName = state.convertBlockchainName(blockchain, notSelected)
TitleSubtitle(derivationPathLabel, blockchainName)
}
) { domainStore.dispatch(OnTokenDerivationPathChanged(Field.Data(it))) }
SpacerH8()
}
@Composable
private fun Warnings(warnings: List<AddCustomTokenWarning>) {
fun Warnings(warnings: List<AddCustomTokenWarning>) {
if (warnings.isEmpty()) return
val context = LocalContext.current
val warningConverter = remember { CustomTokenWarningConverter(context) }
val warningConverter = remember { DomainErrorConverter(context) }
Column {
warnings.forEachIndexed { index, item ->
@ -236,8 +114,8 @@ private fun Warnings(warnings: List<AddCustomTokenWarning>) {
Surface(
modifier = modifier.fillMaxWidth(),
shape = MaterialTheme.shapes.small,
color = colorResource(id = R.color.darkGray2),
contentColor = colorResource(id = R.color.darkGray3)
color = colorResource(id = R.color.warning_warning),
elevation = 4.dp,
) {
Text(
modifier = Modifier.padding(16.dp),
@ -251,30 +129,48 @@ private fun Warnings(warnings: List<AddCustomTokenWarning>) {
}
@Composable
private fun AddButton(
private fun AddButton(state: MutableState<AddCustomTokenState>) {
AddCustomTokenFab(
modifier = Modifier
.widthIn(210.dp, 280.dp),
isEnabled = state.value.screenState.addButton.isEnabled
) { domainStore.dispatch(AddCustomTokenAction.OnAddCustomTokenClicked) }
}
@Composable
fun AddCustomTokenFab(
modifier: Modifier = Modifier,
isEnabled: Boolean,
textId: Int = R.string.common_add,
onClick: () -> Unit,
isEnabled: Boolean = true,
onClick: () -> Unit
) {
Button(
textId = textId,
isEnabled = isEnabled,
modifier = modifier
.height(52.dp)
.padding(horizontal = 16.dp)
.fillMaxWidth(),
leadingView = {
val contentColor = if (isEnabled) {
Color.White
} else {
colorResource(id = R.color.darkGray1)
}
val backgroundColor = Color(0xFF1ACE80)
ExtendedFloatingActionButton(
modifier = modifier,
icon = {
Icon(
imageVector = Icons.Filled.Add,
tint = contentColor,
contentDescription = "Add",
)
},
onClick = onClick
text = {
Text(
text = stringResource(id = R.string.common_add),
)
},
onClick = onClick,
backgroundColor = backgroundColor,
contentColor = contentColor,
)
}
private data class ScreenFieldData(
data class ScreenFieldData(
val field: DataField<*>,
val error: AddCustomTokenError?,
val errorConverter: ErrorConverter<String>,
@ -284,7 +180,7 @@ private data class ScreenFieldData(
fun fromState(
field: DataField<*>,
state: AddCustomTokenState,
errorConverter: CustomTokenErrorConverter
errorConverter: DomainErrorConverter
): ScreenFieldData {
return ScreenFieldData(
field = field,

View file

@ -14,8 +14,10 @@ import com.tangem.blockchain.common.Blockchain
import com.tangem.common.extensions.VoidCallback
import com.tangem.common.services.Result
import com.tangem.domain.common.form.Field
import com.tangem.domain.features.addCustomToken.TangemTechServiceManager
import com.tangem.domain.features.addCustomToken.redux.AddCustomTokenAction
import com.tangem.domain.redux.domainStore
import com.tangem.network.api.tangemTech.TangemTechService
import com.tangem.wallet.BuildConfig
import timber.log.Timber
@ -145,7 +147,7 @@ private fun CustomActions() {
CustomActionButton(
name = "Find tokens in several networks",
action = {
val manager = domainStore.state.addCustomTokensState.addCustomTokenManager
val manager = TangemTechServiceManager(TangemTechService())
val currencies = manager.tokens()
val asdfsd = mutableMapOf<String, MutableList<Any>>()
val contractAddresses = currencies.mapNotNull { currency ->

View file

@ -0,0 +1,134 @@
package com.tangem.tap.features.tokens.addCustomToken.compose
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.runtime.Composable
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.input.KeyboardType
import com.tangem.domain.common.form.Field
import com.tangem.domain.features.addCustomToken.TokenBlockchainField
import com.tangem.domain.features.addCustomToken.TokenDerivationPathField
import com.tangem.domain.features.addCustomToken.TokenField
import com.tangem.domain.features.addCustomToken.redux.AddCustomTokenAction
import com.tangem.domain.features.addCustomToken.redux.AddCustomTokenState
import com.tangem.domain.redux.domainStore
import com.tangem.tap.common.compose.BlockchainSpinner
import com.tangem.tap.common.compose.OutlinedTextFieldWidget
import com.tangem.tap.common.compose.SpacerH8
import com.tangem.tap.common.compose.TitleSubtitle
import com.tangem.wallet.R
/**
[REDACTED_AUTHOR]
*/
@Composable
fun TokenContractAddressView(screenFieldData: ScreenFieldData) {
if (!screenFieldData.viewState.isVisible) return
val tokenField = screenFieldData.field as TokenField
OutlinedTextFieldWidget(
textFieldData = tokenField.data,
labelId = R.string.custom_token_contract_address_input_title,
placeholder = "0x0000000000000000",
isEnabled = screenFieldData.viewState.isEnabled,
isLoading = screenFieldData.viewState.isLoading,
error = screenFieldData.error,
errorConverter = screenFieldData.errorConverter,
) {
domainStore.dispatch(AddCustomTokenAction.OnTokenContractAddressChanged(Field.Data(it)))
}
SpacerH8()
}
@Composable
fun TokenNameView(screenFieldData: ScreenFieldData) {
if (!screenFieldData.viewState.isVisible) return
val tokenField = screenFieldData.field as TokenField
OutlinedTextFieldWidget(
textFieldData = tokenField.data,
labelId = R.string.custom_token_name_input_title,
placeholderId = R.string.custom_token_name_input_placeholder,
isEnabled = screenFieldData.viewState.isEnabled,
error = screenFieldData.error,
errorConverter = screenFieldData.errorConverter,
) {
domainStore.dispatch(AddCustomTokenAction.OnTokenNameChanged(Field.Data(it)))
}
SpacerH8()
}
@Composable
fun TokenNetworkView(screenFieldData: ScreenFieldData, state: AddCustomTokenState) {
if (!screenFieldData.viewState.isVisible) return
val notSelected = stringResource(id = R.string.custom_token_network_input_not_selected)
val networkField = screenFieldData.field as TokenBlockchainField
BlockchainSpinner(
title = R.string.custom_token_network_input_title,
itemList = networkField.itemList,
selectedItem = networkField.data,
isEnabled = screenFieldData.viewState.isEnabled,
textFieldConverter = { state.convertBlockchainName(it, notSelected) },
) { domainStore.dispatch(AddCustomTokenAction.OnTokenNetworkChanged(Field.Data(it))) }
SpacerH8()
}
@Composable
fun TokenSymbolView(screenFieldData: ScreenFieldData) {
if (!screenFieldData.viewState.isVisible) return
val tokenField = screenFieldData.field as TokenField
OutlinedTextFieldWidget(
textFieldData = tokenField.data,
labelId = R.string.custom_token_token_symbol_input_title,
placeholderId = R.string.custom_token_token_symbol_input_placeholder,
isEnabled = screenFieldData.viewState.isEnabled,
error = screenFieldData.error,
errorConverter = screenFieldData.errorConverter,
) { domainStore.dispatch(AddCustomTokenAction.OnTokenSymbolChanged(Field.Data(it))) }
SpacerH8()
}
@Composable
fun TokenDecimalsView(screenFieldData: ScreenFieldData) {
if (!screenFieldData.viewState.isVisible) return
val tokenField = screenFieldData.field as TokenField
OutlinedTextFieldWidget(
textFieldData = tokenField.data,
labelId = R.string.custom_token_decimals_input_title,
placeholder = "8",
isEnabled = screenFieldData.viewState.isEnabled,
error = screenFieldData.error,
errorConverter = screenFieldData.errorConverter,
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number),
) { domainStore.dispatch(AddCustomTokenAction.OnTokenDecimalsChanged(Field.Data(it))) }
SpacerH8()
}
@Composable
fun TokenDerivationPathView(screenFieldData: ScreenFieldData, state: AddCustomTokenState) {
if (!screenFieldData.viewState.isVisible) return
val notSelected = stringResource(id = R.string.custom_token_derivation_path_default)
val networkField = screenFieldData.field as TokenDerivationPathField
BlockchainSpinner(
title = R.string.custom_token_derivation_path_input_title,
itemList = networkField.itemList,
selectedItem = networkField.data,
isEnabled = screenFieldData.viewState.isEnabled,
textFieldConverter = { state.convertBlockchainName(it, notSelected) },
dropdownItemView = { blockchain ->
val derivationPathLabel = state.convertDerivationPathLabel(blockchain, notSelected)
val blockchainName = state.convertBlockchainName(blockchain, notSelected)
TitleSubtitle(derivationPathLabel, blockchainName)
}
) { domainStore.dispatch(AddCustomTokenAction.OnTokenDerivationPathChanged(Field.Data(it))) }
SpacerH8()
}

View file

@ -1,7 +1,5 @@
package com.tangem.tap.features.tokens.addCustomToken.compose
import android.content.Context
import android.util.TypedValue
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.BoxScope
import androidx.compose.foundation.layout.padding
@ -21,36 +19,19 @@ import com.tangem.tap.common.compose.Keyboard
fun HangingOverKeyboardView(
modifier: Modifier = Modifier,
keyboardState: State<Keyboard>,
defaultBottomPadding: Dp = 0.dp,
spaceBetweenKeyboard: Dp = 10.dp,
calculateWithActionBarHeight: Boolean = true,
spaceBetweenKeyboard: Dp = 0.dp,
content: @Composable() (BoxScope.() -> Unit)
) {
fun getActionBarHeight(context: Context): Int {
val typedValue = TypedValue()
return if (context.theme.resolveAttribute(android.R.attr.actionBarSize, typedValue, true)) {
val data = typedValue.data
val displayMetrics = context.resources.displayMetrics
TypedValue.complexToDimensionPixelSize(data, displayMetrics)
} else {
0
}
}
val context = LocalContext.current
val calculatedPadding = when (keyboardState.value) {
Keyboard.Closed -> defaultBottomPadding
val padding = when (keyboardState.value) {
Keyboard.Closed -> 0.dp
is Keyboard.Opened -> {
val keyboardHeight = (keyboardState.value as Keyboard.Opened).height
val keyboardPadding = context.pxToDp(keyboardHeight.toFloat()).dp
if (calculateWithActionBarHeight) {
val actionBarHeight = context.pxToDp(getActionBarHeight(context).toFloat()).dp
keyboardPadding + spaceBetweenKeyboard - actionBarHeight
} else {
keyboardPadding + spaceBetweenKeyboard
}
keyboardPadding + spaceBetweenKeyboard
}
}
Box(modifier.padding(bottom = calculatedPadding)) { content() }
Box(modifier.padding(bottom = padding)) { content() }
}

View file

@ -18,11 +18,13 @@ sealed class TokensAction : Action {
data class SetAddedCurrencies(
val wallets: List<WalletData>, val derivationStyle: DerivationStyle?
) : TokensAction()
) : TokensAction()
data class SetNonRemovableCurrencies(val wallets: List<WalletData>) : TokensAction()
data class SaveChanges(
val addedTokens: List<TokenWithBlockchain>,
val addedBlockchains: List<Blockchain>
) : TokensAction()
object PrepareAndNavigateToAddCustomToken : TokensAction()
}

View file

@ -8,16 +8,22 @@ import com.tangem.common.card.EllipticCurve
import com.tangem.common.extensions.ByteArrayKey
import com.tangem.common.extensions.toMapKey
import com.tangem.common.hdWallet.DerivationPath
import com.tangem.domain.DomainWrapped
import com.tangem.domain.common.KeyWalletPublicKey
import com.tangem.domain.common.ScanResponse
import com.tangem.domain.common.TapWorkarounds.derivationStyle
import com.tangem.domain.common.TapWorkarounds.isTestCard
import com.tangem.domain.features.addCustomToken.CompleteData
import com.tangem.domain.features.addCustomToken.redux.AddCustomTokenAction
import com.tangem.domain.features.addCustomToken.redux.AddedCurrencies
import com.tangem.domain.redux.domainStore
import com.tangem.operations.derivation.ExtendedPublicKeysMap
import com.tangem.tap.*
import com.tangem.tap.common.extensions.dispatchErrorNotification
import com.tangem.tap.common.extensions.dispatchOnMain
import com.tangem.tap.common.redux.AppState
import com.tangem.tap.common.redux.global.GlobalAction
import com.tangem.tap.common.redux.navigation.AppScreen
import com.tangem.tap.common.redux.navigation.NavigationAction
import com.tangem.tap.domain.TapError
import com.tangem.tap.domain.extensions.makeWalletManagerForApp
@ -26,6 +32,7 @@ import com.tangem.tap.features.wallet.redux.WalletAction
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import org.rekotlin.Middleware
import timber.log.Timber
class TokensMiddleware {
@ -35,6 +42,7 @@ class TokensMiddleware {
when (action) {
is TokensAction.LoadCurrencies -> handleLoadCurrencies(action)
is TokensAction.SaveChanges -> handleSaveChanges(action)
is TokensAction.PrepareAndNavigateToAddCustomToken -> handleAddingCustomToken(action)
}
next(action)
}
@ -80,6 +88,22 @@ class TokensMiddleware {
}
}
private fun handleAddingCustomToken(action: TokensAction.PrepareAndNavigateToAddCustomToken) {
val tokensState = store.state.tokensState
val addedTokensList = tokensState.addedTokens.map {
DomainWrapped.TokenWithBlockchain(it.token.copy(), it.blockchain)
}
val addedBlockchains = tokensState.addedBlockchains.map { it }
val addedCurrencies = AddedCurrencies(addedTokensList, addedBlockchains)
domainStore.dispatch(AddCustomTokenAction.Init.SetAddedCurrencies(addedCurrencies))
val callback = fun(data: CompleteData) {
Timber.e("Yoooohhhoooo")
}
domainStore.dispatch(AddCustomTokenAction.Init.SetOnAddTokenCallback(callback))
store.dispatch(NavigationAction.NavigateTo(AppScreen.AddCustomToken))
}
private fun deriveMissingBlockchains(
scanResponse: ScanResponse,
blockchains: List<Blockchain>,

View file

@ -3,8 +3,8 @@ package com.tangem.tap.features.tokens.redux
import com.tangem.blockchain.common.Blockchain
import com.tangem.blockchain.common.DerivationStyle
import com.tangem.blockchain.common.Token
import com.tangem.domain.common.extensions.fromNetworkId
import com.tangem.tap.domain.tokens.Currency
import com.tangem.tap.domain.tokens.fromNetworkId
import com.tangem.tap.features.wallet.redux.WalletData
import org.rekotlin.StateType

View file

@ -103,6 +103,10 @@ class AddTokensFragment : Fragment(R.layout.fragment_add_tokens),
override fun onOptionsItemSelected(item: MenuItem): Boolean {
return when (item.itemId) {
R.id.menu_search -> true
R.id.menu_navigate_add_custom_token -> {
store.dispatch(TokensAction.PrepareAndNavigateToAddCustomToken)
true
}
else -> super.onOptionsItemSelected(item)
}
}

View file

@ -16,10 +16,10 @@ import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import coil.compose.SubcomposeAsyncImage
import com.tangem.blockchain.common.Blockchain
import com.tangem.domain.common.extensions.fromNetworkId
import com.tangem.tap.common.extensions.getGreyedOutIconRes
import com.tangem.tap.common.extensions.getRoundIconRes
import com.tangem.tap.domain.tokens.Currency
import com.tangem.tap.domain.tokens.fromNetworkId
import com.tangem.tap.features.tokens.redux.TokenWithBlockchain
import com.tangem.wallet.R

View file

@ -13,11 +13,11 @@ import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import com.tangem.blockchain.common.Blockchain
import com.tangem.domain.common.extensions.fromNetworkId
import com.tangem.tap.common.compose.Keyboard
import com.tangem.tap.common.compose.keyboardAsState
import com.tangem.tap.common.extensions.pixelsToDp
import com.tangem.tap.domain.tokens.Currency
import com.tangem.tap.domain.tokens.fromNetworkId
import com.tangem.tap.features.tokens.redux.ContractAddress
import com.tangem.tap.features.tokens.redux.TokenWithBlockchain
import com.tangem.tap.features.tokens.redux.TokensState

View file

@ -18,8 +18,8 @@ import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import coil.compose.SubcomposeAsyncImage
import com.tangem.blockchain.common.Blockchain
import com.tangem.domain.common.extensions.fromNetworkId
import com.tangem.tap.domain.tokens.Currency
import com.tangem.tap.domain.tokens.fromNetworkId
import com.tangem.tap.features.tokens.redux.ContractAddress
import com.tangem.tap.features.tokens.redux.TokenWithBlockchain
import com.tangem.wallet.R