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 20f72a0a89..690f35f388 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 @@ -7,6 +7,8 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.res.colorResource import androidx.compose.ui.res.painterResource import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.font.FontWeight @@ -68,6 +70,7 @@ fun PasteButton( enabled: Boolean = true, dpSize: DpSize = DpSize(40.dp, 40.dp), onClick: () -> Unit, + tint: Color? = null, content: @Composable (() -> Unit)? = null ) { IconButton( @@ -77,8 +80,42 @@ fun PasteButton( ) { when (content) { null -> { - val icon = if (enabled) R.drawable.ic_paste else R.drawable.ic_paste_disabled - Icon(painterResource(id = icon), contentDescription = "Paste") + val tintColor = tint + ?: colorResource(id = if (enabled) R.color.accent else R.color.accent_disabled) + Icon( + painterResource(id = R.drawable.ic_paste), + contentDescription = "Paste", + tint = tintColor, + ) + } + else -> content() + } + } +} + +@Composable +fun ClearButton( + modifier: Modifier = Modifier, + enabled: Boolean = true, + dpSize: DpSize = DpSize(40.dp, 40.dp), + onClick: () -> Unit, + tint: Color? = null, + content: @Composable (() -> Unit)? = null +) { + IconButton( + modifier = modifier.size(dpSize), + enabled = enabled, + onClick = onClick, + ) { + when (content) { + null -> { + val tintColor = tint + ?: colorResource(id = if (enabled) R.color.accent else R.color.accent_disabled) + Icon( + painterResource(id = R.drawable.ic_clear), + contentDescription = "Clear", + tint = tintColor, + ) } else -> content() } diff --git a/app/src/main/java/com/tangem/tap/common/compose/ComposableValueDebouncer.kt b/app/src/main/java/com/tangem/tap/common/compose/ComposableValueDebouncer.kt index 2e2288d721..b3251f3cac 100644 --- a/app/src/main/java/com/tangem/tap/common/compose/ComposableValueDebouncer.kt +++ b/app/src/main/java/com/tangem/tap/common/compose/ComposableValueDebouncer.kt @@ -11,7 +11,7 @@ import com.tangem.domain.common.util.ValueDebouncer @Composable fun valueDebouncerAsState( initialValue: T, - debounce: Long = 400, + debounce: Long = 600, onEmitValueReceived: (T) -> Unit = {}, onValueChanged: (T) -> Unit, ): ValueDebouncer { diff --git a/app/src/main/java/com/tangem/tap/common/compose/OutlinedTextFieldWidget.kt b/app/src/main/java/com/tangem/tap/common/compose/OutlinedTextFieldWidget.kt index e7fd761361..4eb6f5e2ed 100644 --- a/app/src/main/java/com/tangem/tap/common/compose/OutlinedTextFieldWidget.kt +++ b/app/src/main/java/com/tangem/tap/common/compose/OutlinedTextFieldWidget.kt @@ -89,7 +89,7 @@ private fun OutlinedProgressTextField( onTextChanged: (String) -> Unit, ) { val logger = remember { - CompositionLogger(label, "OutlinedProgressTextField", listOf("Адрес контракта")) + CompositionLogger(label, "OutlinedProgressTextField", listOf("Символ токена")) } logger.nextComposition() @@ -121,7 +121,7 @@ private fun OutlinedProgressTextField( logger.log("$isNotUserInput: внешние данные ОДИНАКОВЫ с данными в поле") } else { logger.log("$isNotUserInput: внешние данные РАЗЛИЧАЮТСЯ с данными в поле") - if (textDebouncer.emittedValue != textDebouncer.debounced) { + if ((textDebouncer.emittedValue != textDebouncer.debounced) || textDebouncer.emitsCountBeforeDebounce > 0) { logger.log("$isNotUserInput: пользователь ВВОДИТ данные -> внешние данные игнорируем, ждем RECOMPOSE") } else { logger.log("$isNotUserInput: пользователь НЕ вводит данные -> пытаемся обработать внешние данные") @@ -135,7 +135,7 @@ private fun OutlinedProgressTextField( textValueState.value = fieldData.value } } else { - logger.log("$isNotUserInput: UNKNOWN -> start RECOMPOSE by new value for textValueState.value = [${fieldData.value}]") + logger.log("$isNotUserInput: в пустое поле вставляются данные -> start RECOMPOSE новые данные для textValueState.value = [${fieldData.value}]") textValueState.value = fieldData.value } } 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 d9736ae8d0..082182472c 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 @@ -141,6 +141,11 @@ private fun AddCustomTokenFab( } else { Color(0xFFB9E6D3) } + val elevation = if (!isEnabled) { + FloatingActionButtonDefaults.elevation(0.dp, 0.dp) + } else { + FloatingActionButtonDefaults.elevation() + } ToggledRippleTheme(isEnabled) { ExtendedFloatingActionButton( @@ -156,6 +161,7 @@ private fun AddCustomTokenFab( onClick = { if (isEnabled) onClick() }, backgroundColor = backgroundColor, contentColor = contentColor, + elevation = elevation, ) } } 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 index 7a61b5c911..d8a89b6ae8 100644 --- 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 @@ -2,19 +2,19 @@ package com.tangem.tap.features.tokens.addCustomToken.compose import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.runtime.Composable +import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.input.KeyboardType +import androidx.compose.ui.tooling.preview.Preview 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.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.tap.common.compose.* +import com.tangem.tap.common.extensions.getFromClipboard import com.tangem.wallet.R /** @@ -34,12 +34,38 @@ fun TokenContractAddressView(screenFieldData: ScreenFieldData) { isLoading = screenFieldData.viewState.isLoading, error = screenFieldData.error, errorConverter = screenFieldData.errorConverter, +// trailingIcon = { PasteClearButton(showFirst = tokenField.data.value.isEmpty()) } ) { - domainStore.dispatch(AddCustomTokenAction.OnTokenContractAddressChanged(Field.Data(it, true))) + domainStore.dispatch(OnTokenContractAddressChanged(Field.Data(it, true))) } SpacerH8() } +@Composable +private fun PasteClearButton( + showFirst: Boolean, +) { + if (showFirst) { + val context = LocalContext.current + PasteButton( + onClick = { + context.getFromClipboard()?.let { + val fieldData = Field.Data(it.toString(), false) + domainStore.dispatch(OnTokenContractAddressChanged(fieldData)) + } + + } + ) + } else { + ClearButton( + onClick = { + val fieldData = Field.Data("", false) + domainStore.dispatch(OnTokenContractAddressChanged(fieldData)) + } + ) + } +} + @Composable fun TokenNameView(screenFieldData: ScreenFieldData) { if (!screenFieldData.viewState.isVisible) return @@ -54,7 +80,7 @@ fun TokenNameView(screenFieldData: ScreenFieldData) { error = screenFieldData.error, errorConverter = screenFieldData.errorConverter, ) { - domainStore.dispatch(AddCustomTokenAction.OnTokenNameChanged(Field.Data(it, true))) + domainStore.dispatch(OnTokenNameChanged(Field.Data(it, true))) } SpacerH8() } @@ -72,7 +98,7 @@ fun TokenNetworkView(screenFieldData: ScreenFieldData, state: AddCustomTokenStat selectedItem = networkField.data, isEnabled = screenFieldData.viewState.isEnabled, textFieldConverter = { state.blockchainToName(it) ?: notSelected }, - ) { domainStore.dispatch(AddCustomTokenAction.OnTokenNetworkChanged(Field.Data(it, true))) } + ) { domainStore.dispatch(OnTokenNetworkChanged(Field.Data(it, true))) } SpacerH8() } @@ -89,7 +115,7 @@ fun TokenSymbolView(screenFieldData: ScreenFieldData) { isEnabled = screenFieldData.viewState.isEnabled, error = screenFieldData.error, errorConverter = screenFieldData.errorConverter, - ) { domainStore.dispatch(AddCustomTokenAction.OnTokenSymbolChanged(Field.Data(it, true))) } + ) { domainStore.dispatch(OnTokenSymbolChanged(Field.Data(it, true))) } SpacerH8() } @@ -107,7 +133,7 @@ fun TokenDecimalsView(screenFieldData: ScreenFieldData) { error = screenFieldData.error, errorConverter = screenFieldData.errorConverter, keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number), - ) { domainStore.dispatch(AddCustomTokenAction.OnTokenDecimalsChanged(Field.Data(it, true))) } + ) { domainStore.dispatch(OnTokenDecimalsChanged(Field.Data(it, true))) } SpacerH8() } @@ -129,6 +155,12 @@ fun TokenDerivationPathView(screenFieldData: ScreenFieldData, state: AddCustomTo val blockchainName = state.blockchainToName(blockchain) ?: notSelected TitleSubtitle(derivationPathName, blockchainName) } - ) { domainStore.dispatch(AddCustomTokenAction.OnTokenDerivationPathChanged(Field.Data(it, true))) } + ) { domainStore.dispatch(OnTokenDerivationPathChanged(Field.Data(it, true))) } SpacerH8() +} + +@Preview +@Composable +fun TestTokenContractAddressView() { + } \ No newline at end of file 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 0bf2afeb58..05014acd99 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 @@ -62,7 +62,7 @@ fun List.filter(supportedBlockchains: Set?): List Unit) { ) }, onClick = onSaveChanges, - backgroundColor = Color(0xFF1ACE80), + backgroundColor = colorResource(id = R.color.accent), contentColor = Color.White, modifier = Modifier.padding(bottom = padding.dp) ) diff --git a/app/src/main/res/drawable/ic_clear.xml b/app/src/main/res/drawable/ic_clear.xml new file mode 100644 index 0000000000..2f59f4cb34 --- /dev/null +++ b/app/src/main/res/drawable/ic_clear.xml @@ -0,0 +1,10 @@ + + + + diff --git a/app/src/main/res/drawable/ic_paste.xml b/app/src/main/res/drawable/ic_paste.xml index 5bc825fa49..c49d12dc4b 100644 --- a/app/src/main/res/drawable/ic_paste.xml +++ b/app/src/main/res/drawable/ic_paste.xml @@ -4,6 +4,6 @@ android:viewportWidth="16" android:viewportHeight="19"> + android:fillColor="#1C1C1E" + android:pathData="M13.8333,1.6667H10.35C10,0.7 9.0833,0 8,0C6.9167,0 6,0.7 5.65,1.6667H2.1667C1.25,1.6667 0.5,2.4167 0.5,3.3333V16.6667C0.5,17.5833 1.25,18.3333 2.1667,18.3333H13.8333C14.75,18.3333 15.5,17.5833 15.5,16.6667V3.3333C15.5,2.4167 14.75,1.6667 13.8333,1.6667ZM8,1.6667C8.4583,1.6667 8.8333,2.0417 8.8333,2.5C8.8333,2.9583 8.4583,3.3333 8,3.3333C7.5417,3.3333 7.1667,2.9583 7.1667,2.5C7.1667,2.0417 7.5417,1.6667 8,1.6667ZM13.8333,16.6667H2.1667V3.3333H3.8333V5.8333H12.1667V3.3333H13.8333V16.6667Z" /> diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml index ecb9562aad..70a942b055 100644 --- a/app/src/main/res/values/colors.xml +++ b/app/src/main/res/values/colors.xml @@ -3,21 +3,23 @@ #0029FF #0022D4 - #1ACE80 + #19C878 + #801ACE80 - @color/backgroundLightGray + @color/accent @color/accent - #661ACE80 + @color/accent_disabled @color/darkGray6 #6A6A6A - #5AC461 + @color/accent #C4291C #FFB71B - #FFFFFF + @color/backgroundLightGray + #FFFFFF #F3F3F3 #F8F8FB #DADADF @@ -62,6 +64,4 @@ #CA0F03 #EFEFF0 - - #18C077 \ No newline at end of file diff --git a/domain/src/main/java/com/tangem/domain/common/util/ValueDebouncer.kt b/domain/src/main/java/com/tangem/domain/common/util/ValueDebouncer.kt index 83e856abb7..919157a26c 100644 --- a/domain/src/main/java/com/tangem/domain/common/util/ValueDebouncer.kt +++ b/domain/src/main/java/com/tangem/domain/common/util/ValueDebouncer.kt @@ -1,10 +1,10 @@ package com.tangem.domain.common.util -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.Job -import kotlinx.coroutines.flow.* -import kotlinx.coroutines.launch +import kotlinx.coroutines.* +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.collect +import kotlinx.coroutines.flow.debounce +import kotlinx.coroutines.flow.onEach /** [REDACTED_AUTHOR] @@ -18,6 +18,8 @@ class ValueDebouncer( var emittedValue: T = initialValue private set + var emitsCountBeforeDebounce: Int = 0 + private set var debounced: T = initialValue private set @@ -26,11 +28,14 @@ class ValueDebouncer( init { debounceScope.launch { - flow.filter { if (debounced == null) true else debounced != it } - .debounce(debounceDuration) + flow.debounce(debounceDuration) .onEach { debounced = it onValueChanged(it) + debounceScope.launch { + delay(500) + emitsCountBeforeDebounce = 0 + } } .collect() } @@ -39,6 +44,7 @@ class ValueDebouncer( fun isDebounced(value: T): Boolean = this.debounced == value fun emmit(emmitValue: T) { + emitsCountBeforeDebounce++ emittedValue = emmitValue onEmitValueReceived.invoke(emmitValue) debounceScope.launch { flow.emit(emmitValue) } 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 c526237f5a..5d21995d73 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 @@ -59,48 +59,23 @@ internal class AddCustomTokenHub : BaseStoreHub("AddCustomT } is OnDestroy -> cancelAll() is OnTokenContractAddressChanged -> { - val address = action.contractAddress.value + val contractAddress = action.contractAddress.value - when (val error = ContractAddress.validateValue(address)) { - null -> { - // valid contract address - ContractAddress.removeError() - updateTokenDetailFields(false) - changeBlockchainNetworkList() - checkAndUpdateAddButton() - manageFoundTokenChanges(requestInfoAboutToken(address)) - } - AddCustomTokenError.FieldIsEmpty -> { - // empty contract address - ContractAddress.removeError() - clearTokenDetailsFields() - updateTokenDetailFields(false) - changeBlockchainNetworkList() - checkAndUpdateAddButton() - } - AddCustomTokenError.InvalidContractAddress -> { - ContractAddress.addError(error) - updateTokenDetailFields(hubState.tokensAnyFieldsIsFilled()) - changeBlockchainNetworkList() - checkAndUpdateAddButton() - } - } + validateContractAddressAndNotify(contractAddress) } is OnTokenNetworkChanged -> { if (!action.blockchainNetwork.isUserInput) return val contractAddress = ContractAddress.getFieldValue() - val error = ContractAddress.validateValue(contractAddress) - if (error == null && contractAddress.isNotEmpty()) { - // token branch - manageFoundTokenChanges(requestInfoAboutToken(contractAddress)) - } else { - // blockchain branch - val isAlreadyAdded = isBlockchainPersistIntoAppSavedTokensList( - selectedNetwork = action.blockchainNetwork.value - ) - updateWarningAlreadyAdded(isAlreadyAdded) - checkAndUpdateAddButton() + when (validateContractAddressAndNotify(contractAddress)) { + is AddCustomTokenError.InvalidContractAddress -> { + val isAlreadyAdded = isBlockchainPersistIntoAppSavedTokensList( + selectedNetwork = action.blockchainNetwork.value + ) + updateWarningAlreadyAdded(isAlreadyAdded) + checkAndUpdateAddButton() + } + else -> {} } } is OnTokenDerivationPathChanged -> { @@ -118,19 +93,7 @@ internal class AddCustomTokenHub : BaseStoreHub("AddCustomT updateWarningAlreadyAdded(isAlreadyAdded) checkAndUpdateAddButton() } - is OnTokenNameChanged -> { -// changeBlockchainNetworkList() -// updateTokenDetailFields(hubState.tokensAnyFieldsIsFilled()) - checkAndUpdateAddButton() - } - is OnTokenSymbolChanged -> { -// changeBlockchainNetworkList() -// updateTokenDetailFields(hubState.tokensAnyFieldsIsFilled()) - checkAndUpdateAddButton() - } - is OnTokenDecimalsChanged -> { -// changeBlockchainNetworkList() -// updateTokenDetailFields(hubState.tokensAnyFieldsIsFilled()) + is OnTokenNameChanged, is OnTokenSymbolChanged, is OnTokenDecimalsChanged -> { checkAndUpdateAddButton() } is OnAddCustomTokenClicked -> { @@ -157,6 +120,35 @@ internal class AddCustomTokenHub : BaseStoreHub("AddCustomT } } + private suspend fun validateContractAddressAndNotify(contractAddress: String): AddCustomTokenError? { + val error = ContractAddress.validateValue(contractAddress) + when (error) { + null -> { + // valid contract address + ContractAddress.removeError() + updateTokenDetailFields(false) + changeBlockchainNetworkList() + checkAndUpdateAddButton() + manageFoundTokenChanges(requestInfoAboutToken(contractAddress)) + } + AddCustomTokenError.FieldIsEmpty -> { + // empty contract address + ContractAddress.removeError() + clearTokenDetailsFields() + updateTokenDetailFields(false) + changeBlockchainNetworkList() + checkAndUpdateAddButton() + } + AddCustomTokenError.InvalidContractAddress -> { + ContractAddress.addError(error) + updateTokenDetailFields(hubState.tokensAnyFieldsIsFilled()) + changeBlockchainNetworkList() + checkAndUpdateAddButton() + } + } + return error + } + /** * This feature is only needed until Solana coins are added. * While they are not there - this function excludes the Solana blockchain if the user has 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 77177b013c..7db7052347 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 @@ -172,10 +172,10 @@ data class AddCustomTokenState( Blockchain.Unknown, Blockchain.Ethereum, Blockchain.BSC, - Blockchain.Binance, // not evm Blockchain.Polygon, Blockchain.Avalanche, Blockchain.Fantom, + Blockchain.Binance, // not evm Blockchain.Solana, // not evm. Should be unsupported for coins until they are added to the Blockchain SDK ) if (type == CustomTokenType.Token) networks.remove(Blockchain.Solana)