From a07f371456eeab94146e4705bb46058a69a7daaa Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 14 Apr 2022 17:28:33 +0300 Subject: [PATCH] Updated on 2026-08-14 --- .../com/tangem/tap/common/compose/Button.kt | 4 +- .../common/compose/ComposeDialogManager.kt | 64 ++++-- .../tap/common/compose/KeyboardState.kt | 13 +- .../tap/common/compose/extensions/Context.kt | 19 ++ .../tap/common/extensions/Navigation.kt | 2 + .../tangem/tap/common/extensions/Picasso.kt | 2 +- .../redux/navigation/NavigationState.kt | 2 +- .../com/tangem/tap/domain/tokens/Currency.kt | 59 +---- ...orConverter.kt => DomainErrorConverter.kt} | 40 ++-- .../compose/AddCustomTokenScreen.kt | 206 +++++------------ .../addCustomToken/compose/DebugActions.kt | 4 +- .../addCustomToken/compose/FormFieldViews.kt | 134 +++++++++++ .../compose/HangingOverKeyboardView.kt | 31 +-- .../tap/features/tokens/redux/TokensAction.kt | 4 +- .../features/tokens/redux/TokensMiddleware.kt | 24 ++ .../tap/features/tokens/redux/TokensState.kt | 2 +- .../features/tokens/ui/AddTokensFragment.kt | 4 + .../ui/compose/CollapsedCurrencyItem.kt | 2 +- .../tokens/ui/compose/CurrenciesScreen.kt | 2 +- .../tokens/ui/compose/ExpandedCurrencyItem.kt | 2 +- app/src/main/res/menu/popular_tokens.xml | 7 + app/src/main/res/values/colors.xml | 21 +- .../java/com/tangem/domain/DomainError.kt | 2 +- .../java/com/tangem/domain/DomainException.kt | 4 + .../java/com/tangem/domain/DomainMessage.kt | 15 ++ .../com/tangem/domain/DomainStateDialog.kt | 2 + .../java/com/tangem/domain/DomainWrapped.kt | 18 ++ .../domain/common/extensions/Blockchain.kt | 60 +++++ .../features/addCustomToken/CompleteData.kt | 53 +++-- .../domain/features/addCustomToken/Errors.kt | 10 +- ...Manager.kt => TangemTechServiceManager.kt} | 2 +- .../redux/AddCustomTokenAction.kt | 11 + .../addCustomToken/redux/AddCustomTokenHub.kt | 212 ++++++++---------- .../redux/AddCustomTokenState.kt | 81 +++++-- .../features/addCustomToken/redux/Models.kt | 10 +- .../com/tangem/domain/redux/ReStoreHub.kt | 7 + 36 files changed, 666 insertions(+), 469 deletions(-) create mode 100644 app/src/main/java/com/tangem/tap/common/compose/extensions/Context.kt rename app/src/main/java/com/tangem/tap/features/tokens/addCustomToken/{CustomTokenErrorConverter.kt => DomainErrorConverter.kt} (71%) create mode 100644 app/src/main/java/com/tangem/tap/features/tokens/addCustomToken/compose/FormFieldViews.kt create mode 100644 domain/src/main/java/com/tangem/domain/DomainMessage.kt create mode 100644 domain/src/main/java/com/tangem/domain/DomainWrapped.kt create mode 100644 domain/src/main/java/com/tangem/domain/common/extensions/Blockchain.kt rename domain/src/main/java/com/tangem/domain/features/addCustomToken/{AddCustomTokenManager.kt => TangemTechServiceManager.kt} (98%) diff --git a/app/src/main/java/com/tangem/tap/common/compose/Button.kt b/app/src/main/java/com/tangem/tap/common/compose/Button.kt index 64b9daa152..992939febe 100644 --- a/app/src/main/java/com/tangem/tap/common/compose/Button.kt +++ b/app/src/main/java/com/tangem/tap/common/compose/Button.kt @@ -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 = {}) diff --git a/app/src/main/java/com/tangem/tap/common/compose/ComposeDialogManager.kt b/app/src/main/java/com/tangem/tap/common/compose/ComposeDialogManager.kt index 80cb2936ec..aa0e7e71d0 100644 --- a/app/src/main/java/com/tangem/tap/common/compose/ComposeDialogManager.kt +++ b/app/src/main/java/com/tangem/tap/common/compose/ComposeDialogManager.kt @@ -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) { +private fun ShowTheDialog(dialogState: MutableState) { 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 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 SimpleDialog( } } } -} \ No newline at end of file +} + +@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)) + } + } + ) +} diff --git a/app/src/main/java/com/tangem/tap/common/compose/KeyboardState.kt b/app/src/main/java/com/tangem/tap/common/compose/KeyboardState.kt index 2c8fc377be..2f08f3ae15 100644 --- a/app/src/main/java/com/tangem/tap/common/compose/KeyboardState.kt +++ b/app/src/main/java/com/tangem/tap/common/compose/KeyboardState.kt @@ -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 { val keyboardState: MutableState = 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 { diff --git a/app/src/main/java/com/tangem/tap/common/compose/extensions/Context.kt b/app/src/main/java/com/tangem/tap/common/compose/extensions/Context.kt new file mode 100644 index 0000000000..25ae94623c --- /dev/null +++ b/app/src/main/java/com/tangem/tap/common/compose/extensions/Context.kt @@ -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) +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/common/extensions/Navigation.kt b/app/src/main/java/com/tangem/tap/common/extensions/Navigation.kt index 5fdd3a9279..86d8393672 100644 --- a/app/src/main/java/com/tangem/tap/common/extensions/Navigation.kt +++ b/app/src/main/java/com/tangem/tap/common/extensions/Navigation.kt @@ -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() diff --git a/app/src/main/java/com/tangem/tap/common/extensions/Picasso.kt b/app/src/main/java/com/tangem/tap/common/extensions/Picasso.kt index 4799d802ad..dbeefadbe8 100644 --- a/app/src/main/java/com/tangem/tap/common/extensions/Picasso.kt +++ b/app/src/main/java/com/tangem/tap/common/extensions/Picasso.kt @@ -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( diff --git a/app/src/main/java/com/tangem/tap/common/redux/navigation/NavigationState.kt b/app/src/main/java/com/tangem/tap/common/redux/navigation/NavigationState.kt index 6f68439f61..1fc6832148 100644 --- a/app/src/main/java/com/tangem/tap/common/redux/navigation/NavigationState.kt +++ b/app/src/main/java/com/tangem/tap/common/redux/navigation/NavigationState.kt @@ -17,7 +17,7 @@ enum class AppScreen { Wallet, WalletDetails, Send, Details, DetailsConfirm, DetailsSecurity, - AddTokens, + AddTokens, AddCustomToken, WalletConnectSessions, QrScan } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/domain/tokens/Currency.kt b/app/src/main/java/com/tangem/tap/domain/tokens/Currency.kt index be3d1b4229..128190acd2 100644 --- a/app/src/main/java/com/tangem/tap/domain/tokens/Currency.kt +++ b/app/src/main/java/com/tangem/tap/domain/tokens/Currency.kt @@ -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" - } } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/tokens/addCustomToken/CustomTokenErrorConverter.kt b/app/src/main/java/com/tangem/tap/features/tokens/addCustomToken/DomainErrorConverter.kt similarity index 71% rename from app/src/main/java/com/tangem/tap/features/tokens/addCustomToken/CustomTokenErrorConverter.kt rename to app/src/main/java/com/tangem/tap/features/tokens/addCustomToken/DomainErrorConverter.kt index f151530a1b..89c205afee 100644 --- a/app/src/main/java/com/tangem/tap/features/tokens/addCustomToken/CustomTokenErrorConverter.kt +++ b/app/src/main/java/com/tangem/tap/features/tokens/addCustomToken/DomainErrorConverter.kt @@ -10,40 +10,40 @@ import com.tangem.wallet.R /** [REDACTED_AUTHOR] */ -class CustomTokenErrorConverter( +class DomainErrorConverter( + private val context: Context +) : ErrorConverter { + + 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 { 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 { - - 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}" } } } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/tokens/addCustomToken/compose/AddCustomTokenScreen.kt b/app/src/main/java/com/tangem/tap/features/tokens/addCustomToken/compose/AddCustomTokenScreen.kt index 473d4aca55..ad66229ed4 100644 --- a/app/src/main/java/com/tangem/tap/features/tokens/addCustomToken/compose/AddCustomTokenScreen.kt +++ b/app/src/main/java/com/tangem/tap/features/tokens/addCustomToken/compose/AddCustomTokenScreen.kt @@ -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) { 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) { } 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) { @Composable private fun FormFields(state: MutableState) { 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) { } @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) { +fun Warnings(warnings: List) { 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) { 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) { } @Composable -private fun AddButton( +private fun AddButton(state: MutableState) { + 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, @@ -284,7 +180,7 @@ private data class ScreenFieldData( fun fromState( field: DataField<*>, state: AddCustomTokenState, - errorConverter: CustomTokenErrorConverter + errorConverter: DomainErrorConverter ): ScreenFieldData { return ScreenFieldData( field = field, diff --git a/app/src/main/java/com/tangem/tap/features/tokens/addCustomToken/compose/DebugActions.kt b/app/src/main/java/com/tangem/tap/features/tokens/addCustomToken/compose/DebugActions.kt index 56ab3e1f37..18c1a5fc2e 100644 --- a/app/src/main/java/com/tangem/tap/features/tokens/addCustomToken/compose/DebugActions.kt +++ b/app/src/main/java/com/tangem/tap/features/tokens/addCustomToken/compose/DebugActions.kt @@ -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>() val contractAddresses = currencies.mapNotNull { currency -> diff --git a/app/src/main/java/com/tangem/tap/features/tokens/addCustomToken/compose/FormFieldViews.kt b/app/src/main/java/com/tangem/tap/features/tokens/addCustomToken/compose/FormFieldViews.kt new file mode 100644 index 0000000000..e9207987e7 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/features/tokens/addCustomToken/compose/FormFieldViews.kt @@ -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() +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/tokens/addCustomToken/compose/HangingOverKeyboardView.kt b/app/src/main/java/com/tangem/tap/features/tokens/addCustomToken/compose/HangingOverKeyboardView.kt index 0a8f736373..46e31151a8 100644 --- a/app/src/main/java/com/tangem/tap/features/tokens/addCustomToken/compose/HangingOverKeyboardView.kt +++ b/app/src/main/java/com/tangem/tap/features/tokens/addCustomToken/compose/HangingOverKeyboardView.kt @@ -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, - 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() } } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/tokens/redux/TokensAction.kt b/app/src/main/java/com/tangem/tap/features/tokens/redux/TokensAction.kt index 9582b3e9a7..d9bafa0dd3 100644 --- a/app/src/main/java/com/tangem/tap/features/tokens/redux/TokensAction.kt +++ b/app/src/main/java/com/tangem/tap/features/tokens/redux/TokensAction.kt @@ -18,11 +18,13 @@ sealed class TokensAction : Action { data class SetAddedCurrencies( val wallets: List, val derivationStyle: DerivationStyle? - ) : TokensAction() + ) : TokensAction() data class SetNonRemovableCurrencies(val wallets: List) : TokensAction() data class SaveChanges( val addedTokens: List, val addedBlockchains: List ) : TokensAction() + + object PrepareAndNavigateToAddCustomToken : TokensAction() } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/tokens/redux/TokensMiddleware.kt b/app/src/main/java/com/tangem/tap/features/tokens/redux/TokensMiddleware.kt index dcd52c1c49..2984b99f31 100644 --- a/app/src/main/java/com/tangem/tap/features/tokens/redux/TokensMiddleware.kt +++ b/app/src/main/java/com/tangem/tap/features/tokens/redux/TokensMiddleware.kt @@ -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, diff --git a/app/src/main/java/com/tangem/tap/features/tokens/redux/TokensState.kt b/app/src/main/java/com/tangem/tap/features/tokens/redux/TokensState.kt index 22db9bc589..29b7289131 100644 --- a/app/src/main/java/com/tangem/tap/features/tokens/redux/TokensState.kt +++ b/app/src/main/java/com/tangem/tap/features/tokens/redux/TokensState.kt @@ -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 diff --git a/app/src/main/java/com/tangem/tap/features/tokens/ui/AddTokensFragment.kt b/app/src/main/java/com/tangem/tap/features/tokens/ui/AddTokensFragment.kt index fd1062e1dc..e0ec165944 100644 --- a/app/src/main/java/com/tangem/tap/features/tokens/ui/AddTokensFragment.kt +++ b/app/src/main/java/com/tangem/tap/features/tokens/ui/AddTokensFragment.kt @@ -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) } } diff --git a/app/src/main/java/com/tangem/tap/features/tokens/ui/compose/CollapsedCurrencyItem.kt b/app/src/main/java/com/tangem/tap/features/tokens/ui/compose/CollapsedCurrencyItem.kt index 5d5de44aa4..51db0013b3 100644 --- a/app/src/main/java/com/tangem/tap/features/tokens/ui/compose/CollapsedCurrencyItem.kt +++ b/app/src/main/java/com/tangem/tap/features/tokens/ui/compose/CollapsedCurrencyItem.kt @@ -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 diff --git a/app/src/main/java/com/tangem/tap/features/tokens/ui/compose/CurrenciesScreen.kt b/app/src/main/java/com/tangem/tap/features/tokens/ui/compose/CurrenciesScreen.kt index e9b9d3f3eb..a37109a9ef 100644 --- a/app/src/main/java/com/tangem/tap/features/tokens/ui/compose/CurrenciesScreen.kt +++ b/app/src/main/java/com/tangem/tap/features/tokens/ui/compose/CurrenciesScreen.kt @@ -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 diff --git a/app/src/main/java/com/tangem/tap/features/tokens/ui/compose/ExpandedCurrencyItem.kt b/app/src/main/java/com/tangem/tap/features/tokens/ui/compose/ExpandedCurrencyItem.kt index 1756881f8f..e5f57c2eaf 100644 --- a/app/src/main/java/com/tangem/tap/features/tokens/ui/compose/ExpandedCurrencyItem.kt +++ b/app/src/main/java/com/tangem/tap/features/tokens/ui/compose/ExpandedCurrencyItem.kt @@ -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 diff --git a/app/src/main/res/menu/popular_tokens.xml b/app/src/main/res/menu/popular_tokens.xml index 80d52565c9..5cc1ac82f8 100644 --- a/app/src/main/res/menu/popular_tokens.xml +++ b/app/src/main/res/menu/popular_tokens.xml @@ -9,4 +9,11 @@ app:actionViewClass="androidx.appcompat.widget.SearchView" app:showAsAction="always" /> + + \ No newline at end of file diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml index 3318e7609d..ecb9562aad 100644 --- a/app/src/main/res/values/colors.xml +++ b/app/src/main/res/values/colors.xml @@ -17,8 +17,17 @@ #FFB71B #FFFFFF - #8E8E93 - #636366 + + #F3F3F3 + #F8F8FB + #DADADF + #D0D0D5 + #C7C7CC + #D1D1D6 + #CACACC + #B6B6B8 + #8E8E90 + #666668 #48484A #3A3A3C #2C2C2E @@ -37,14 +46,6 @@ #14181D - #F3F3F3 - #F8F8FB - #DADADF - #D0D0D5 - #C7C7CC - #D1D1D6 - #C9C9CD - #1F000000 #14212121 diff --git a/domain/src/main/java/com/tangem/domain/DomainError.kt b/domain/src/main/java/com/tangem/domain/DomainError.kt index ad0115427a..7f6b14c1ed 100644 --- a/domain/src/main/java/com/tangem/domain/DomainError.kt +++ b/domain/src/main/java/com/tangem/domain/DomainError.kt @@ -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? diff --git a/domain/src/main/java/com/tangem/domain/DomainException.kt b/domain/src/main/java/com/tangem/domain/DomainException.kt index 4788eed8f8..f137262717 100644 --- a/domain/src/main/java/com/tangem/domain/DomainException.kt +++ b/domain/src/main/java/com/tangem/domain/DomainException.kt @@ -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 []" + ) } \ No newline at end of file diff --git a/domain/src/main/java/com/tangem/domain/DomainMessage.kt b/domain/src/main/java/com/tangem/domain/DomainMessage.kt new file mode 100644 index 0000000000..5ff7266ab9 --- /dev/null +++ b/domain/src/main/java/com/tangem/domain/DomainMessage.kt @@ -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 {} + +} \ No newline at end of file diff --git a/domain/src/main/java/com/tangem/domain/DomainStateDialog.kt b/domain/src/main/java/com/tangem/domain/DomainStateDialog.kt index f2df99a7c1..a3c7aa9da2 100644 --- a/domain/src/main/java/com/tangem/domain/DomainStateDialog.kt +++ b/domain/src/main/java/com/tangem/domain/DomainStateDialog.kt @@ -10,6 +10,8 @@ interface DomainStateDialog sealed class DomainDialog : DomainStateDialog { + data class DialogError(val error: DomainError) : DomainDialog() + data class SelectTokenDialog( val items: List, val networkIdConverter: (String) -> String, diff --git a/domain/src/main/java/com/tangem/domain/DomainWrapped.kt b/domain/src/main/java/com/tangem/domain/DomainWrapped.kt new file mode 100644 index 0000000000..41ab1c8569 --- /dev/null +++ b/domain/src/main/java/com/tangem/domain/DomainWrapped.kt @@ -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 + ) +} \ No newline at end of file diff --git a/domain/src/main/java/com/tangem/domain/common/extensions/Blockchain.kt b/domain/src/main/java/com/tangem/domain/common/extensions/Blockchain.kt new file mode 100644 index 0000000000..94e5daec42 --- /dev/null +++ b/domain/src/main/java/com/tangem/domain/common/extensions/Blockchain.kt @@ -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" + } +} \ No newline at end of file diff --git a/domain/src/main/java/com/tangem/domain/features/addCustomToken/CompleteData.kt b/domain/src/main/java/com/tangem/domain/features/addCustomToken/CompleteData.kt index 88ef38e4cd..90b11472d2 100644 --- a/domain/src/main/java/com/tangem/domain/features/addCustomToken/CompleteData.kt +++ b/domain/src/main/java/com/tangem/domain/features/addCustomToken/CompleteData.kt @@ -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 = - 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() { - 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 = 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() { - 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() { + + 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 = CustomTokenFieldId.values().toList() } diff --git a/domain/src/main/java/com/tangem/domain/features/addCustomToken/Errors.kt b/domain/src/main/java/com/tangem/domain/features/addCustomToken/Errors.kt index b651dd844b..ebf097d964 100644 --- a/domain/src/main/java/com/tangem/domain/features/addCustomToken/Errors.kt +++ b/domain/src/main/java/com/tangem/domain/features/addCustomToken/Errors.kt @@ -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() } \ No newline at end of file diff --git a/domain/src/main/java/com/tangem/domain/features/addCustomToken/AddCustomTokenManager.kt b/domain/src/main/java/com/tangem/domain/features/addCustomToken/TangemTechServiceManager.kt similarity index 98% rename from domain/src/main/java/com/tangem/domain/features/addCustomToken/AddCustomTokenManager.kt rename to domain/src/main/java/com/tangem/domain/features/addCustomToken/TangemTechServiceManager.kt index 0b49f500d7..438e75e47a 100644 --- a/domain/src/main/java/com/tangem/domain/features/addCustomToken/AddCustomTokenManager.kt +++ b/domain/src/main/java/com/tangem/domain/features/addCustomToken/TangemTechServiceManager.kt @@ -8,7 +8,7 @@ import com.tangem.network.common.AddHeaderInterceptor /** [REDACTED_AUTHOR] */ -class AddCustomTokenManager( +class TangemTechServiceManager( private val tangemTechService: TangemTechService ) { diff --git a/domain/src/main/java/com/tangem/domain/features/addCustomToken/redux/AddCustomTokenAction.kt b/domain/src/main/java/com/tangem/domain/features/addCustomToken/redux/AddCustomTokenAction.kt index 03cb125db1..0b541fb309 100644 --- a/domain/src/main/java/com/tangem/domain/features/addCustomToken/redux/AddCustomTokenAction.kt +++ b/domain/src/main/java/com/tangem/domain/features/addCustomToken/redux/AddCustomTokenAction.kt @@ -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) : AddCustomTokenAction() data class OnTokenDerivationPathChanged(val blockchainDerivationPath: Field.Data) : AddCustomTokenAction() data class OnTokenDecimalsChanged(val tokenDecimals: Field.Data) : 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) : Warning() diff --git a/domain/src/main/java/com/tangem/domain/features/addCustomToken/redux/AddCustomTokenHub.kt b/domain/src/main/java/com/tangem/domain/features/addCustomToken/redux/AddCustomTokenHub.kt index ea1a059fe5..3e0ed80316 100644 --- a/domain/src/main/java/com/tangem/domain/features/addCustomToken/redux/AddCustomTokenHub.kt +++ b/domain/src/main/java/com/tangem/domain/features/addCustomToken/redux/AddCustomTokenHub.kt @@ -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("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("AddCustomT cancel: ValueCallback ) { 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(ContractAddress, hubState).data.value - val foundTokens = requestInfoAboutContractAddress(contractAddress, hubState) - manageTokenChanges(null, foundTokens) + val contractAddress = hubState.getField(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("AddCustomT // addOrRemoveError(DerivationPath, validator.validate(action.value.value)) // } is ClearTokenFields -> { - val nameField = getField(Name, hubState) - val symbolField = getField(Symbol, hubState) - val decimalsField = getField(Decimals, hubState) + val nameField = hubState.getField(Name) + val symbolField = hubState.getField(Symbol) + val decimalsField = hubState.getField(Decimals) nameField.data = Field.Data("", false) symbolField.data = Field.Data("", false) @@ -104,14 +110,14 @@ internal class AddCustomTokenHub : BaseStoreHub("AddCustomT dispatchOnMain(UpdateForm(hubState)) } is FillTokenFields -> { - val networkField = getField(Network, hubState) - val nameField = getField(Name, hubState) - val symbolField = getField(Symbol, hubState) - val decimalsField = getField(Decimals, hubState) + val networkField = hubState.getField(Network) + val nameField = hubState.getField(Name) + val symbolField = hubState.getField(Symbol) + val decimalsField = hubState.getField(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("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 { + val tangemTechServiceManager = requireNotNull(hubState.tangemTechServiceManager) dispatchOnMain(Screen.UpdateTokenFields(listOf(ContractAddress to ViewStates.TokenField(isLoading = true)))) - val tokenManager = hubState.addCustomTokenManager - val field = getField(Network, hubState) + val field = hubState.getField(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("AddCustomT return result } - private suspend fun manageTokenChanges( - card: Card?, - foundTokens: List, - ) { + private suspend fun manageTokenChanges(foundTokens: List) { val toAddWarnings = mutableSetOf() val toRemoveWarnings = mutableSetOf() @@ -159,7 +185,7 @@ internal class AddCustomTokenHub : BaseStoreHub("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("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("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("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("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("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("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("AddCustomT )) } - private inline fun getField(id: FieldId, state: AddCustomTokenState): T { - return state.form.getField(id) as T - } - - private inline fun 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("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("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" + ) } } \ No newline at end of file diff --git a/domain/src/main/java/com/tangem/domain/features/addCustomToken/redux/AddCustomTokenState.kt b/domain/src/main/java/com/tangem/domain/features/addCustomToken/redux/AddCustomTokenState.kt index cfcdf6da1b..13acabb71e 100644 --- a/domain/src/main/java/com/tangem/domain/features/addCustomToken/redux/AddCustomTokenState.kt +++ b/domain/src/main/java/com/tangem/domain/features/addCustomToken/redux/AddCustomTokenState.kt @@ -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> = createFormValidators(), + val formValidators: Map> = createFormValidators(), val formErrors: Map = emptyMap(), + val tokenId: String? = null, val warnings: Set = 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 getField(id: FieldId): T = form.getField(id) as T + + inline fun 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 visitDataConverter(converter: FieldDataConverter): 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(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> { + private fun createFormValidators(): Map> { return mapOf( ContractAddress to TokenContractAddressValidator(), Network to TokenNetworkValidator(), diff --git a/domain/src/main/java/com/tangem/domain/features/addCustomToken/redux/Models.kt b/domain/src/main/java/com/tangem/domain/features/addCustomToken/redux/Models.kt index 3dba21a577..1d9bc38a7a 100644 --- a/domain/src/main/java/com/tangem/domain/features/addCustomToken/redux/Models.kt +++ b/domain/src/main/java/com/tangem/domain/features/addCustomToken/redux/Models.kt @@ -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() -} \ No newline at end of file +} + +data class AddedCurrencies( + val addedTokens: List, + val addedBlockchains: List +) \ No newline at end of file diff --git a/domain/src/main/java/com/tangem/domain/redux/ReStoreHub.kt b/domain/src/main/java/com/tangem/domain/redux/ReStoreHub.kt index 183599aa4a..9de3cedcf3 100644 --- a/domain/src/main/java/com/tangem/domain/redux/ReStoreHub.kt +++ b/domain/src/main/java/com/tangem/domain/redux/ReStoreHub.kt @@ -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 { * 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( private val name: String, private val dispatcher: CoroutineDispatcher = Executors.newFixedThreadPool(1).asCoroutineDispatcher() ) : ReStoreHub { + val globalState: DomainGlobalState + get() = domainStore.state.globalState + val hubScope = CoroutineScope( Job() + dispatcher + CoroutineName(name) + FeatureCoroutineExceptionHandler.create(name) )