Updated on 2026-08-14
This commit is contained in:
commit
3981db48c8
7 changed files with 420 additions and 428 deletions
|
|
@ -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<AddCustomTokenState>,
|
||||
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<AddCustomTokenState>,
|
||||
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
|
||||
|
|
|
|||
|
|
@ -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<Pair<String, String>>,
|
||||
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) }
|
||||
}
|
||||
|
|
@ -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)))
|
||||
}
|
||||
|
|
@ -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) }), ;
|
||||
}
|
||||
|
|
@ -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<String>) : 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()
|
||||
|
|
|
|||
|
|
@ -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<AddCustomTokenState>("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<AddCustomTokenState>("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<TokenBlockchainField>().itemList
|
||||
val card = requireNotNull(globalState.scanResponse?.card)
|
||||
val newNetworkBlockchainList: List<Blockchain> = state.getNetworks(card, state.getCustomTokenType())
|
||||
|
||||
val listsIdentical = newNetworkBlockchainList.toSet() == networkBlockchainList.toSet()
|
||||
if (listsIdentical) return
|
||||
|
||||
val networkFieldIsFilled = Network.isFilled()
|
||||
val newNetworkField = Network.getField<TokenBlockchainField>().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<Blockchain>()
|
||||
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<AddCustomTokenState>("AddCustomT
|
|||
}
|
||||
}
|
||||
|
||||
private suspend fun updateWarnings() {
|
||||
val state = hubState
|
||||
val warningsAdd = mutableSetOf<AddCustomTokenError.Warning>()
|
||||
val warningsRemove = mutableSetOf<AddCustomTokenError.Warning>()
|
||||
|
||||
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<String>())
|
||||
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<CoinsResponse.Coin> {
|
||||
|
|
@ -232,132 +338,16 @@ internal class AddCustomTokenHub : BaseStoreHub<AddCustomTokenState>("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<String>())
|
||||
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<Blockchain>().toNetworkId(),
|
||||
selectedDerivation: Blockchain = DerivationPath.getFieldValue()
|
||||
|
|
@ -430,7 +420,15 @@ internal class AddCustomTokenHub : BaseStoreHub<AddCustomTokenState>("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<AddCustomTokenState>("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<AddCustomTokenState>("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) }
|
||||
|
|
|
|||
|
|
@ -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<CustomTokenFieldId, CustomTokenValidator<out Any>> = createFormValidators(),
|
||||
val formErrors: Map<CustomTokenFieldId, AddCustomTokenError> = emptyMap(),
|
||||
val tokenId: String? = null,
|
||||
val foundToken: CoinsResponse.Coin? = null,
|
||||
val warnings: Set<AddCustomTokenError.Warning> = 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()
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue