From fb1ed471afb167f77291cc374958d2a050a7ef7e Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 31 May 2022 14:20:10 +0300 Subject: [PATCH] Updated on 2026-08-14 --- .../compose/AddCustomTokenScreen.kt | 57 ++- .../compose/test/ContractAddressTests.kt | 123 ++++++ .../compose/test/TestAddCostomTokenActions.kt | 225 ----------- .../compose/test/TestCasesList.kt | 63 +++ .../redux/AddCustomTokenAction.kt | 3 +- .../addCustomToken/redux/AddCustomTokenHub.kt | 370 +++++++++--------- .../redux/AddCustomTokenState.kt | 7 +- 7 files changed, 420 insertions(+), 428 deletions(-) create mode 100644 app/src/main/java/com/tangem/tap/features/tokens/addCustomToken/compose/test/ContractAddressTests.kt delete mode 100644 app/src/main/java/com/tangem/tap/features/tokens/addCustomToken/compose/test/TestAddCostomTokenActions.kt create mode 100644 app/src/main/java/com/tangem/tap/features/tokens/addCustomToken/compose/test/TestCasesList.kt 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 2fd177fa85..14f9fa3397 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 @@ -23,18 +23,66 @@ import com.tangem.domain.features.addCustomToken.redux.ViewStates import com.tangem.domain.redux.domainStore import com.tangem.tap.common.compose.* import com.tangem.tap.common.moduleMessage.ModuleMessageConverter -import com.tangem.tap.features.tokens.addCustomToken.compose.test.TestAddCustomTokenActions +import com.tangem.tap.features.tokens.addCustomToken.compose.test.TestCase +import com.tangem.tap.features.tokens.addCustomToken.compose.test.TestCasesList import com.tangem.wallet.R +import kotlinx.coroutines.launch /** [REDACTED_AUTHOR] */ private class AddCustomTokenScreen {} // for simple search +@OptIn(ExperimentalMaterialApi::class) @Composable fun AddCustomTokenScreen( state: MutableState, closePopupTrigger: ClosePopupTrigger, +) { + val selectedTestCase = remember { mutableStateOf(TestCase.ContractAddress) } + + val bottomSheetScaffoldState = rememberBottomSheetScaffoldState( + bottomSheetState = BottomSheetState(BottomSheetValue.Collapsed) + ) + val coroutineScope = rememberCoroutineScope() + val toggleBottomSheet = { coroutineScope.launch { bottomSheetScaffoldState.toggle() } } + + BottomSheetScaffold( + scaffoldState = bottomSheetScaffoldState, + sheetContent = { + Surface(color = colorResource(id = R.color.lightGray5)) { + selectedTestCase.value.content(toggleBottomSheet) + } + }, + sheetPeekHeight = 0.dp, + ) { + Column() { + TestCasesList(onItemClick = { + selectedTestCase.value = it + toggleBottomSheet() + }) + ScreenContent(state, closePopupTrigger) + } + } + + ComposeDialogManager() + LaunchedEffect(key1 = Unit, block = { domainStore.dispatch(AddCustomTokenAction.OnCreate) }) + DisposableEffect(key1 = Unit, effect = { onDispose { domainStore.dispatch(AddCustomTokenAction.OnDestroy) } }) +} + +@OptIn(ExperimentalMaterialApi::class) +private suspend fun BottomSheetScaffoldState.toggle() { + if (bottomSheetState.isCollapsed) { + bottomSheetState.expand() + } else { + bottomSheetState.collapse() + } +} + +@Composable +private fun ScreenContent( + state: MutableState, + closePopupTrigger: ClosePopupTrigger, ) { val scaffoldState = rememberScaffoldState() @@ -50,9 +98,8 @@ fun AddCustomTokenScreen( ) { Box(Modifier.fillMaxSize()) { LazyColumn( - contentPadding = PaddingValues(bottom = 90.dp) + contentPadding = PaddingValues(bottom = 90.dp), ) { - item { TestAddCustomTokenActions() } item { Surface( modifier = Modifier.padding(16.dp), @@ -71,11 +118,7 @@ fun AddCustomTokenScreen( item { Warnings(state.value.warnings.toList()) } } } - ComposeDialogManager() } - - LaunchedEffect(key1 = Unit, block = { domainStore.dispatch(AddCustomTokenAction.OnCreate) }) - DisposableEffect(key1 = Unit, effect = { onDispose { domainStore.dispatch(AddCustomTokenAction.OnDestroy) } }) } @Composable diff --git a/app/src/main/java/com/tangem/tap/features/tokens/addCustomToken/compose/test/ContractAddressTests.kt b/app/src/main/java/com/tangem/tap/features/tokens/addCustomToken/compose/test/ContractAddressTests.kt new file mode 100644 index 0000000000..25494b3627 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/features/tokens/addCustomToken/compose/test/ContractAddressTests.kt @@ -0,0 +1,123 @@ +package com.tangem.tap.features.tokens.addCustomToken.compose.test + +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.material.Button +import androidx.compose.material.Divider +import androidx.compose.material.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import com.tangem.blockchain.common.Blockchain +import com.tangem.common.extensions.VoidCallback +import com.tangem.domain.common.form.Field +import com.tangem.domain.features.addCustomToken.redux.AddCustomTokenAction +import com.tangem.domain.redux.domainStore + +/** +[REDACTED_AUTHOR] + */ +@Composable +fun ContractAddressTests( + onItemClick: VoidCallback +) { + val casesInfo = listOf( + "USDC on ETH" to "0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48", + "BUSD on ETH" to "0x4fabb145d64652a948d72533023f6e7a623c7c53", + "ETH on AVALANCHE" to "0xf20d962a6c8f70c731bd838a3a388d7d48fa6e15", + "USDC on ETH (invalid - cut address)" to "0xa0b86991c6218b36c1d1", + "Custom EVM" to "0x1111111111111111112111111111111111111113", + "Supported by several networks" to "0xa1faa113cbe53436df28ff0aee54275c13b40975", + "Invalid" to "!@#_ _-%%^&&*((){P P2iOWsdfFQLA", + ) + CasesListContent(casesInfo, onItemClick) +} + +@Composable +fun SolanaAddressTests( + onItemClick: VoidCallback +) { + val casesInfo = listOf( + "USDT (full)" to "Es9vMFrzaCERmJfrF4H2FYD4KCoNkY11McCe8BenwNYB", + "USDT (valid - 2/3 of address)" to "Es9vMFrzaCERmJfrF4H2FYD4KCoNkY11McCe8Ben", + "USDT (invalid - 1/3 of address)" to "Es9vMFrzaCERmJ", + "ETH (full)" to "2FPyTwcZLUg1MDrwsyoP4D6s1tM7hAkHYRjkNb5w6Pxk", + ) + CasesListContent(casesInfo, onItemClick) +} + +@Composable +private fun CasesListContent( + casesList: List>, + onItemClick: VoidCallback, +) { + LazyColumn(content = { + item { + Row() { + ResetContractAddressButton(onItemClick) + Text("", modifier = Modifier.weight(1f)) + ResetAllFieldsButton(onItemClick) + } + Divider() + } + items(casesList.size) { + val (info, address) = casesList[it] + ContractAddressButton(info, address, onItemClick) + } + }) +} + +@Composable +fun ResetAllFieldsButton( + onItemClick: VoidCallback +) { + ActionButton(name = "Reset") { + onItemClick() + domainStore.dispatch(AddCustomTokenAction.OnTokenContractAddressChanged(Field.Data("", false))) + domainStore.dispatch(AddCustomTokenAction.OnTokenNetworkChanged(Field.Data(Blockchain.Unknown, false))) + domainStore.dispatch(AddCustomTokenAction.OnTokenNameChanged(Field.Data("", false))) + domainStore.dispatch(AddCustomTokenAction.OnTokenSymbolChanged(Field.Data("", false))) + domainStore.dispatch(AddCustomTokenAction.OnTokenDecimalsChanged(Field.Data("", false))) + domainStore.dispatch(AddCustomTokenAction.OnTokenDerivationPathChanged(Field.Data(Blockchain.Unknown, false))) + } +} + +@Composable +fun ResetContractAddressButton( + onItemClick: VoidCallback +) { + ActionButton(name = "Set empty address") { + onItemClick() + domainStore.dispatch(AddCustomTokenAction.OnTokenContractAddressChanged(Field.Data("", false))) + } +} + +@Composable +private fun ContractAddressButton( + name: String, + address: String, + onItemClick: VoidCallback +) { + ActionButton( + modifier = Modifier.fillMaxWidth(), + name = name, + ) { + onItemClick() + domainStore.dispatch(AddCustomTokenAction.OnTokenContractAddressChanged(Field.Data(address, false))) + } +} + +@Composable +fun ActionButton( + modifier: Modifier = Modifier, + name: String, + onClick: () -> Unit, +) { + Button( + modifier = modifier.padding(horizontal = 8.dp), + onClick = onClick + ) { Text(name, fontSize = 12.sp) } +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/tokens/addCustomToken/compose/test/TestAddCostomTokenActions.kt b/app/src/main/java/com/tangem/tap/features/tokens/addCustomToken/compose/test/TestAddCostomTokenActions.kt deleted file mode 100644 index ea41adc0e6..0000000000 --- a/app/src/main/java/com/tangem/tap/features/tokens/addCustomToken/compose/test/TestAddCostomTokenActions.kt +++ /dev/null @@ -1,225 +0,0 @@ -package com.tangem.tap.features.tokens.addCustomToken.compose.test - -import androidx.compose.foundation.layout.* -import androidx.compose.material.Button -import androidx.compose.material.Text -import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.ui.Modifier -import androidx.compose.ui.unit.dp -import androidx.compose.ui.unit.sp -import com.tangem.blockchain.common.Blockchain -import com.tangem.common.extensions.VoidCallback -import com.tangem.domain.common.form.Field -import com.tangem.domain.features.addCustomToken.redux.AddCustomTokenAction.* -import com.tangem.domain.redux.domainStore -import com.tangem.wallet.BuildConfig - -/** -[REDACTED_AUTHOR] - */ -@Composable -fun TestAddCustomTokenActions() { - if (!BuildConfig.TEST_ACTION_ENABLED) return - - Column() { - // deep test -// ActionRow("Invalid field value") { InvalidFieldValue() } -// ActionRow("Active(true)") { ActiveTrue() } -// ActionRow("Active(false) && decimalCount != null") { ActiveFalseDecimals() } -// ActionRow("In several networks") { InSeveralNetworks() } -// ActionRow("Address not found") { UnknownContracts() } - - // test - ActionRow("All in one") { AllInOne() } - - // Any action -// ActionRow("CustomActions - find coins active=false, decimals != null") { CustomActions() } - } -} - -@Composable -private fun AllInOne() { - // validation error -// ContractAddressButton( -// name = "invalid", -// address = "unk" -// ) - // active = true - ContractAddressButton( - name = "true", - address = "0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48" - ) - // more than one network - ContractAddressButton( - name = ">1 network", - address = "0xa1faa113cbe53436df28ff0aee54275c13b40975" - ) - // unknown - ContractAddressButton( - name = "unknown", - address = "0x1111111111111111112111111111111111111113" - ) - ContractAddressButton( - name = "full custom", - address = "0x3019BF2a2eF8040C242C9a4c5c4BD4C81678b2A1", - tokenNetwork = Blockchain.Ethereum, - tokenName = "Test unknown", - tokenSymbol = "TU", - tokenDecimals = "5", - ) -} - -@Composable -private fun InvalidFieldValue() { - ContractAddressButton( - name = "unk", - address = "unk" - ) - ContractAddressButton( - name = "someText", - address = "someText" - ) - ContractAddressButton( - name = "alskdml...fa s", - address = "alskdmlasd asdln alsdknflasd flasd fa s" - ) -} - -@Composable -private fun ActiveTrue() { - ContractAddressButton( - name = "USDC- Ethereum", - address = "0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48" - ) - ContractAddressButton( - name = "USDT- Avalanche", - address = "0xc7198437980c041c805a1edcba50c1ce5db95118" - ) - ContractAddressButton( - name = "VID- Fantom", - address = "0x922d641a426dcffaef11680e5358f34d97d112e1" - ) -} - -@Composable -private fun ActiveFalseDecimals() { - ContractAddressButton( - name = "ALPHA- avalanche", - address = "0x2147efff675e4a4ee1c2f918d181cdbd7a8e208f" - ) - ContractAddressButton( - name = "BETA- avalanche", - address = "0x511d35c52a3c244e7b8bd92c0c297755fbd89212" - ) -} - -@Composable -private fun InSeveralNetworks() { - ContractAddressButton( - name = "ALPHA- Eth,Bsc", - address = "0xa1faa113cbe53436df28ff0aee54275c13b40975" - ) - ContractAddressButton( - name = "BETA- Eth,Bsc", - address = "0xbe1a001fe942f96eea22ba08783140b9dcc09d28" - ) -} - -@Composable -private fun UnknownContracts() { - ContractAddressButton( - name = "0x11...113", - address = "0x1111111111111111112111111111111111111113" - ) - ContractAddressButton( - name = "0xc7...111", - address = "0xc7198437980c041c805a1edcba50c1ce5db95111" - ) -} - -@Composable -private fun CustomActions() { -} - -@Composable -private fun ActionRow( - name: String, - content: @Composable () -> Unit -) { - Column() { - Text( - modifier = Modifier.padding(horizontal = 16.dp), - text = name, - fontSize = 14.sp - ) - Row( - Modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.Start - ) { content() } - } - -} - -@Composable -private fun ActionButton( - name: String, - onClick: VoidCallback, -) { - Button( - modifier = Modifier.padding(horizontal = 4.dp), - onClick = onClick - ) { Text(name, fontSize = 8.sp) } -} - -@Composable -private fun ContractAddressButton( - name: String, - address: String, - tokenNetwork: Blockchain? = null, - tokenName: String? = null, - tokenSymbol: String? = null, - tokenDecimals: String? = null, -) { - ActionButton(name = name) { - resetTokenValues() - domainStore.dispatch(OnTokenContractAddressChanged(Field.Data(address, false))) - tokenNetwork?.let { - domainStore.dispatch(OnTokenNetworkChanged(Field.Data(tokenNetwork, false))) - } - tokenName?.let { - domainStore.dispatch(OnTokenNameChanged(Field.Data(tokenName, false))) - } - tokenSymbol?.let { - domainStore.dispatch(OnTokenSymbolChanged(Field.Data(tokenSymbol, false))) - } - tokenDecimals?.let { - domainStore.dispatch(OnTokenDecimalsChanged(Field.Data(tokenDecimals, false))) - } - } -} - -@Composable -private fun CustomActionButton( - name: String, - action: suspend () -> Unit -) { - val startValue = 0 - val anyValue = remember { mutableStateOf(startValue) } - LaunchedEffect(key1 = anyValue.value, block = { - if (anyValue.value != startValue) { - action() - } - }) - - ActionButton(name) { anyValue.value = anyValue.value + 1 } -} - -private fun resetTokenValues() { - domainStore.dispatch(OnTokenNetworkChanged(Field.Data(Blockchain.Unknown, false))) -// domainStore.dispatch(AddCustomTokenAction.OnTokenNameChanged(Field.Data("", false))) -// domainStore.dispatch(AddCustomTokenAction.OnTokenSymbolChanged(Field.Data("", false))) -// domainStore.dispatch(AddCustomTokenAction.OnTokenDecimalsChanged(Field.Data("", false))) -} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/tokens/addCustomToken/compose/test/TestCasesList.kt b/app/src/main/java/com/tangem/tap/features/tokens/addCustomToken/compose/test/TestCasesList.kt new file mode 100644 index 0000000000..ecdc68bd8c --- /dev/null +++ b/app/src/main/java/com/tangem/tap/features/tokens/addCustomToken/compose/test/TestCasesList.kt @@ -0,0 +1,63 @@ +package com.tangem.tap.features.tokens.addCustomToken.compose.test + +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.padding +import androidx.compose.material.Button +import androidx.compose.material.Surface +import androidx.compose.material.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.colorResource +import androidx.compose.ui.unit.dp +import com.tangem.common.extensions.VoidCallback +import com.tangem.wallet.BuildConfig +import com.tangem.wallet.R + +/** +[REDACTED_AUTHOR] + */ +@Composable +fun TestCasesList( + onItemClick: (TestCase) -> Unit +) { + if (!BuildConfig.TEST_ACTION_ENABLED) return + + Surface( + color = colorResource(id = R.color.lightGray5) + ) { + Column( + Modifier.padding(horizontal = 16.dp) + ) { + listOf( + TestCase.ContractAddress, + TestCase.SolanaTokens, + ).map { TestCaseListItem(it, onItemClick) } + } + } +} + +@Composable +fun TestCaseListItem( + testCase: TestCase, + onItemClick: (TestCase) -> Unit, +) { + Row( + verticalAlignment = Alignment.CenterVertically, + ) { + Text( + modifier = Modifier.weight(1f), + text = testCase.description, + ) + Button( + onClick = { onItemClick(testCase) } + ) { Text("Start") } + } +} + +enum class TestCase(val description: String, val content: @Composable (VoidCallback) -> Unit) { + ContractAddress("Test contract address field", { ContractAddressTests(it) }), + Auto("Test contract address field", { ContractAddressTests(it) }), + SolanaTokens("Test Solana contract addresses", { SolanaAddressTests(it) }), ; +} \ No newline at end of file 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 f1eadfeab5..6949c1de51 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 @@ -8,6 +8,7 @@ import com.tangem.domain.common.form.Field import com.tangem.domain.common.form.FieldId import com.tangem.domain.features.addCustomToken.CustomCurrency import com.tangem.domain.features.addCustomToken.CustomTokenFieldId +import com.tangem.network.api.tangemTech.CoinsResponse import org.rekotlin.Action /** @@ -34,7 +35,7 @@ sealed class AddCustomTokenAction : Action { data class OnTokenDecimalsChanged(val tokenDecimals: Field.Data) : AddCustomTokenAction() object OnAddCustomTokenClicked : AddCustomTokenAction() - data class SetFoundTokenId(val id: String?) : AddCustomTokenAction() + data class SetFoundTokenInfo(val foundToken: CoinsResponse.Coin?) : AddCustomTokenAction() // form fields data class UpdateForm(val state: AddCustomTokenState) : AddCustomTokenAction() 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 7cfd1ec542..9a4b0be414 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 @@ -6,8 +6,7 @@ import com.tangem.blockchain.common.DerivationStyle import com.tangem.common.extensions.guard import com.tangem.common.services.Result import com.tangem.domain.AddCustomTokenError -import com.tangem.domain.AddCustomTokenError.Warning.PotentialScamToken -import com.tangem.domain.AddCustomTokenError.Warning.TokenAlreadyAdded +import com.tangem.domain.AddCustomTokenError.Warning.* import com.tangem.domain.AddCustomTokenException import com.tangem.domain.DomainDialog import com.tangem.domain.DomainWrapped @@ -73,10 +72,10 @@ internal class AddCustomTokenHub : BaseStoreHub("AddCustomT validateContractAddressAndNotify(ContractAddress.getFieldValue()) } is OnTokenDerivationPathChanged -> { - checkAndUpdateAddButton() + updateAddButton() } is OnTokenNameChanged, is OnTokenSymbolChanged, is OnTokenDecimalsChanged -> { - checkAndUpdateAddButton() + updateAddButton() } is OnAddCustomTokenClicked -> { val state = hubState @@ -109,74 +108,82 @@ internal class AddCustomTokenHub : BaseStoreHub("AddCustomT null -> { // valid contract address ContractAddress.removeError() - reassembleBlockchainNetworkList() findTokenAndUpdateFields(contractAddress) } AddCustomTokenError.InvalidContractAddress -> { - // invalid contract address ContractAddress.addError(error) - updateTokenDetailFields(hubState.tokensAnyFieldsIsFilled()) - reassembleBlockchainNetworkList() - checkAndUpdateAddButton() + enableDisableTokenDetailFields(hubState.tokensAnyFieldsIsFilled()) } AddCustomTokenError.FieldIsEmpty -> { - // empty contract address ContractAddress.removeError() clearTokenDetailsFields() - updateTokenDetailFields(false) - reassembleBlockchainNetworkList() - checkAndUpdateAddButton() + disableTokenDetailFields() } + else -> {} } } else { + // is default selection (Blockchain.Unknown) when (error) { null -> { - // valid contract address + // Blockchain.Unknown has always valid contract address ContractAddress.removeError() - reassembleBlockchainNetworkList() findTokenAndUpdateFields(contractAddress) } else -> { ContractAddress.removeError() clearTokenDetailsFields() - updateTokenDetailFields(false) - reassembleBlockchainNetworkList() - checkAndUpdateAddButton() + disableTokenDetailFields() } } } updateDerivationPath(Network.getFieldValue()) + updateWarnings() + updateAddButton() } - private suspend fun reassembleBlockchainNetworkList() { - val state = hubState - val networkBlockchainList = Network.getField().itemList - val card = requireNotNull(globalState.scanResponse?.card) - val newNetworkBlockchainList: List = state.getNetworks(card, state.getCustomTokenType()) - - val listsIdentical = newNetworkBlockchainList.toSet() == networkBlockchainList.toSet() - if (listsIdentical) return - - val networkFieldIsFilled = Network.isFilled() - val newNetworkField = Network.getField().copy(itemList = newNetworkBlockchainList) - newNetworkField.data = Field.Data(Network.getFieldValue(), newNetworkField.data.isUserInput) - if (networkFieldIsFilled) { - val listSameSize = networkBlockchainList.size == newNetworkBlockchainList.size - val newListLessThanOld = networkBlockchainList.size > newNetworkBlockchainList.size - if (listSameSize || newListLessThanOld) { - // check selected blockchain - val selectedBlockchain = Network.getFieldValue() - val defaultSelection = Blockchain.Unknown - if (!newNetworkBlockchainList.contains(selectedBlockchain) - && newNetworkBlockchainList.contains(defaultSelection)) { - // selectedBlockchain not present in the new list. Change selection to default - newNetworkField.data = Field.Data(defaultSelection, false) - } - } + private suspend fun findTokenAndUpdateFields(contractAddress: String) { + val foundTokens = requestInfoAboutToken(contractAddress) + if (foundTokens.isEmpty()) { + // token not found - it's completely custom + dispatchOnMain(SetFoundTokenInfo(null)) + enableTokenDetailFields() + return } - hubState.setField(newNetworkField) - dispatchOnMain(UpdateForm(hubState)) + // foundToken - contains all info about the token + val foundToken = foundTokens[0] + dispatchOnMain(SetFoundTokenInfo(foundToken)) + when { + foundToken.networks.isEmpty() -> { + Timber.e("Unexpected state -> throw to FB") + } + foundToken.networks.size == 1 -> { + // token with single contract address + val singleTokenContract = foundToken.networks[0] + fillTokenFields(foundToken, singleTokenContract) + disableTokenDetailFields() + } + else -> { + val dialog = DomainDialog.SelectTokenDialog( + items = foundToken.networks, + networkIdConverter = { networkId -> + val blockchain = Blockchain.fromNetworkId(networkId) + if (blockchain == null || blockchain == Blockchain.Unknown) { + throw AddCustomTokenException.SelectTokeNetworkException(networkId) + } + hubState.blockchainToName(blockchain) ?: "" + }, + onSelect = { selectedContract -> + hubScope.launch { + // find how to connect to the upper coroutineContext and dispatch through them + fillTokenFields(foundToken, selectedContract) + disableTokenDetailFields() + } + }, + ) + dispatchOnMain(DomainGlobalAction.ShowDialog(dialog)) + } + } } private suspend fun updateDerivationPath(blockchainNetwork: Blockchain) { @@ -204,6 +211,105 @@ internal class AddCustomTokenHub : BaseStoreHub("AddCustomT } } + private suspend fun updateWarnings() { + val state = hubState + val warningsAdd = mutableSetOf() + val warningsRemove = mutableSetOf() + + val tokenIsSupported = tokenIsSupported(Network.getFieldValue()) + val alreadyAdded = isPersistIntoAppSavedTokensList() + when (state.getCustomTokenType()) { + CustomTokenType.Blockchain -> { + warningsRemove.add(UnsupportedSolanaToken) + if (alreadyAdded) { + warningsAdd.add(TokenAlreadyAdded) + } else { + warningsRemove.add(TokenAlreadyAdded) + } + if (state.derivationPathIsSelected()) { + warningsAdd.add(PotentialScamToken) + } else { + warningsRemove.add(PotentialScamToken) + } + } + CustomTokenType.Token -> { + if (tokenIsSupported) { + warningsRemove.add(UnsupportedSolanaToken) + } else { + val error = ContractAddress.validateValue(ContractAddress.getFieldValue()) + when (error) { + AddCustomTokenError.FieldIsEmpty -> warningsRemove.add(UnsupportedSolanaToken) + else -> { + warningsAdd.add(UnsupportedSolanaToken) + } + } + } + + if (isPersistIntoAppSavedTokensList()) { + warningsAdd.add(TokenAlreadyAdded) + } else { + warningsRemove.add(TokenAlreadyAdded) + } + + if (state.foundToken == null) { + if (state.tokensAnyFieldsIsFilled()) { + warningsAdd.add(PotentialScamToken) + } else { + warningsRemove.add(PotentialScamToken) + } + } else { + if (state.foundToken.active) { + warningsRemove.add(PotentialScamToken) + } else { + warningsAdd.add(PotentialScamToken) + } + } + } + } + + dispatchOnMain(Warning.Replace( + remove = warningsRemove, + add = warningsAdd, + )) + } + + private suspend fun updateAddButton() { + if (isPersistIntoAppSavedTokensList()) { + TokenAlreadyAdded.add() + disableAddButton() + return + } else { + TokenAlreadyAdded.remove() + } + + val state = hubState + when { + // token + state.tokensFieldsIsFilled() && state.networkIsSelected() -> { + val error = ContractAddress.validateValue(ContractAddress.getFieldValue()) + val tokenIsSupported = tokenIsSupported(Network.getFieldValue()) + enableDisableAddButton(tokenIsSupported && error == null) + } + // token + state.tokensAnyFieldsIsFilled() -> { + disableAddButton() + } + // blockchain + else -> { + if (state.networkIsSelected()) { + val alreadyAdded = isBlockchainPersistIntoAppSavedTokensList() + if (alreadyAdded) { + disableAddButton() + } else { + enableAddButton() + } + } else { + disableAddButton() + } + } + } + } + private suspend fun requestInfoAboutToken( contractAddress: String, ): List { @@ -232,132 +338,16 @@ internal class AddCustomTokenHub : BaseStoreHub("AddCustomT return result } - private suspend fun findTokenAndUpdateFields(contractAddress: String) { - val foundTokens = requestInfoAboutToken(contractAddress) - if (foundTokens.isEmpty()) { - // token not found - it's completely custom - PotentialScamToken.add() - - dispatchOnMain(SetFoundTokenId(null)) - updateTokenDetailFields(true) - checkAndUpdateAddButton() - return - } - - // foundToken - contains all info about the token - val foundToken = foundTokens[0] - dispatchOnMain(SetFoundTokenId(foundToken.id)) - when { - foundToken.networks.isEmpty() -> { - Timber.e("Unexpected state -> throw to FB") - } - foundToken.networks.size == 1 -> { - // token with single contract address - val singleTokenContract = foundToken.networks[0] - fillTokenFields(foundToken, singleTokenContract) - - if (canHandleToken(Network.getFieldValue())) { - AddCustomTokenError.Warning.UnsupportedSolanaToken.remove() - } else { - AddCustomTokenError.Warning.UnsupportedSolanaToken.add() - updateTokenDetailFields(false) - updateAddButton(false) - return - } - - val isInAppSavedTokens = isTokenPersistIntoAppSavedTokensList() - if (isInAppSavedTokens) { - updateTokenDetailFields(false) - updateAddButton(false) - PotentialScamToken.replaceBy(TokenAlreadyAdded) - } else { - // not in the saved coins list - if (foundToken.active) { - updateTokenDetailFields(false) - updateAddButton(true) - if (hubState.derivationPathIsSelected()) { - PotentialScamToken.add() - } else { - TokenAlreadyAdded.remove() - PotentialScamToken.remove() - } - } else { - updateAddButton(true) - PotentialScamToken.add() - } - } - } - else -> { - val dialog = DomainDialog.SelectTokenDialog( - items = foundToken.networks, - networkIdConverter = { networkId -> - val blockchain = Blockchain.fromNetworkId(networkId) - if (blockchain == null || blockchain == Blockchain.Unknown) { - throw AddCustomTokenException.SelectTokeNetworkException(networkId) - } - hubState.blockchainToName(blockchain) ?: "" - }, - onSelect = { selectedContract -> - hubScope.launch { - // find how to connect to the upper coroutineContext and dispatch through them - PotentialScamToken.remove() - fillTokenFields(foundToken, selectedContract) - updateTokenDetailFields(false) - checkAndUpdateAddButton() - } - }, - ) - dispatchOnMain(DomainGlobalAction.ShowDialog(dialog)) - } - } - } - - private suspend fun checkAndUpdateAddButton() { - val alreadyAddedToAppTokensList = if (hubState.tokensAnyFieldsIsFilled()) { - isTokenPersistIntoAppSavedTokensList() - } else { - isBlockchainPersistIntoAppSavedTokensList() - } - if (alreadyAddedToAppTokensList) { - TokenAlreadyAdded.add() - updateAddButton(false) - return - } else { - TokenAlreadyAdded.remove() - } - - val state = hubState - when { - // token - state.tokensFieldsIsFilled() && state.networkIsSelected() -> { - val error = ContractAddress.validateValue(ContractAddress.getFieldValue()) - updateAddButton(error == null) - } - // token - state.tokensAnyFieldsIsFilled() -> { - updateAddButton(false) - } - // blockchain - else -> { - if (state.networkIsSelected()) { - val alreadyAdded = isBlockchainPersistIntoAppSavedTokensList() - if (alreadyAdded) { - updateAddButton(false) - } else { - updateAddButton(true) - } - } else { - updateAddButton(false) - } - } - } - } - /** * These are helper functions. */ + private fun isPersistIntoAppSavedTokensList(): Boolean = when (hubState.getCustomTokenType()) { + CustomTokenType.Blockchain -> isBlockchainPersistIntoAppSavedTokensList() + CustomTokenType.Token -> isTokenPersistIntoAppSavedTokensList() + } + private fun isTokenPersistIntoAppSavedTokensList( - tokenId: String? = hubState.tokenId, + tokenId: String? = hubState.foundToken?.id, tokenContractAddress: String = ContractAddress.getFieldValue(), tokenNetworkId: String = Network.getFieldValue().toNetworkId(), selectedDerivation: Blockchain = DerivationPath.getFieldValue() @@ -430,7 +420,15 @@ internal class AddCustomTokenHub : BaseStoreHub("AddCustomT dispatchOnMain(UpdateForm(hubState)) } - private suspend fun updateTokenDetailFields(isEnabled: Boolean = true) { + private suspend fun enableTokenDetailFields() { + enableDisableTokenDetailFields(true) + } + + private suspend fun disableTokenDetailFields() { + enableDisableTokenDetailFields(false) + } + + private suspend fun enableDisableTokenDetailFields(isEnabled: Boolean = true) { val state = hubState val action = Screen.UpdateTokenFields(listOf( Name to state.screenState.name.copy(isEnabled = isEnabled), @@ -440,20 +438,21 @@ internal class AddCustomTokenHub : BaseStoreHub("AddCustomT dispatchOnMain(action) } - private suspend fun updateBlockchainNetworkField(isEnabled: Boolean) { - val action = Screen.UpdateTokenFields(listOf( - Name to hubState.screenState.name.copy(isEnabled = isEnabled), - )) - dispatchOnMain(action) + private suspend fun enableAddButton() { + enableDisableAddButton(true) } - private suspend fun updateAddButton(isEnabled: Boolean) { + private suspend fun disableAddButton() { + enableDisableAddButton(false) + } + + private suspend fun enableDisableAddButton(isEnabled: Boolean) { dispatchOnMain(Screen.UpdateAddButton(ViewStates.AddButton(isEnabled))) } - private fun canHandleToken(blockchain: Blockchain): Boolean { - val card = globalState.scanResponse?.card ?: return false - return card.canHandleToken(blockchain) + private fun tokenIsSupported(blockchain: Blockchain): Boolean = when (blockchain) { + Blockchain.Unknown -> true + else -> globalState.scanResponse?.card?.canHandleToken(blockchain) ?: false } @Throws @@ -537,19 +536,6 @@ internal class AddCustomTokenHub : BaseStoreHub("AddCustomT } } - /** - * The field is being validated. - * If there is an error, then it adds it to the field. - */ - private suspend fun CustomTokenFieldId.validateAndUpdateError(value: Any): AddCustomTokenError? { - val error = this.validateValue(value) - when (error) { - null -> this.removeError() - else -> this.addError(error) - } - return error - } - private suspend fun AddCustomTokenError.Warning.add() { dispatchOnMain(Warning.Add(setOf(this))) } @@ -643,8 +629,8 @@ private class AddCustomTokenReducer( val newMap = state.formErrors.toMutableMap().apply { remove(action.id) } state.copy(formErrors = newMap) } - is SetFoundTokenId -> { - state.copy(tokenId = action.id) + is SetFoundTokenInfo -> { + state.copy(foundToken = action.foundToken) } is Warning.Add -> { val newList = state.warnings.toMutableSet().apply { addAll(action.warnings) } 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 7f958eaa58..ed18fe69f5 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 @@ -14,6 +14,7 @@ import com.tangem.domain.features.addCustomToken.* import com.tangem.domain.features.addCustomToken.CustomTokenFieldId.* import com.tangem.domain.redux.DomainState import com.tangem.domain.redux.state.StringActionStateConverter +import com.tangem.network.api.tangemTech.CoinsResponse import org.rekotlin.Action import org.rekotlin.StateType @@ -24,7 +25,7 @@ data class AddCustomTokenState( val form: Form = Form(listOf()), val formValidators: Map> = createFormValidators(), val formErrors: Map = emptyMap(), - val tokenId: String? = null, + val foundToken: CoinsResponse.Coin? = null, val warnings: Set = emptySet(), val screenState: ScreenState = createInitialScreenState(), val tangemTechServiceManager: AddCustomTokenService? = null @@ -116,7 +117,7 @@ data class AddCustomTokenState( cardDerivationStyle = null, form = Form(createFormFields(card, CustomTokenType.Blockchain)), formErrors = emptyMap(), - tokenId = null, + foundToken = null, warnings = emptySet(), screenState = createInitialScreenState(), tangemTechServiceManager = null, @@ -124,7 +125,7 @@ data class AddCustomTokenState( } private fun getToken(): CustomCurrency.CustomToken { - return CustomCurrency.CustomToken.Converter(tokenId, cardDerivationStyle) + return CustomCurrency.CustomToken.Converter(foundToken?.id, cardDerivationStyle) .apply { visitDataConverter(this) } .getConvertedData() }