Updated on 2026-08-14
This commit is contained in:
commit
64a40de825
10 changed files with 769 additions and 139 deletions
|
|
@ -5,33 +5,24 @@ import androidx.compose.runtime.getValue
|
|||
import androidx.compose.ui.Modifier
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import com.tangem.core.decompose.context.AppComponentContext
|
||||
import com.tangem.core.ui.extensions.resourceReference
|
||||
import com.tangem.core.ui.extensions.stringReference
|
||||
import com.tangem.core.decompose.model.getOrCreateModel
|
||||
import com.tangem.features.managetokens.component.CustomTokenFormComponent
|
||||
import com.tangem.features.managetokens.entity.customtoken.ClickableFieldUM
|
||||
import com.tangem.features.managetokens.entity.customtoken.CustomTokenFormUM
|
||||
import com.tangem.features.managetokens.entity.customtoken.CustomTokenFormValues
|
||||
import com.tangem.features.managetokens.entity.customtoken.TextInputFieldUM
|
||||
import com.tangem.features.managetokens.impl.R
|
||||
import com.tangem.features.managetokens.model.CustomTokenFormModel
|
||||
import com.tangem.features.managetokens.ui.CustomTokenFormContent
|
||||
import dagger.assisted.Assisted
|
||||
import dagger.assisted.AssistedFactory
|
||||
import dagger.assisted.AssistedInject
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.update
|
||||
|
||||
internal class DefaultCustomTokenFormComponent @AssistedInject constructor(
|
||||
@Assisted context: AppComponentContext,
|
||||
@Assisted private val params: CustomTokenFormComponent.Params,
|
||||
) : CustomTokenFormComponent, AppComponentContext by context {
|
||||
|
||||
private val state: MutableStateFlow<CustomTokenFormUM> = MutableStateFlow(
|
||||
value = getInitialState(),
|
||||
)
|
||||
private val model: CustomTokenFormModel = getOrCreateModel(params)
|
||||
|
||||
@Composable
|
||||
override fun Content(modifier: Modifier) {
|
||||
val state by state.collectAsStateWithLifecycle()
|
||||
val state by model.state.collectAsStateWithLifecycle()
|
||||
|
||||
CustomTokenFormContent(
|
||||
modifier = modifier,
|
||||
|
|
@ -39,114 +30,6 @@ internal class DefaultCustomTokenFormComponent @AssistedInject constructor(
|
|||
)
|
||||
}
|
||||
|
||||
private fun getInitialState(): CustomTokenFormUM {
|
||||
return CustomTokenFormUM(
|
||||
networkName = ClickableFieldUM(
|
||||
label = resourceReference(R.string.custom_token_network_input_title),
|
||||
value = params.network.name,
|
||||
onClick = ::selectNetwork,
|
||||
),
|
||||
tokenForm = getInitialTokenForm(),
|
||||
derivationPath = ClickableFieldUM(
|
||||
label = resourceReference(R.string.custom_token_derivation_path),
|
||||
value = params.derivationPath.networkName,
|
||||
onClick = ::selectDerivationPath,
|
||||
),
|
||||
saveToken = {
|
||||
// TODO: Save token: [REDACTED_JIRA]
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
private fun getInitialTokenForm(): CustomTokenFormUM.TokenFormUM {
|
||||
val formValues = params.formValues
|
||||
|
||||
val form = CustomTokenFormUM.TokenFormUM(
|
||||
contractAddress = TextInputFieldUM(
|
||||
label = resourceReference(R.string.custom_token_contract_address_input_title),
|
||||
placeholder = stringReference(value = "0x000000000000000000000000000..."),
|
||||
onValueChange = ::updateContractAddress,
|
||||
),
|
||||
name = TextInputFieldUM(
|
||||
label = resourceReference(R.string.custom_token_name_input_title),
|
||||
placeholder = resourceReference(R.string.custom_token_name_input_placeholder),
|
||||
onValueChange = ::updateTokenName,
|
||||
),
|
||||
symbol = TextInputFieldUM(
|
||||
label = resourceReference(R.string.custom_token_token_symbol_input_title),
|
||||
placeholder = resourceReference(R.string.custom_token_token_symbol_input_placeholder),
|
||||
onValueChange = ::updateTokenSymbol,
|
||||
),
|
||||
decimals = TextInputFieldUM(
|
||||
label = resourceReference(R.string.custom_token_decimals_input_title),
|
||||
placeholder = stringReference(value = "0"),
|
||||
onValueChange = ::updateDecimals,
|
||||
),
|
||||
)
|
||||
|
||||
return formValues.fillValues(form)
|
||||
}
|
||||
|
||||
private fun updateContractAddress(value: String) {
|
||||
// TODO: Add field validation: [REDACTED_JIRA]
|
||||
state.update { state ->
|
||||
val tokenForm = state.tokenForm ?: return@update state
|
||||
|
||||
state.copy(
|
||||
tokenForm = tokenForm.copy(
|
||||
contractAddress = tokenForm.contractAddress.copy(value = value),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateTokenName(value: String) {
|
||||
// TODO: Add field validation: [REDACTED_JIRA]
|
||||
state.update { state ->
|
||||
val tokenForm = state.tokenForm ?: return@update state
|
||||
|
||||
state.copy(
|
||||
tokenForm = tokenForm.copy(
|
||||
name = tokenForm.name.copy(value = value),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateTokenSymbol(value: String) {
|
||||
// TODO: Add field validation: [REDACTED_JIRA]
|
||||
state.update { state ->
|
||||
val tokenForm = state.tokenForm ?: return@update state
|
||||
|
||||
state.copy(
|
||||
tokenForm = tokenForm.copy(
|
||||
symbol = tokenForm.symbol.copy(value = value),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateDecimals(value: String) {
|
||||
// TODO: Add field validation: [REDACTED_JIRA]
|
||||
state.update { state ->
|
||||
val tokenForm = state.tokenForm ?: return@update state
|
||||
|
||||
state.copy(
|
||||
tokenForm = tokenForm.copy(
|
||||
decimals = tokenForm.decimals.copy(value = value),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun selectNetwork() {
|
||||
params.onSelectNetworkClick(CustomTokenFormValues(state.value.tokenForm))
|
||||
}
|
||||
|
||||
private fun selectDerivationPath() {
|
||||
params.onSelectDerivationPathClick(CustomTokenFormValues(state.value.tokenForm))
|
||||
}
|
||||
|
||||
@AssistedFactory
|
||||
interface Factory : CustomTokenFormComponent.Factory {
|
||||
override fun create(
|
||||
|
|
@ -154,8 +37,4 @@ internal class DefaultCustomTokenFormComponent @AssistedInject constructor(
|
|||
params: CustomTokenFormComponent.Params,
|
||||
): DefaultCustomTokenFormComponent
|
||||
}
|
||||
|
||||
private companion object {
|
||||
const val FORM_VALUES_KEY = "form_values"
|
||||
}
|
||||
}
|
||||
|
|
@ -11,7 +11,7 @@ import com.tangem.features.managetokens.entity.customtoken.CustomTokenFormUM
|
|||
import com.tangem.features.managetokens.entity.customtoken.TextInputFieldUM
|
||||
import com.tangem.features.managetokens.impl.R
|
||||
import com.tangem.features.managetokens.ui.CustomTokenFormContent
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.collections.immutable.PersistentList
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
|
||||
internal class PreviewCustomTokenFormComponent(
|
||||
|
|
@ -19,7 +19,7 @@ internal class PreviewCustomTokenFormComponent(
|
|||
derivationPath: ClickableFieldUM = PreviewCustomTokenFormComponent.derivationPath,
|
||||
canAddToken: Boolean = false,
|
||||
tokenForm: CustomTokenFormUM.TokenFormUM? = PreviewCustomTokenFormComponent.tokenForm,
|
||||
notifications: ImmutableList<CustomTokenFormUM.NotificationUM> = PreviewCustomTokenFormComponent.notifications,
|
||||
notifications: PersistentList<CustomTokenFormUM.NotificationUM> = PreviewCustomTokenFormComponent.notifications,
|
||||
) : CustomTokenFormComponent {
|
||||
|
||||
private val previewState = CustomTokenFormUM(
|
||||
|
|
@ -73,7 +73,7 @@ internal class PreviewCustomTokenFormComponent(
|
|||
onValueChange = {},
|
||||
),
|
||||
)
|
||||
val notifications: ImmutableList<CustomTokenFormUM.NotificationUM> = persistentListOf(
|
||||
val notifications: PersistentList<CustomTokenFormUM.NotificationUM> = persistentListOf(
|
||||
CustomTokenFormUM.NotificationUM(
|
||||
id = "1",
|
||||
config = NotificationConfig(
|
||||
|
|
|
|||
|
|
@ -2,15 +2,16 @@ package com.tangem.features.managetokens.entity.customtoken
|
|||
|
||||
import com.tangem.core.ui.components.notifications.NotificationConfig
|
||||
import com.tangem.core.ui.extensions.TextReference
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.collections.immutable.PersistentList
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
|
||||
internal data class CustomTokenFormUM(
|
||||
val networkName: ClickableFieldUM,
|
||||
val derivationPath: ClickableFieldUM,
|
||||
val tokenForm: TokenFormUM?,
|
||||
val notifications: ImmutableList<NotificationUM> = persistentListOf(),
|
||||
val notifications: PersistentList<NotificationUM> = persistentListOf(),
|
||||
val canAddToken: Boolean = false,
|
||||
val isValidating: Boolean = false,
|
||||
val saveToken: () -> Unit,
|
||||
) {
|
||||
|
||||
|
|
@ -32,6 +33,7 @@ internal data class TextInputFieldUM(
|
|||
val placeholder: TextReference,
|
||||
val value: String = "",
|
||||
val error: TextReference? = null,
|
||||
val isEnabled: Boolean = true,
|
||||
val onValueChange: (String) -> Unit,
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
package com.tangem.features.managetokens.entity.customtoken
|
||||
|
||||
import com.tangem.domain.managetokens.model.AddCustomTokenForm
|
||||
import com.tangem.features.managetokens.entity.customtoken.CustomTokenFormUM.TokenFormUM
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
|
|
@ -28,4 +29,17 @@ internal value class CustomTokenFormValues private constructor(private val value
|
|||
symbol = to.symbol.copy(value = values.getOrElse(index = 2) { "" }),
|
||||
decimals = to.decimals.copy(value = values.getOrElse(index = 3) { "" }),
|
||||
)
|
||||
|
||||
fun toDomainModel(): AddCustomTokenForm.Raw? {
|
||||
return if (values.isEmpty()) {
|
||||
null
|
||||
} else {
|
||||
AddCustomTokenForm.Raw(
|
||||
contractAddress = values.getOrElse(index = 0) { "" },
|
||||
name = values.getOrElse(index = 1) { "" },
|
||||
symbol = values.getOrElse(index = 2) { "" },
|
||||
decimals = values.getOrElse(index = 3) { "" },
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,311 @@
|
|||
package com.tangem.features.managetokens.model
|
||||
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import com.tangem.core.decompose.di.ComponentScoped
|
||||
import com.tangem.core.decompose.model.Model
|
||||
import com.tangem.core.decompose.model.ParamsContainer
|
||||
import com.tangem.core.decompose.ui.UiMessageSender
|
||||
import com.tangem.core.ui.components.SimpleOkDialog
|
||||
import com.tangem.core.ui.extensions.resourceReference
|
||||
import com.tangem.core.ui.extensions.stringReference
|
||||
import com.tangem.core.ui.message.ContentMessage
|
||||
import com.tangem.domain.managetokens.model.exceptoin.CustomTokenFormValidationException
|
||||
import com.tangem.domain.tokens.model.CryptoCurrency
|
||||
import com.tangem.features.managetokens.component.CustomTokenFormComponent
|
||||
import com.tangem.features.managetokens.entity.customtoken.ClickableFieldUM
|
||||
import com.tangem.features.managetokens.entity.customtoken.CustomTokenFormUM
|
||||
import com.tangem.features.managetokens.entity.customtoken.CustomTokenFormValues
|
||||
import com.tangem.features.managetokens.entity.customtoken.TextInputFieldUM
|
||||
import com.tangem.features.managetokens.impl.R
|
||||
import com.tangem.features.managetokens.utils.CustomCurrencyValidator
|
||||
import com.tangem.features.managetokens.utils.mapper.mapToDomainModel
|
||||
import com.tangem.features.managetokens.utils.ui.*
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import kotlinx.coroutines.FlowPreview
|
||||
import kotlinx.coroutines.flow.*
|
||||
import kotlinx.coroutines.launch
|
||||
import timber.log.Timber
|
||||
import javax.inject.Inject
|
||||
|
||||
@ComponentScoped
|
||||
internal class CustomTokenFormModel @Inject constructor(
|
||||
override val dispatchers: CoroutineDispatcherProvider,
|
||||
private val customCurrencyValidator: CustomCurrencyValidator,
|
||||
private val messageSender: UiMessageSender,
|
||||
paramsContainer: ParamsContainer,
|
||||
) : Model() {
|
||||
|
||||
private val params: CustomTokenFormComponent.Params = paramsContainer.require()
|
||||
|
||||
val state: MutableStateFlow<CustomTokenFormUM> = MutableStateFlow(
|
||||
value = getInitialState(),
|
||||
)
|
||||
|
||||
init {
|
||||
observeValidatorUpdates()
|
||||
|
||||
if (params.network.canHandleTokens) {
|
||||
observeTokenFormUpdates()
|
||||
} else {
|
||||
modelScope.launch {
|
||||
customCurrencyValidator.createCoin(
|
||||
userWalletId = params.userWalletId,
|
||||
networkId = params.network.id,
|
||||
derivationPath = params.derivationPath.value,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun getInitialState(): CustomTokenFormUM {
|
||||
return CustomTokenFormUM(
|
||||
networkName = ClickableFieldUM(
|
||||
label = resourceReference(R.string.custom_token_network_input_title),
|
||||
value = params.network.name,
|
||||
onClick = ::selectNetwork,
|
||||
),
|
||||
tokenForm = if (params.network.canHandleTokens) {
|
||||
getInitialTokenForm()
|
||||
} else {
|
||||
null
|
||||
},
|
||||
derivationPath = ClickableFieldUM(
|
||||
label = resourceReference(R.string.custom_token_derivation_path),
|
||||
value = if (params.derivationPath.id == params.network.id) {
|
||||
resourceReference(R.string.custom_token_derivation_path_default)
|
||||
} else {
|
||||
params.derivationPath.networkName
|
||||
},
|
||||
onClick = ::selectDerivationPath,
|
||||
),
|
||||
saveToken = {
|
||||
// TODO: Save token: [REDACTED_JIRA]
|
||||
val dialog = ContentMessage { onDismiss ->
|
||||
SimpleOkDialog(
|
||||
message = "Not yet implemented",
|
||||
onDismissDialog = onDismiss,
|
||||
)
|
||||
}
|
||||
|
||||
messageSender.send(dialog)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
@OptIn(FlowPreview::class)
|
||||
private fun observeTokenFormUpdates() {
|
||||
state
|
||||
.sample(periodMillis = 1_000)
|
||||
.transform {
|
||||
val values = it.tokenForm?.mapToDomainModel()
|
||||
|
||||
if (values != null) {
|
||||
emit(values)
|
||||
}
|
||||
}
|
||||
.distinctUntilChanged()
|
||||
.onEach { formValues ->
|
||||
customCurrencyValidator.validateForm(
|
||||
userWalletId = params.userWalletId,
|
||||
networkId = params.network.id,
|
||||
derivationPath = params.derivationPath.value,
|
||||
formValues = formValues,
|
||||
)
|
||||
}
|
||||
.launchIn(modelScope)
|
||||
}
|
||||
|
||||
private fun observeValidatorUpdates() = modelScope.launch {
|
||||
customCurrencyValidator.consumeUpdates { validatorState ->
|
||||
when (validatorState) {
|
||||
is CustomCurrencyValidator.State.NotStarted -> Unit
|
||||
is CustomCurrencyValidator.State.SearchingToken -> updateStateWithSearching()
|
||||
is CustomCurrencyValidator.State.UnexpectedException -> showErrorDialog()
|
||||
is CustomCurrencyValidator.State.FormValidationException -> updateStateWithExceptions(
|
||||
exceptions = validatorState.exceptions,
|
||||
)
|
||||
is CustomCurrencyValidator.State.TokenNotFound -> updateStateWithNotFoundNotification()
|
||||
is CustomCurrencyValidator.State.Validated -> updateStateWithCurrency(
|
||||
currency = validatorState.currency,
|
||||
fillForm = validatorState.fillForm,
|
||||
isAlreadyAdded = validatorState.isAlreadyAdded,
|
||||
isCustom = validatorState.isCustom,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateStateWithCurrency(
|
||||
currency: CryptoCurrency,
|
||||
fillForm: Boolean,
|
||||
isAlreadyAdded: Boolean,
|
||||
isCustom: Boolean,
|
||||
) {
|
||||
state.update { state ->
|
||||
var updatedState = state
|
||||
.updateWithProgress(
|
||||
showProgress = false,
|
||||
canAddToken = !isAlreadyAdded,
|
||||
clearNotifications = true,
|
||||
clearFieldErrors = true,
|
||||
disableSecondaryFields = !isCustom,
|
||||
)
|
||||
|
||||
if (fillForm) {
|
||||
updatedState = updatedState.updateWithCurrency(currency)
|
||||
}
|
||||
|
||||
if (isAlreadyAdded) {
|
||||
updatedState = updatedState.updateWithCurrencyAlreadyAddedNotification()
|
||||
}
|
||||
|
||||
if (isCustom) {
|
||||
updatedState = updatedState.updateWithCurrencyNotFoundNotification()
|
||||
}
|
||||
|
||||
updatedState
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateStateWithNotFoundNotification() {
|
||||
state.update { state ->
|
||||
state
|
||||
.updateWithProgress(
|
||||
showProgress = false,
|
||||
canAddToken = false,
|
||||
clearNotifications = true,
|
||||
clearFieldErrors = true,
|
||||
)
|
||||
.updateWithCurrencyNotFoundNotification()
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateStateWithSearching() {
|
||||
state.update { state ->
|
||||
state.updateWithProgress(
|
||||
showProgress = true,
|
||||
canAddToken = false,
|
||||
clearNotifications = false,
|
||||
clearFieldErrors = false,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun showErrorDialog() {
|
||||
val dialog = ContentMessage { onDismiss ->
|
||||
SimpleOkDialog(
|
||||
message = stringResource(R.string.common_unknown_error),
|
||||
onDismissDialog = onDismiss,
|
||||
)
|
||||
}
|
||||
|
||||
messageSender.send(dialog)
|
||||
}
|
||||
|
||||
private fun updateStateWithExceptions(exceptions: List<CustomTokenFormValidationException>) {
|
||||
state.update { state ->
|
||||
val validatedState = state
|
||||
.updateWithProgress(
|
||||
showProgress = false,
|
||||
canAddToken = false,
|
||||
clearNotifications = true,
|
||||
clearFieldErrors = true,
|
||||
)
|
||||
|
||||
exceptions.fold(validatedState) { stateAcc, exception ->
|
||||
when (exception) {
|
||||
is CustomTokenFormValidationException.ContractAddress -> {
|
||||
stateAcc.updateWithContractAddressException(exception)
|
||||
}
|
||||
is CustomTokenFormValidationException.Decimals -> {
|
||||
stateAcc.updateWithDecimalsException(exception)
|
||||
}
|
||||
is CustomTokenFormValidationException.EmptyName -> {
|
||||
stateAcc // No need to display error
|
||||
}
|
||||
is CustomTokenFormValidationException.EmptySymbol -> {
|
||||
stateAcc // No need to display error
|
||||
}
|
||||
is CustomTokenFormValidationException.DataError -> {
|
||||
Timber.e(exception.cause, "Unable to validate custom currency")
|
||||
stateAcc
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun getInitialTokenForm(): CustomTokenFormUM.TokenFormUM {
|
||||
val formValues = params.formValues
|
||||
|
||||
val form = CustomTokenFormUM.TokenFormUM(
|
||||
contractAddress = TextInputFieldUM(
|
||||
label = resourceReference(R.string.custom_token_contract_address_input_title),
|
||||
placeholder = stringReference(CONTRACT_ADDRESS_PLACEHOLDER),
|
||||
onValueChange = ::updateContractAddress,
|
||||
),
|
||||
name = TextInputFieldUM(
|
||||
label = resourceReference(R.string.custom_token_name_input_title),
|
||||
placeholder = resourceReference(R.string.custom_token_name_input_placeholder),
|
||||
onValueChange = ::updateTokenName,
|
||||
),
|
||||
symbol = TextInputFieldUM(
|
||||
label = resourceReference(R.string.custom_token_token_symbol_input_title),
|
||||
placeholder = resourceReference(R.string.custom_token_token_symbol_input_placeholder),
|
||||
onValueChange = ::updateTokenSymbol,
|
||||
),
|
||||
decimals = TextInputFieldUM(
|
||||
label = resourceReference(R.string.custom_token_decimals_input_title),
|
||||
placeholder = stringReference(DECIMALS_PLACEHOLDER),
|
||||
onValueChange = ::updateDecimals,
|
||||
),
|
||||
)
|
||||
|
||||
return formValues.fillValues(form)
|
||||
}
|
||||
|
||||
private fun updateContractAddress(value: String) {
|
||||
state.update { state ->
|
||||
state.updateTokenForm {
|
||||
copy(contractAddress = contractAddress.updateValue(value))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateTokenName(value: String) {
|
||||
state.update { state ->
|
||||
state.updateTokenForm {
|
||||
copy(name = name.updateValue(value))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateTokenSymbol(value: String) {
|
||||
state.update { state ->
|
||||
state.updateTokenForm {
|
||||
copy(symbol = symbol.updateValue(value))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateDecimals(value: String) {
|
||||
state.update { state ->
|
||||
state.updateTokenForm {
|
||||
copy(decimals = decimals.updateValue(value))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun selectNetwork() {
|
||||
params.onSelectNetworkClick(CustomTokenFormValues(state.value.tokenForm))
|
||||
}
|
||||
|
||||
private fun selectDerivationPath() {
|
||||
params.onSelectDerivationPathClick(CustomTokenFormValues(state.value.tokenForm))
|
||||
}
|
||||
|
||||
private companion object {
|
||||
const val CONTRACT_ADDRESS_PLACEHOLDER = "0x000000000000000000000000000..."
|
||||
const val DECIMALS_PLACEHOLDER = "0"
|
||||
}
|
||||
}
|
||||
|
|
@ -11,11 +11,11 @@ import androidx.compose.foundation.text.KeyboardActions
|
|||
import androidx.compose.foundation.text.KeyboardOptions
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.focus.onFocusChanged
|
||||
import androidx.compose.ui.platform.LocalDensity
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.input.ImeAction
|
||||
|
|
@ -82,6 +82,7 @@ internal fun CustomTokenFormContent(model: CustomTokenFormUM, modifier: Modifier
|
|||
.fillMaxWidth(),
|
||||
text = stringResource(id = R.string.custom_token_add_token),
|
||||
enabled = model.canAddToken,
|
||||
showProgress = model.isValidating,
|
||||
onClick = model.saveToken,
|
||||
)
|
||||
}
|
||||
|
|
@ -159,14 +160,25 @@ private fun TextField(
|
|||
keyboardOptions: KeyboardOptions = KeyboardOptions.Default,
|
||||
keyboardActions: KeyboardActions = KeyboardActions.Default,
|
||||
) {
|
||||
var isFocused by remember { mutableStateOf(value = false) }
|
||||
|
||||
InformationBlock(
|
||||
modifier = modifier,
|
||||
title = {
|
||||
val color by animateColorAsState(
|
||||
targetValue = if (model.error != null) {
|
||||
TangemTheme.colors.text.warning
|
||||
} else {
|
||||
TangemTheme.colors.text.tertiary
|
||||
targetValue = when {
|
||||
!model.isEnabled -> {
|
||||
TangemTheme.colors.text.disabled
|
||||
}
|
||||
model.error != null -> {
|
||||
TangemTheme.colors.text.warning
|
||||
}
|
||||
model.value.isNotBlank() || isFocused -> {
|
||||
TangemTheme.colors.text.tertiary
|
||||
}
|
||||
else -> {
|
||||
TangemTheme.colors.text.disabled
|
||||
}
|
||||
},
|
||||
label = "Field label color",
|
||||
)
|
||||
|
|
@ -178,14 +190,27 @@ private fun TextField(
|
|||
)
|
||||
},
|
||||
content = {
|
||||
val color by animateColorAsState(
|
||||
targetValue = if (model.isEnabled) {
|
||||
TangemTheme.colors.text.primary1
|
||||
} else {
|
||||
TangemTheme.colors.text.disabled
|
||||
},
|
||||
label = "Field value color",
|
||||
)
|
||||
|
||||
SimpleTextField(
|
||||
modifier = Modifier
|
||||
.padding(bottom = TangemTheme.dimens.spacing12)
|
||||
.fillMaxWidth(),
|
||||
.fillMaxWidth()
|
||||
.onFocusChanged {
|
||||
isFocused = it.isFocused
|
||||
},
|
||||
value = model.value,
|
||||
color = color,
|
||||
onValueChange = model.onValueChange,
|
||||
readOnly = false,
|
||||
placeholder = model.placeholder,
|
||||
readOnly = !model.isEnabled && !isFocused,
|
||||
singleLine = true,
|
||||
keyboardOptions = keyboardOptions,
|
||||
keyboardActions = keyboardActions,
|
||||
|
|
@ -236,7 +261,16 @@ private class PreviewCustomTokenFormComponentProvider :
|
|||
|
||||
override val values: Sequence<PreviewCustomTokenFormComponent>
|
||||
get() = sequenceOf(
|
||||
PreviewCustomTokenFormComponent(),
|
||||
PreviewCustomTokenFormComponent(
|
||||
tokenForm = PreviewCustomTokenFormComponent.tokenForm.copy(
|
||||
contractAddress = TextInputFieldUM(
|
||||
label = stringReference("Contract address"),
|
||||
value = "0x1234567890",
|
||||
placeholder = stringReference("0x1234567890"),
|
||||
onValueChange = {},
|
||||
),
|
||||
),
|
||||
),
|
||||
PreviewCustomTokenFormComponent(
|
||||
tokenForm = PreviewCustomTokenFormComponent.tokenForm.copy(
|
||||
contractAddress = TextInputFieldUM(
|
||||
|
|
|
|||
|
|
@ -44,8 +44,10 @@ import com.tangem.core.ui.components.rows.BlockchainRow
|
|||
import com.tangem.core.ui.components.rows.ChainRow
|
||||
import com.tangem.core.ui.components.rows.model.BlockchainRowUM
|
||||
import com.tangem.core.ui.components.rows.model.ChainRowUM
|
||||
import com.tangem.core.ui.components.snackbar.TangemSnackbarHost
|
||||
import com.tangem.core.ui.extensions.resolveReference
|
||||
import com.tangem.core.ui.extensions.resourceReference
|
||||
import com.tangem.core.ui.res.LocalSnackbarHostState
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.core.ui.res.TangemThemePreview
|
||||
import com.tangem.core.ui.utils.WindowInsetsZero
|
||||
|
|
@ -93,6 +95,12 @@ internal fun ManageTokensScreen(state: ManageTokensUM, modifier: Modifier = Modi
|
|||
state = state,
|
||||
)
|
||||
},
|
||||
snackbarHost = {
|
||||
TangemSnackbarHost(
|
||||
modifier = Modifier.padding(all = TangemTheme.dimens.spacing16),
|
||||
hostState = LocalSnackbarHostState.current,
|
||||
)
|
||||
},
|
||||
floatingActionButtonPosition = FabPosition.Center,
|
||||
floatingActionButton = {
|
||||
if (state is ManageTokensUM.ManageContent) {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,214 @@
|
|||
package com.tangem.features.managetokens.utils
|
||||
|
||||
import arrow.core.getOrElse
|
||||
import com.tangem.core.decompose.di.ComponentScoped
|
||||
import com.tangem.domain.managetokens.CheckIsCurrencyNotAddedUseCase
|
||||
import com.tangem.domain.managetokens.CreateCurrencyUseCase
|
||||
import com.tangem.domain.managetokens.FindTokenUseCase
|
||||
import com.tangem.domain.managetokens.ValidateTokenFormUseCase
|
||||
import com.tangem.domain.managetokens.model.AddCustomTokenForm
|
||||
import com.tangem.domain.managetokens.model.exceptoin.CustomTokenFormValidationException
|
||||
import com.tangem.domain.managetokens.model.exceptoin.FindTokenException
|
||||
import com.tangem.domain.tokens.model.CryptoCurrency
|
||||
import com.tangem.domain.tokens.model.Network
|
||||
import com.tangem.domain.wallets.models.UserWalletId
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.collectLatest
|
||||
import timber.log.Timber
|
||||
import javax.inject.Inject
|
||||
|
||||
@ComponentScoped
|
||||
internal class CustomCurrencyValidator @Inject constructor(
|
||||
private val validateTokenFormUseCase: ValidateTokenFormUseCase,
|
||||
private val createCustomCurrencyUseCase: CreateCurrencyUseCase,
|
||||
private val findTokenUseCase: FindTokenUseCase,
|
||||
private val checkIsCurrencyNotAddedUseCase: CheckIsCurrencyNotAddedUseCase,
|
||||
) {
|
||||
|
||||
private val state: MutableStateFlow<State> = MutableStateFlow(State.NotStarted)
|
||||
|
||||
suspend fun consumeUpdates(block: suspend (State) -> Unit) {
|
||||
state.collectLatest { s ->
|
||||
block(s)
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun validateForm(
|
||||
userWalletId: UserWalletId,
|
||||
networkId: Network.ID,
|
||||
derivationPath: Network.DerivationPath,
|
||||
formValues: AddCustomTokenForm.Raw,
|
||||
) {
|
||||
val result = validateTokenFormUseCase(
|
||||
networkId = networkId,
|
||||
formValues = formValues,
|
||||
)
|
||||
|
||||
val validatedForm = result.getOrElse { e ->
|
||||
state.value = State.FormValidationException(e)
|
||||
return
|
||||
}
|
||||
|
||||
when (validatedForm) {
|
||||
is AddCustomTokenForm.Validated.All -> {
|
||||
findOrCreateCurrency(userWalletId, networkId, derivationPath, validatedForm)
|
||||
}
|
||||
is AddCustomTokenForm.Validated.ContractAddress -> {
|
||||
findToken(userWalletId, networkId, derivationPath, validatedForm)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun createCoin(userWalletId: UserWalletId, networkId: Network.ID, derivationPath: Network.DerivationPath) {
|
||||
createCurrency(userWalletId, networkId, derivationPath, validatedForm = null)
|
||||
}
|
||||
|
||||
private suspend fun findOrCreateCurrency(
|
||||
userWalletId: UserWalletId,
|
||||
networkId: Network.ID,
|
||||
derivationPath: Network.DerivationPath,
|
||||
validatedForm: AddCustomTokenForm.Validated.All,
|
||||
) {
|
||||
state.value = State.SearchingToken
|
||||
|
||||
val foundToken = findTokenUseCase(
|
||||
userWalletId = userWalletId,
|
||||
contractAddress = validatedForm.contractAddress,
|
||||
networkId = networkId,
|
||||
derivationPath = derivationPath,
|
||||
).getOrElse { e ->
|
||||
when (e) {
|
||||
is FindTokenException.DataError -> {
|
||||
Timber.e(e.cause, "Unable to find custom currency")
|
||||
state.value = State.UnexpectedException(e.cause)
|
||||
return
|
||||
}
|
||||
is FindTokenException.NotFound -> {
|
||||
null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (foundToken != null) {
|
||||
updateStateToValidated(userWalletId, foundToken, fillForm = true, isCustom = false)
|
||||
} else {
|
||||
createCurrency(userWalletId, networkId, derivationPath, validatedForm)
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun findToken(
|
||||
userWalletId: UserWalletId,
|
||||
networkId: Network.ID,
|
||||
derivationPath: Network.DerivationPath,
|
||||
validatedForm: AddCustomTokenForm.Validated.ContractAddress,
|
||||
) {
|
||||
state.value = State.SearchingToken
|
||||
|
||||
val token = findTokenUseCase(
|
||||
userWalletId = userWalletId,
|
||||
contractAddress = validatedForm.contractAddress,
|
||||
networkId = networkId,
|
||||
derivationPath = derivationPath,
|
||||
).getOrElse { e ->
|
||||
state.value = when (e) {
|
||||
is FindTokenException.DataError -> {
|
||||
Timber.e(e.cause, "Unable to find custom currency")
|
||||
State.UnexpectedException(e.cause)
|
||||
}
|
||||
is FindTokenException.NotFound -> {
|
||||
State.TokenNotFound
|
||||
}
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
updateStateToValidated(userWalletId, token, fillForm = true, isCustom = false)
|
||||
}
|
||||
|
||||
private suspend fun createCurrency(
|
||||
userWalletId: UserWalletId,
|
||||
networkId: Network.ID,
|
||||
derivationPath: Network.DerivationPath,
|
||||
validatedForm: AddCustomTokenForm.Validated.All?,
|
||||
) {
|
||||
val currency = createCustomCurrencyUseCase(
|
||||
networkId = networkId,
|
||||
derivationPath = derivationPath,
|
||||
formValues = validatedForm,
|
||||
).getOrElse { e ->
|
||||
Timber.e(e, "Unable to create custom currency")
|
||||
state.value = State.UnexpectedException(e)
|
||||
return
|
||||
}
|
||||
|
||||
updateStateToValidated(userWalletId, currency, fillForm = false, isCustom = validatedForm != null)
|
||||
}
|
||||
|
||||
private suspend fun updateStateToValidated(
|
||||
userWalletId: UserWalletId,
|
||||
currency: CryptoCurrency,
|
||||
fillForm: Boolean,
|
||||
isCustom: Boolean,
|
||||
) {
|
||||
val isNotAdded = checkIsCurrencyNotAddedUseCase(
|
||||
userWalletId = userWalletId,
|
||||
networkId = currency.network.id,
|
||||
derivationPath = currency.network.derivationPath,
|
||||
contractAddress = when (currency) {
|
||||
is CryptoCurrency.Coin -> null
|
||||
is CryptoCurrency.Token -> currency.contractAddress
|
||||
},
|
||||
).getOrElse { e ->
|
||||
Timber.e(e, "Unable to check if currency is already added")
|
||||
state.value = State.UnexpectedException(e)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
state.value = State.Validated(
|
||||
currency = currency,
|
||||
fillForm = fillForm,
|
||||
isAlreadyAdded = !isNotAdded,
|
||||
isCustom = isCustom,
|
||||
)
|
||||
}
|
||||
|
||||
sealed class State {
|
||||
|
||||
abstract val isFinished: Boolean
|
||||
|
||||
data object NotStarted : State() {
|
||||
override val isFinished: Boolean = false
|
||||
}
|
||||
|
||||
data object SearchingToken : State() {
|
||||
override val isFinished: Boolean = false
|
||||
}
|
||||
|
||||
data class Validated(
|
||||
val currency: CryptoCurrency,
|
||||
val fillForm: Boolean,
|
||||
val isAlreadyAdded: Boolean,
|
||||
val isCustom: Boolean,
|
||||
) : State() {
|
||||
override val isFinished: Boolean = true
|
||||
}
|
||||
|
||||
data class FormValidationException(
|
||||
val exceptions: List<CustomTokenFormValidationException>,
|
||||
) : State() {
|
||||
override val isFinished: Boolean = true
|
||||
}
|
||||
|
||||
data object TokenNotFound : State() {
|
||||
override val isFinished: Boolean = true
|
||||
}
|
||||
|
||||
data class UnexpectedException(
|
||||
val cause: Throwable,
|
||||
) : State() {
|
||||
override val isFinished: Boolean = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
package com.tangem.features.managetokens.utils.mapper
|
||||
|
||||
import com.tangem.domain.managetokens.model.AddCustomTokenForm
|
||||
import com.tangem.features.managetokens.entity.customtoken.CustomTokenFormUM
|
||||
|
||||
internal fun CustomTokenFormUM.TokenFormUM.mapToDomainModel(): AddCustomTokenForm.Raw {
|
||||
return AddCustomTokenForm.Raw(
|
||||
contractAddress = contractAddress.value,
|
||||
symbol = symbol.value,
|
||||
name = name.value,
|
||||
decimals = decimals.value,
|
||||
)
|
||||
}
|
||||
|
|
@ -0,0 +1,155 @@
|
|||
package com.tangem.features.managetokens.utils.ui
|
||||
|
||||
import com.tangem.core.ui.components.notifications.NotificationConfig
|
||||
import com.tangem.core.ui.extensions.TextReference
|
||||
import com.tangem.core.ui.extensions.resourceReference
|
||||
import com.tangem.core.ui.extensions.wrappedList
|
||||
import com.tangem.domain.managetokens.ValidateTokenFormUseCase
|
||||
import com.tangem.domain.managetokens.model.exceptoin.CustomTokenFormValidationException
|
||||
import com.tangem.domain.tokens.model.CryptoCurrency
|
||||
import com.tangem.features.managetokens.entity.customtoken.CustomTokenFormUM
|
||||
import com.tangem.features.managetokens.entity.customtoken.TextInputFieldUM
|
||||
import com.tangem.features.managetokens.impl.R
|
||||
import kotlinx.collections.immutable.mutate
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
|
||||
internal fun CustomTokenFormUM.updateTokenForm(
|
||||
block: CustomTokenFormUM.TokenFormUM.() -> CustomTokenFormUM.TokenFormUM,
|
||||
): CustomTokenFormUM {
|
||||
val form = tokenForm ?: return this
|
||||
|
||||
val updatedForm = form.block()
|
||||
|
||||
return copy(tokenForm = updatedForm)
|
||||
}
|
||||
|
||||
internal fun TextInputFieldUM.updateValue(
|
||||
value: String = this.value,
|
||||
error: TextReference? = this.error,
|
||||
isEnabled: Boolean = this.isEnabled,
|
||||
clearError: Boolean = false,
|
||||
): TextInputFieldUM {
|
||||
return copy(
|
||||
value = value,
|
||||
error = if (clearError) null else error,
|
||||
isEnabled = isEnabled,
|
||||
)
|
||||
}
|
||||
|
||||
internal fun CustomTokenFormUM.updateWithProgress(
|
||||
showProgress: Boolean,
|
||||
canAddToken: Boolean = this.canAddToken,
|
||||
clearNotifications: Boolean = false,
|
||||
clearFieldErrors: Boolean = false,
|
||||
disableSecondaryFields: Boolean = false,
|
||||
): CustomTokenFormUM {
|
||||
return copy(
|
||||
isValidating = showProgress,
|
||||
canAddToken = canAddToken,
|
||||
notifications = if (clearNotifications) persistentListOf() else notifications,
|
||||
).updateTokenForm {
|
||||
copy(
|
||||
contractAddress = contractAddress.updateValue(
|
||||
clearError = clearFieldErrors,
|
||||
),
|
||||
name = name.updateValue(
|
||||
isEnabled = !showProgress && !disableSecondaryFields,
|
||||
clearError = clearFieldErrors,
|
||||
),
|
||||
symbol = symbol.updateValue(
|
||||
isEnabled = !showProgress && !disableSecondaryFields,
|
||||
clearError = clearFieldErrors,
|
||||
),
|
||||
decimals = decimals.updateValue(
|
||||
isEnabled = !showProgress && !disableSecondaryFields,
|
||||
clearError = clearFieldErrors,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
internal fun CustomTokenFormUM.updateWithCurrency(currency: CryptoCurrency): CustomTokenFormUM {
|
||||
return updateTokenForm {
|
||||
copy(
|
||||
contractAddress = contractAddress.updateValue(error = null),
|
||||
name = name.updateValue(currency.name),
|
||||
symbol = symbol.updateValue(currency.symbol),
|
||||
decimals = decimals.updateValue(currency.decimals.toString()),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
internal fun CustomTokenFormUM.updateWithContractAddressException(
|
||||
exception: CustomTokenFormValidationException.ContractAddress,
|
||||
): CustomTokenFormUM {
|
||||
return updateTokenForm {
|
||||
copy(
|
||||
contractAddress = contractAddress.updateValue(
|
||||
error = when (exception) {
|
||||
CustomTokenFormValidationException.ContractAddress.Empty -> {
|
||||
null
|
||||
}
|
||||
CustomTokenFormValidationException.ContractAddress.Invalid -> {
|
||||
resourceReference(R.string.custom_token_creation_error_invalid_contract_address)
|
||||
}
|
||||
},
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
internal fun CustomTokenFormUM.updateWithDecimalsException(
|
||||
exception: CustomTokenFormValidationException.Decimals,
|
||||
): CustomTokenFormUM {
|
||||
return updateTokenForm {
|
||||
copy(
|
||||
decimals = decimals.updateValue(
|
||||
error = when (exception) {
|
||||
is CustomTokenFormValidationException.Decimals.Empty -> {
|
||||
null
|
||||
}
|
||||
is CustomTokenFormValidationException.Decimals.Invalid -> {
|
||||
resourceReference(
|
||||
R.string.custom_token_creation_error_wrong_decimals,
|
||||
wrappedList(ValidateTokenFormUseCase.MAX_DECIMALS),
|
||||
)
|
||||
}
|
||||
},
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
internal fun CustomTokenFormUM.updateWithCurrencyNotFoundNotification(): CustomTokenFormUM {
|
||||
val notification = CustomTokenFormUM.NotificationUM(
|
||||
id = "currency_not_found",
|
||||
config = NotificationConfig(
|
||||
title = resourceReference(R.string.custom_token_validation_error_not_found_title),
|
||||
subtitle = resourceReference(R.string.custom_token_validation_error_not_found_description),
|
||||
iconResId = R.drawable.img_attention_20,
|
||||
),
|
||||
)
|
||||
|
||||
return copy(
|
||||
notifications = notifications.mutate {
|
||||
it.add(notification)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
internal fun CustomTokenFormUM.updateWithCurrencyAlreadyAddedNotification(): CustomTokenFormUM {
|
||||
val notification = CustomTokenFormUM.NotificationUM(
|
||||
id = "currency_already_added",
|
||||
config = NotificationConfig(
|
||||
title = resourceReference(R.string.custom_token_creation_error_token_already_exist_title),
|
||||
subtitle = resourceReference(R.string.custom_token_creation_error_token_already_exist_message),
|
||||
iconResId = R.drawable.img_attention_20,
|
||||
),
|
||||
)
|
||||
|
||||
return copy(
|
||||
notifications = notifications.mutate {
|
||||
it.add(notification)
|
||||
},
|
||||
)
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue