Updated on 2026-08-14

This commit is contained in:
Tangem 2024-08-30 13:18:56 +04:00
commit 64a40de825
10 changed files with 769 additions and 139 deletions

View file

@ -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"
}
}

View file

@ -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(

View file

@ -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,
)

View file

@ -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) { "" },
)
}
}
}

View file

@ -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"
}
}

View file

@ -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(

View file

@ -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) {

View file

@ -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
}
}
}

View file

@ -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,
)
}

View file

@ -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)
},
)
}