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

View file

@ -9,4 +9,11 @@
app:actionViewClass="androidx.appcompat.widget.SearchView"
app:showAsAction="always" />
<item
android:id="@+id/menu_navigate_add_custom_token"
android:icon="@drawable/ic_add"
android:title="@string/common_add"
app:iconTint="@color/darkGray2"
app:showAsAction="always" />
</menu>

View file

@ -17,8 +17,17 @@
<color name="warning">#FFB71B</color>
<color name="white">#FFFFFF</color>
<color name="darkGray1">#8E8E93</color>
<color name="darkGray2">#636366</color>
<color name="lightGray0">#F3F3F3</color>
<color name="lightGray1">#F8F8FB</color>
<color name="lightGray2">#DADADF</color>
<color name="lightGray3">#D0D0D5</color>
<color name="lightGray4">#C7C7CC</color>
<color name="lightGray5">#D1D1D6</color>
<color name="lightGray6">#CACACC</color>
<color name="darkGray0">#B6B6B8</color>
<color name="darkGray1">#8E8E90</color>
<color name="darkGray2">#666668</color>
<color name="darkGray3">#48484A</color>
<color name="darkGray4">#3A3A3C</color>
<color name="darkGray5">#2C2C2E</color>
@ -37,14 +46,6 @@
<color name="twins_dark">#14181D</color>
<color name="lightGray0">#F3F3F3</color>
<color name="lightGray1">#F8F8FB</color>
<color name="lightGray2">#DADADF</color>
<color name="lightGray3">#D0D0D5</color>
<color name="lightGray4">#C7C7CC</color>
<color name="lightGray5">#D1D1D6</color>
<color name="lightGray6">#C9C9CD</color>
<color name="separatorGrey1">#1F000000</color>
<color name="separatorGrey2">#14212121</color>

View file

@ -6,7 +6,7 @@ package com.tangem.domain
* @property message the error description
* @property data any data that can help in the part where this error is being handled
*/
interface DomainError {
interface DomainError : DomainMessage {
val code: Int
val message: String
val data: Any?

View file

@ -10,4 +10,8 @@ sealed class DomainException(message: String?) : Throwable(message), DomainInter
data class SelectTokeNetworkException(val networkId: String) : DomainException(
"Unknown network [$networkId] should not be included in the network selection dialog."
)
data class UnAppropriateInitializationException(val of: String, val info: String? = null) : DomainException(
"The [$of], must be properly initialized. Info []"
)
}

View file

@ -0,0 +1,15 @@
package com.tangem.domain
/**
[REDACTED_AUTHOR]
*/
sealed interface DomainMessage
sealed interface DomainNotification : DomainMessage {
interface Toast : DomainNotification {}
interface Snackbar : DomainNotification {}
interface Dialog : DomainNotification {}
}

View file

@ -10,6 +10,8 @@ interface DomainStateDialog
sealed class DomainDialog : DomainStateDialog {
data class DialogError(val error: DomainError) : DomainDialog()
data class SelectTokenDialog(
val items: List<Coins.CheckAddressResponse.Token.Contract>,
val networkIdConverter: (String) -> String,

View file

@ -0,0 +1,18 @@
package com.tangem.domain
import com.tangem.blockchain.common.Blockchain
import com.tangem.blockchain.common.Token
/**
[REDACTED_AUTHOR]
* Provides a temporary copies of the app module classes, data structures, etc.
*/
//TODO: refactoring: : after refactoring they should be unwrapped and moved
// to appropriate parts of module
sealed interface DomainWrapped {
data class TokenWithBlockchain(
val token: Token,
val blockchain: Blockchain
)
}

View file

@ -0,0 +1,60 @@
package com.tangem.domain.common.extensions
import com.tangem.blockchain.common.Blockchain
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

@ -1,8 +1,8 @@
package com.tangem.domain.features.addCustomToken
import com.tangem.blockchain.common.Blockchain
import com.tangem.blockchain.common.Token
import com.tangem.domain.common.form.BaseFieldDataConverter
import com.tangem.domain.common.form.FieldDataConverter
import com.tangem.domain.common.form.FieldId
/**
@ -14,47 +14,44 @@ enum class CompleteDataType {
sealed class CompleteData() {
companion object {
fun createDataConverter(completeDataType: CompleteDataType): FieldDataConverter<out CompleteData> =
when (completeDataType) {
CompleteDataType.Blockchain -> CustomBlockchain.Converter()
CompleteDataType.Token -> CustomToken.Converter()
}
}
class CustomBlockchain(
val selectedNetwork: Blockchain,
val network: Blockchain,
val derivationPath: String?
) : CompleteData() {
class Converter : BaseFieldDataConverter<CustomBlockchain>() {
override fun getConvertedData(): CustomBlockchain = CustomBlockchain(
collectedData[CustomTokenFieldId.Network] as Blockchain,
collectedData[CustomTokenFieldId.DerivationPath] as? String,
)
override fun getConvertedData(): CustomBlockchain {
val network = collectedData[CustomTokenFieldId.Network] as Blockchain
val derivationPath = collectedData[CustomTokenFieldId.DerivationPath] as? String
return CustomBlockchain(network, derivationPath)
}
override fun getIdToCollect(): List<FieldId> = listOf(CustomTokenFieldId.Network, CustomTokenFieldId.DerivationPath)
}
}
class CustomToken(
val contractAddress: String,
val selectedNetwork: Blockchain,
val name: String,
val tokenSymbol: String,
val decimals: Int,
val token: Token,
val network: Blockchain,
val derivationPath: String?,
) : CompleteData() {
class Converter : BaseFieldDataConverter<CustomToken>() {
override fun getConvertedData(): CustomToken = CustomToken(
collectedData[CustomTokenFieldId.ContractAddress] as String,
collectedData[CustomTokenFieldId.Network] as Blockchain,
collectedData[CustomTokenFieldId.Name] as String,
collectedData[CustomTokenFieldId.Symbol] as String,
collectedData[CustomTokenFieldId.Decimals] as Int,
collectedData[CustomTokenFieldId.DerivationPath] as? String,
)
class Converter(val tokenId: String?) : BaseFieldDataConverter<CustomToken>() {
override fun getConvertedData(): CustomToken {
val token = Token(
name = collectedData[CustomTokenFieldId.Name] as String,
symbol = collectedData[CustomTokenFieldId.Symbol] as String,
contractAddress = collectedData[CustomTokenFieldId.ContractAddress] as String,
decimals = (collectedData[CustomTokenFieldId.Decimals] as String).toInt(),
id = tokenId,
)
return CustomToken(
token,
collectedData[CustomTokenFieldId.Network] as Blockchain,
collectedData[CustomTokenFieldId.DerivationPath] as? String,
)
}
override fun getIdToCollect(): List<FieldId> = CustomTokenFieldId.values().toList()
}

View file

@ -13,13 +13,13 @@ sealed class AddCustomTokenError : AnError(ERROR_CODE_ADD_CUSTOM_TOKEN, "Add cus
object NetworkIsNotSelected : AddCustomTokenError()
object InvalidDecimalsCount : AddCustomTokenError()
object InvalidDerivationPath : AddCustomTokenError()
}
sealed class AddCustomTokenWarning : AnError(ERROR_CODE_ADD_CUSTOM_TOKEN, "Add custom token - warning") {
object PotentialScamToken : AddCustomTokenWarning()
object TokenAlreadyAdded : AddCustomTokenWarning()
sealed class Network : AddCustomTokenWarning() {
object CheckAddressRequestError : Network()
}
}
sealed class AddCustomTokenWarning : AddCustomTokenError() {
object PotentialScamToken : AddCustomTokenWarning()
object TokenAlreadyAdded : AddCustomTokenWarning()
}

View file

@ -8,7 +8,7 @@ import com.tangem.network.common.AddHeaderInterceptor
/**
[REDACTED_AUTHOR]
*/
class AddCustomTokenManager(
class TangemTechServiceManager(
private val tangemTechService: TangemTechService
) {

View file

@ -6,6 +6,7 @@ import com.tangem.domain.common.form.Field
import com.tangem.domain.common.form.FieldId
import com.tangem.domain.features.addCustomToken.AddCustomTokenError
import com.tangem.domain.features.addCustomToken.AddCustomTokenWarning
import com.tangem.domain.features.addCustomToken.CompleteData
import com.tangem.domain.features.addCustomToken.CustomTokenFieldId
import com.tangem.network.api.tangemTech.Coins
import org.rekotlin.Action
@ -14,6 +15,12 @@ import org.rekotlin.Action
[REDACTED_AUTHOR]
*/
sealed class AddCustomTokenAction : Action {
sealed class Init : AddCustomTokenAction() {
data class SetAddedCurrencies(val addedCurrencies: AddedCurrencies) : AddCustomTokenAction()
data class SetOnAddTokenCallback(val callback: (CompleteData) -> Unit) : AddCustomTokenAction()
}
object OnCreate : AddCustomTokenAction() {
data class SetDerivationStyle(val derivationStyle: DerivationStyle?) : AddCustomTokenAction()
}
@ -27,10 +34,12 @@ sealed class AddCustomTokenAction : Action {
data class OnTokenSymbolChanged(val tokenSymbol: Field.Data<String>) : AddCustomTokenAction()
data class OnTokenDerivationPathChanged(val blockchainDerivationPath: Field.Data<Blockchain>) : AddCustomTokenAction()
data class OnTokenDecimalsChanged(val tokenDecimals: Field.Data<String>) : AddCustomTokenAction()
object OnAddCustomTokenClicked : AddCustomTokenAction()
// form fields
data class UpdateForm(val state: AddCustomTokenState) : AddCustomTokenAction()
object ClearTokenFields : AddCustomTokenAction()
data class FillTokenFields(
val token: Coins.CheckAddressResponse.Token,
val contract: Coins.CheckAddressResponse.Token.Contract,
@ -41,6 +50,8 @@ sealed class AddCustomTokenAction : Action {
data class Remove(val id: CustomTokenFieldId) : FieldError()
}
data class SetTokenId(val id: String) : AddCustomTokenAction()
// warnings
sealed class Warning : AddCustomTokenAction() {
data class Add(val warnings: Set<AddCustomTokenWarning>) : Warning()

View file

@ -2,12 +2,14 @@ package com.tangem.domain.features.addCustomToken.redux
import android.webkit.ValueCallback
import com.tangem.blockchain.common.Blockchain
import com.tangem.common.card.Card
import com.tangem.common.extensions.guard
import com.tangem.common.extensions.toHexString
import com.tangem.common.services.Result
import com.tangem.domain.DomainDialog
import com.tangem.domain.DomainException
import com.tangem.domain.common.TapWorkarounds.derivationStyle
import com.tangem.domain.common.extensions.fromNetworkId
import com.tangem.domain.common.extensions.toNetworkId
import com.tangem.domain.common.form.*
import com.tangem.domain.features.addCustomToken.*
import com.tangem.domain.features.addCustomToken.CustomTokenFieldId.*
@ -18,7 +20,9 @@ import com.tangem.domain.redux.dispatchOnMain
import com.tangem.domain.redux.domainStore
import com.tangem.domain.redux.global.DomainGlobalAction
import com.tangem.network.api.tangemTech.Coins
import com.tangem.network.api.tangemTech.TangemTechService
import kotlinx.coroutines.cancel
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import org.rekotlin.Action
import timber.log.Timber
@ -31,9 +35,7 @@ internal class AddCustomTokenHub : BaseStoreHub<AddCustomTokenState>("AddCustomT
private val hubState: AddCustomTokenState
get() = domainStore.state.addCustomTokensState
override fun getHubState(storeState: DomainState): AddCustomTokenState {
return storeState.addCustomTokensState
}
override fun getHubState(storeState: DomainState): AddCustomTokenState = hubState
override fun updateStoreState(storeState: DomainState, newHubState: AddCustomTokenState): DomainState {
return storeState.copy(addCustomTokensState = newHubState)
@ -45,47 +47,51 @@ internal class AddCustomTokenHub : BaseStoreHub<AddCustomTokenState>("AddCustomT
cancel: ValueCallback<Action>
) {
if (action !is AddCustomTokenAction) return
val card = storeState.globalState.scanResponse?.card
?: throw IllegalStateException("ScanResponse must be set before showing the AddCustomToken screen")
when (action) {
is Init.SetAddedCurrencies -> {}
is Init.SetOnAddTokenCallback -> {}
is OnCreate -> {
hubState.addCustomTokenManager.attachAuthKey(card.cardPublicKey.toHexString())
dispatchOnMain(OnCreate.SetDerivationStyle(card.derivationStyle))
hubState.addedCurrencies.guard {
return throwUnAppropriateInitialization("addedTokens")
}
}
is OnDestroy -> hubScope.cancel()
is OnTokenContractAddressChanged -> {
dispatchOnMain(
Screen.UpdateAddButton(
ViewStates.AddButton(!hubState.allFieldsIsEmpty())
)
)
val contractAddress = action.contractAddress.value
val validator: TokenContractAddressValidator = getValidator(ContractAddress, hubState)
val validator: TokenContractAddressValidator = hubState.getValidator(ContractAddress)
val error = validator.validate(contractAddress)
addOrRemoveError(ContractAddress, error)
if (error != null || contractAddress.isEmpty()) {
dispatchOnMain(actionsUnlockTokenFields())
dispatchOnMain(unlockTokenFields())
return
}
if (!action.contractAddress.isUserInput) return
val foundTokens = requestInfoAboutContractAddress(contractAddress, hubState)
manageTokenChanges(null, foundTokens)
manageTokenChanges(requestInfoAboutContractAddress(contractAddress))
}
is OnTokenNetworkChanged -> {
if (!action.blockchainNetwork.isUserInput) return
val contractAddress = getField<TokenField>(ContractAddress, hubState).data.value
val foundTokens = requestInfoAboutContractAddress(contractAddress, hubState)
manageTokenChanges(null, foundTokens)
val contractAddress = hubState.getField<TokenField>(ContractAddress).data.value
manageTokenChanges(requestInfoAboutContractAddress(contractAddress))
}
is OnTokenNameChanged -> {
val validator: TokenNameValidator = getValidator(Name, hubState)
val validator: TokenNameValidator = hubState.getValidator(Name)
addOrRemoveError(Name, validator.validate(action.tokenName.value))
}
is OnTokenSymbolChanged -> {
val validator: TokenSymbolValidator = getValidator(Symbol, hubState)
val validator: TokenSymbolValidator = hubState.getValidator(Symbol)
addOrRemoveError(Symbol, validator.validate(action.tokenSymbol.value))
}
is OnTokenDecimalsChanged -> {
val validator: TokenDecimalsValidator = getValidator(Decimals, hubState)
val validator: TokenDecimalsValidator = hubState.getValidator(Decimals)
addOrRemoveError(Decimals, validator.validate(action.tokenDecimals.value))
}
// is OnTokenDerivationPathChanged -> {
@ -93,9 +99,9 @@ internal class AddCustomTokenHub : BaseStoreHub<AddCustomTokenState>("AddCustomT
// addOrRemoveError(DerivationPath, validator.validate(action.value.value))
// }
is ClearTokenFields -> {
val nameField = getField<TokenField>(Name, hubState)
val symbolField = getField<TokenField>(Symbol, hubState)
val decimalsField = getField<TokenField>(Decimals, hubState)
val nameField = hubState.getField<TokenField>(Name)
val symbolField = hubState.getField<TokenField>(Symbol)
val decimalsField = hubState.getField<TokenField>(Decimals)
nameField.data = Field.Data("", false)
symbolField.data = Field.Data("", false)
@ -104,14 +110,14 @@ internal class AddCustomTokenHub : BaseStoreHub<AddCustomTokenState>("AddCustomT
dispatchOnMain(UpdateForm(hubState))
}
is FillTokenFields -> {
val networkField = getField<TokenBlockchainField>(Network, hubState)
val nameField = getField<TokenField>(Name, hubState)
val symbolField = getField<TokenField>(Symbol, hubState)
val decimalsField = getField<TokenField>(Decimals, hubState)
val networkField = hubState.getField<TokenBlockchainField>(Network)
val nameField = hubState.getField<TokenField>(Name)
val symbolField = hubState.getField<TokenField>(Symbol)
val decimalsField = hubState.getField<TokenField>(Decimals)
val token = action.token
val contract = action.contract
val blockchain = Blockchain.fromNetworkId(contract.networkId)
val blockchain = Blockchain.fromNetworkId(contract.networkId) ?: Blockchain.Unknown
networkField.data = Field.Data(blockchain, false)
nameField.data = Field.Data(token.name, false)
symbolField.data = Field.Data(token.symbol, false)
@ -119,23 +125,46 @@ internal class AddCustomTokenHub : BaseStoreHub<AddCustomTokenState>("AddCustomT
dispatchOnMain(UpdateForm(hubState))
}
is OnAddCustomTokenClicked -> {
// if (hubState.allFieldsIsEmpty()) {
dispatchOnMain(
DomainGlobalAction.ShowDialog(DomainDialog.DialogError(
AddCustomTokenError.FieldIsEmpty
)))
return
// }
when {
!hubState.customTokensFieldsIsEmpty() && !hubState.networkIsEmpty() -> {
hubState.getCompleteData(CompleteDataType.Token)
}
// !hubState.customTokensFieldsIsEmpty() && -> {
// }
}
// if (true) {
// dispatchOnMain(NavigationAction.PopBackTo())
// hubState.onTokenAddCallback?.invoke()
// }
}
else -> {}
}
}
private suspend fun requestInfoAboutContractAddress(
contractAddress: String,
hubState: AddCustomTokenState
): List<Coins.CheckAddressResponse.Token> {
val tangemTechServiceManager = requireNotNull(hubState.tangemTechServiceManager)
dispatchOnMain(Screen.UpdateTokenFields(listOf(ContractAddress to ViewStates.TokenField(isLoading = true))))
val tokenManager = hubState.addCustomTokenManager
val field = getField<TokenBlockchainField>(Network, hubState)
val field = hubState.getField<TokenBlockchainField>(Network)
val selectedNetworkId: String? = field.data.value.let {
if (it == Blockchain.Unknown) null else it
}?.toNetworkId()
// delay(1000)
val result = when (val foundTokensResult = tokenManager.checkAddress(contractAddress, selectedNetworkId)) {
// simulate loading effect. It would be better if the delay would only run if tokenManager.checkAddress()
// got the result faster than 500ms and the delay would only be the difference between them.
delay(500)
val foundTokensResult = tangemTechServiceManager.checkAddress(contractAddress, selectedNetworkId)
val result = when (foundTokensResult) {
is Result.Success -> foundTokensResult.data
is Result.Failure -> {
// val warning = AddCustomTokenWarning.Network.CheckAddressRequestError
@ -147,10 +176,7 @@ internal class AddCustomTokenHub : BaseStoreHub<AddCustomTokenState>("AddCustomT
return result
}
private suspend fun manageTokenChanges(
card: Card?,
foundTokens: List<Coins.CheckAddressResponse.Token>,
) {
private suspend fun manageTokenChanges(foundTokens: List<Coins.CheckAddressResponse.Token>) {
val toAddWarnings = mutableSetOf<AddCustomTokenWarning>()
val toRemoveWarnings = mutableSetOf<AddCustomTokenWarning>()
@ -159,7 +185,7 @@ internal class AddCustomTokenHub : BaseStoreHub<AddCustomTokenState>("AddCustomT
toAddWarnings.add(AddCustomTokenWarning.PotentialScamToken)
toRemoveWarnings.add(AddCustomTokenWarning.TokenAlreadyAdded)
dispatchOnMain(ClearTokenFields)
dispatchOnMain(actionsUnlockTokenFields())
dispatchOnMain(unlockTokenFields())
}
else -> {
val token = foundTokens[0]
@ -178,7 +204,7 @@ internal class AddCustomTokenHub : BaseStoreHub<AddCustomTokenState>("AddCustomT
toRemoveWarnings.add(AddCustomTokenWarning.PotentialScamToken)
dispatchOnMain(Screen.UpdateAddButton(ViewStates.AddButton(false)))
dispatchOnMain(actionsLockTokenFields())
dispatchOnMain(lockTokenFields())
} else {
toRemoveWarnings.add(AddCustomTokenWarning.TokenAlreadyAdded)
dispatchOnMain(Screen.UpdateAddButton(ViewStates.AddButton(true)))
@ -188,11 +214,11 @@ internal class AddCustomTokenHub : BaseStoreHub<AddCustomTokenState>("AddCustomT
if (tokenContract.active && isStandardDerivation) {
toRemoveWarnings.add(AddCustomTokenWarning.PotentialScamToken)
dispatchOnMain(FillTokenFields(token, contract))
dispatchOnMain(actionsLockTokenFields())
dispatchOnMain(lockTokenFields())
} else {
toAddWarnings.add(AddCustomTokenWarning.PotentialScamToken)
dispatchOnMain(ClearTokenFields)
dispatchOnMain(actionsUnlockTokenFields())
dispatchOnMain(unlockTokenFields())
}
}
}
@ -201,7 +227,7 @@ internal class AddCustomTokenHub : BaseStoreHub<AddCustomTokenState>("AddCustomT
items = contracts,
networkIdConverter = { networkId ->
val blockchain = Blockchain.fromNetworkId(networkId)
if (blockchain == Blockchain.Unknown) {
if (blockchain == null || blockchain == Blockchain.Unknown) {
throw DomainException.SelectTokeNetworkException(networkId)
}
hubState.convertBlockchainName(blockchain, "")
@ -210,7 +236,7 @@ internal class AddCustomTokenHub : BaseStoreHub<AddCustomTokenState>("AddCustomT
hubScope.launch {
// find how to connect to the upper coroutineContext and dispatch through them
dispatchOnMain(FillTokenFields(token, selectedContract))
dispatchOnMain(actionsLockTokenFields())
dispatchOnMain(lockTokenFields())
}
},
)
@ -238,7 +264,7 @@ internal class AddCustomTokenHub : BaseStoreHub<AddCustomTokenState>("AddCustomT
}
}
private fun actionsLockTokenFields(): Action {
private fun lockTokenFields(): Action {
val state = hubState
return Screen.UpdateTokenFields(listOf(
Network to state.screenState.network.copy(isEnabled = false),
@ -248,7 +274,7 @@ internal class AddCustomTokenHub : BaseStoreHub<AddCustomTokenState>("AddCustomT
))
}
private fun actionsUnlockTokenFields(): Action {
private fun unlockTokenFields(): Action {
val state = hubState
return Screen.UpdateTokenFields(listOf(
Network to state.screenState.network.copy(isEnabled = true),
@ -258,46 +284,54 @@ internal class AddCustomTokenHub : BaseStoreHub<AddCustomTokenState>("AddCustomT
))
}
private inline fun <reified T> getField(id: FieldId, state: AddCustomTokenState): T {
return state.form.getField(id) as T
}
private inline fun <reified T> getValidator(id: FieldId, state: AddCustomTokenState): T {
return state.getValidator(id) as T
}
override fun reduceAction(action: Action, state: AddCustomTokenState): AddCustomTokenState {
return when (action) {
is Init.SetAddedCurrencies -> {
state.copy(addedCurrencies = action.addedCurrencies)
}
is Init.SetOnAddTokenCallback -> {
state.copy(onTokenAddCallback = action.callback)
}
is OnCreate -> {
val card = requireNotNull(globalState.scanResponse?.card)
val tangemTechServiceManager = TangemTechServiceManager(TangemTechService())
tangemTechServiceManager.attachAuthKey(card.cardPublicKey.toHexString())
state.copy(
derivationStyle = card.derivationStyle,
tangemTechServiceManager = tangemTechServiceManager
)
}
is OnDestroy -> state.reset()
is UpdateForm -> {
updateFormState(action.state)
}
is OnTokenContractAddressChanged -> {
val field: TokenField = getField(ContractAddress, state)
val field: TokenField = state.getField(ContractAddress)
field.data = action.contractAddress
updateFormState(state)
}
is OnTokenNetworkChanged -> {
val field: TokenBlockchainField = getField(Network, state)
val field: TokenBlockchainField = state.getField(Network)
field.data = action.blockchainNetwork
updateFormState(state)
}
is OnTokenNameChanged -> {
val field: TokenField = getField(Name, state)
val field: TokenField = state.getField(Name)
field.data = action.tokenName
updateFormState(state)
}
is OnTokenSymbolChanged -> {
val field: TokenField = getField(Symbol, state)
val field: TokenField = state.getField(Symbol)
field.data = action.tokenSymbol
updateFormState(state)
}
is OnTokenDecimalsChanged -> {
val field: TokenField = getField(Decimals, state)
val field: TokenField = state.getField(Decimals)
field.data = action.tokenDecimals
updateFormState(state)
}
is OnTokenDerivationPathChanged -> {
val field: TokenDerivationPathField = getField(DerivationPath, state)
val field: TokenDerivationPathField = state.getField(DerivationPath)
field.data = action.blockchainDerivationPath
updateFormState(state)
}
@ -309,6 +343,9 @@ internal class AddCustomTokenHub : BaseStoreHub<AddCustomTokenState>("AddCustomT
val newMap = state.formErrors.toMutableMap().apply { remove(action.id) }
state.copy(formErrors = newMap)
}
is SetTokenId -> {
state.copy(tokenId = action.id)
}
is Warning.Add -> {
val newList = state.warnings.toMutableSet().apply { addAll(action.warnings) }
state.copy(warnings = newList.toSet())
@ -391,62 +428,11 @@ internal class AddCustomTokenHub : BaseStoreHub<AddCustomTokenState>("AddCustomT
private fun updateFormState(state: AddCustomTokenState): AddCustomTokenState {
return state.copy(form = Form(state.form.fieldList))
}
}
//TODO: refactoring: replace by Blockchain.Companion.fromNetworkId
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 -> Blockchain.Unknown
}
}
fun Blockchain.toNetworkId(): String? {
return when (this) {
Blockchain.Unknown -> null
Blockchain.Avalanche -> "avalanche"
Blockchain.AvalancheTestnet -> "avalanche"
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"
@Throws
private fun throwUnAppropriateInitialization(objName: String) {
throw DomainException.UnAppropriateInitializationException(
"AddCustomTokenHub", "$objName must be not NULL"
)
}
}

View file

@ -5,35 +5,39 @@ import com.tangem.blockchain.common.DerivationStyle
import com.tangem.domain.common.form.*
import com.tangem.domain.features.addCustomToken.*
import com.tangem.domain.features.addCustomToken.CustomTokenFieldId.*
import com.tangem.network.api.tangemTech.TangemTechService
import org.rekotlin.StateType
data class AddCustomTokenState(
val addedCurrencies: AddedCurrencies? = null,
val onTokenAddCallback: ((CompleteData) -> Unit)? = null,
val derivationStyle: DerivationStyle? = null,
val form: Form = Form(createFormFields()),
val formValidators: Map<CustomTokenFieldId, CustomTokenValidator<*>> = createFormValidators(),
val formValidators: Map<CustomTokenFieldId, CustomTokenValidator<out Any>> = createFormValidators(),
val formErrors: Map<CustomTokenFieldId, AddCustomTokenError> = emptyMap(),
val tokenId: String? = null,
val warnings: Set<AddCustomTokenWarning> = emptySet(),
val screenState: ScreenState = createInitialScreenState(),
val addCustomTokenManager: AddCustomTokenManager = AddCustomTokenManager(TangemTechService()),
val derivationStyle: DerivationStyle? = null
val tangemTechServiceManager: TangemTechServiceManager? = null
) : StateType {
val completeDataType: CompleteDataType
get() = calculateDataType()
inline fun <reified T> getField(id: FieldId): T = form.getField(id) as T
inline fun <reified T> getValidator(id: FieldId): T = formValidators[id] as T
fun getError(id: FieldId): AddCustomTokenError? = formErrors[id]
fun hasError(id: FieldId): Boolean = formErrors[id] != null
fun getCompleteData(type: CompleteDataType): CompleteData = when (type) {
CompleteDataType.Token -> getToken()
CompleteDataType.Blockchain -> getBlockchain()
}
inline fun <reified T> visitDataConverter(converter: FieldDataConverter<T>): T {
form.visitDataConverter(converter)
return converter.getConvertedData()
}
fun getValidator(id: FieldId): CustomTokenValidator<*> = formValidators[id]!!
fun hasError(id: FieldId): Boolean = formErrors[id] != null
fun getError(id: FieldId): AddCustomTokenError? {
return formErrors[id]
}
fun convertBlockchainName(blockchain: Blockchain, unknown: String): String = when (blockchain) {
Blockchain.Unknown -> unknown
else -> blockchain.fullName
@ -43,18 +47,49 @@ data class AddCustomTokenState(
return blockchain.derivationPath(derivationStyle)?.rawPath ?: unknown
}
private fun calculateDataType(): CompleteDataType {
fun reset(): AddCustomTokenState {
return this.copy(
addedCurrencies = null,
onTokenAddCallback = null,
derivationStyle = null,
form = Form(createFormFields()),
formErrors = emptyMap(),
tokenId = null,
warnings = emptySet(),
screenState = createInitialScreenState(),
tangemTechServiceManager = null,
)
}
fun networkIsEmpty(): Boolean {
val network = getField<TokenBlockchainField>(Network)
return network.data.value != Blockchain.Unknown
}
fun customTokensFieldsIsEmpty(): Boolean {
val idsToCheck = listOf(ContractAddress, Name, Symbol, Decimals)
val fieldsToCheck = form.fieldList.filter { idsToCheck.contains(it.id) }
val isEmptyValidator = StringIsEmptyValidator()
fieldsToCheck.map { data -> data.toString() }.forEach {
// if one of the fields has error -> then it
val error = isEmptyValidator.validate(it)
if (error != null) return CompleteDataType.Token
val validator = StringIsEmptyValidator()
// val errors = mutableMapOf<>()
fieldsToCheck.forEach { field ->
val error = validator.validate(field.data.value?.toString())
if (error != null) return true
}
return false
}
return CompleteDataType.Blockchain
fun allFieldsIsEmpty(): Boolean = networkIsEmpty() && customTokensFieldsIsEmpty()
private fun getToken(): CompleteData.CustomToken {
return CompleteData.CustomToken.Converter(tokenId)
.apply { visitDataConverter(this) }
.getConvertedData()
}
private fun getBlockchain(): CompleteData.CustomBlockchain {
return CompleteData.CustomBlockchain.Converter()
.apply { visitDataConverter(this) }
.getConvertedData()
}
companion object {
@ -69,7 +104,7 @@ data class AddCustomTokenState(
)
}
private fun createFormValidators(): Map<CustomTokenFieldId, CustomTokenValidator<*>> {
private fun createFormValidators(): Map<CustomTokenFieldId, CustomTokenValidator<out Any>> {
return mapOf(
ContractAddress to TokenContractAddressValidator(),
Network to TokenNetworkValidator(),

View file

@ -1,5 +1,8 @@
package com.tangem.domain.features.addCustomToken.redux
import com.tangem.blockchain.common.Blockchain
import com.tangem.domain.DomainWrapped
/**
[REDACTED_AUTHOR]
*/
@ -24,4 +27,9 @@ sealed class ViewStates {
data class AddButton(
val isEnabled: Boolean = true
) : ViewStates()
}
}
data class AddedCurrencies(
val addedTokens: List<DomainWrapped.TokenWithBlockchain>,
val addedBlockchains: List<Blockchain>
)

View file

@ -4,6 +4,7 @@ import android.webkit.ValueCallback
import com.tangem.domain.common.FeatureCoroutineExceptionHandler
import com.tangem.domain.common.extensions.withIOContext
import com.tangem.domain.common.extensions.withMainContext
import com.tangem.domain.redux.global.DomainGlobalState
import kotlinx.coroutines.*
import org.rekotlin.Action
import org.rekotlin.DispatchFunction
@ -32,14 +33,20 @@ internal interface HubReducer<StoreState> {
* All action went from the middleware must be dispatched through ReStoreHub.dispatchOnMain(Actions) to prevent
* concurrent modification in the Store
* Only the changed hub State will change its state in the DomainState
* Do not implement other states like as DomainGlobalState. Because it can dilute the responsibility of
* states.
* @param name - name of the Hub
* @param dispatcher - main coroutine dispatcher for actions
* @property globalState - state witch produce accessibility to global variables
*/
internal abstract class BaseStoreHub<State>(
private val name: String,
private val dispatcher: CoroutineDispatcher = Executors.newFixedThreadPool(1).asCoroutineDispatcher()
) : ReStoreHub<DomainState, State> {
val globalState: DomainGlobalState
get() = domainStore.state.globalState
val hubScope = CoroutineScope(
Job() + dispatcher + CoroutineName(name) + FeatureCoroutineExceptionHandler.create(name)
)