diff --git a/core/utils/src/main/java/com/tangem/utils/coroutines/JobHolder.kt b/core/utils/src/main/java/com/tangem/utils/coroutines/JobHolder.kt index 14e64f1b62..d50f9f295c 100644 --- a/core/utils/src/main/java/com/tangem/utils/coroutines/JobHolder.kt +++ b/core/utils/src/main/java/com/tangem/utils/coroutines/JobHolder.kt @@ -24,4 +24,6 @@ class JobHolder { } } -fun Job.saveIn(jobHolder: JobHolder): Job = jobHolder.update(job = this) \ No newline at end of file +fun Job.saveIn(jobHolder: JobHolder): Job = jobHolder.update(job = this) + +suspend fun Job.saveInAndJoin(jobHolder: JobHolder) = saveIn(jobHolder).join() \ No newline at end of file diff --git a/domain/manage-tokens/src/main/kotlin/com/tangem/domain/managetokens/model/AddCustomTokenForm.kt b/domain/manage-tokens/src/main/kotlin/com/tangem/domain/managetokens/model/AddCustomTokenForm.kt index b70cd54ab9..a5b5dc5bdc 100644 --- a/domain/manage-tokens/src/main/kotlin/com/tangem/domain/managetokens/model/AddCustomTokenForm.kt +++ b/domain/manage-tokens/src/main/kotlin/com/tangem/domain/managetokens/model/AddCustomTokenForm.kt @@ -9,7 +9,7 @@ sealed class AddCustomTokenForm { val decimals: String, ) : AddCustomTokenForm() - sealed class Validated { + sealed class Validated : AddCustomTokenForm() { data class ContractAddress( val contractAddress: String, diff --git a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/entity/customtoken/CustomTokenFormUM.kt b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/entity/customtoken/CustomTokenFormUM.kt index 1fbd8e4de3..fe72248f0d 100644 --- a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/entity/customtoken/CustomTokenFormUM.kt +++ b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/entity/customtoken/CustomTokenFormUM.kt @@ -20,6 +20,7 @@ internal data class CustomTokenFormUM( val name: TextInputFieldUM, val symbol: TextInputFieldUM, val decimals: TextInputFieldUM, + val wasFilled: Boolean = false, ) data class NotificationUM( diff --git a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/model/CustomTokenFormModel.kt b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/model/CustomTokenFormModel.kt index 2fa77b8e92..4e678f1bb4 100644 --- a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/model/CustomTokenFormModel.kt +++ b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/model/CustomTokenFormModel.kt @@ -92,15 +92,15 @@ internal class CustomTokenFormModel @Inject constructor( @OptIn(FlowPreview::class) private fun observeTokenFormUpdates() { state - .sample(periodMillis = 1_000) - .transform { - val values = it.tokenForm?.mapToDomainModel() + .transform { state -> + val form = state.tokenForm - if (values != null) { - emit(values) + if (form != null && !form.wasFilled) { + emit(form.mapToDomainModel()) } } .distinctUntilChanged() + .sample(periodMillis = 1_000) .onEach { formValues -> customCurrencyValidator.validateForm( userWalletId = params.userWalletId, @@ -117,14 +117,16 @@ internal class CustomTokenFormModel @Inject constructor( createdCurrency = null when (validatorState) { - is CustomCurrencyValidator.State.NotStarted -> Unit - is CustomCurrencyValidator.State.SearchingToken -> updateStateWithSearching() - is CustomCurrencyValidator.State.UnexpectedException -> showErrorDialog() - is CustomCurrencyValidator.State.FormValidationException -> updateStateWithExceptions( + is CustomCurrencyValidator.Status.NotStarted, + is CustomCurrencyValidator.Status.Validating, + -> Unit + is CustomCurrencyValidator.Status.SearchingToken -> updateStateWithProgress() + is CustomCurrencyValidator.Status.UnexpectedException -> showErrorDialog() + is CustomCurrencyValidator.Status.FormValidationException -> updateStateWithExceptions( exceptions = validatorState.exceptions, ) - is CustomCurrencyValidator.State.TokenNotFound -> updateStateWithNotFoundNotification() - is CustomCurrencyValidator.State.Validated -> { + is CustomCurrencyValidator.Status.TokenNotFound -> updateStateWithNotFoundNotification() + is CustomCurrencyValidator.Status.Validated -> { createdCurrency = validatorState.currency updateStateWithCurrency( @@ -149,6 +151,7 @@ internal class CustomTokenFormModel @Inject constructor( .updateWithProgress( showProgress = false, canAddToken = !isAlreadyAdded, + isWasFilled = fillForm, clearNotifications = true, clearFieldErrors = true, disableSecondaryFields = !isCustom, @@ -183,7 +186,7 @@ internal class CustomTokenFormModel @Inject constructor( } } - private fun updateStateWithSearching() { + private fun updateStateWithProgress() { state.update { state -> state.updateWithProgress( showProgress = true, @@ -274,7 +277,10 @@ internal class CustomTokenFormModel @Inject constructor( private fun updateContractAddress(value: String) { state.update { state -> state.updateTokenForm { - copy(contractAddress = contractAddress.updateValue(value)) + copy( + contractAddress = contractAddress.updateValue(value), + wasFilled = false, + ) } } } @@ -282,7 +288,10 @@ internal class CustomTokenFormModel @Inject constructor( private fun updateTokenName(value: String) { state.update { state -> state.updateTokenForm { - copy(name = name.updateValue(value)) + copy( + name = name.updateValue(value), + wasFilled = false, + ) } } } @@ -290,7 +299,10 @@ internal class CustomTokenFormModel @Inject constructor( private fun updateTokenSymbol(value: String) { state.update { state -> state.updateTokenForm { - copy(symbol = symbol.updateValue(value)) + copy( + symbol = symbol.updateValue(value), + wasFilled = false, + ) } } } @@ -298,7 +310,10 @@ internal class CustomTokenFormModel @Inject constructor( private fun updateDecimals(value: String) { state.update { state -> state.updateTokenForm { - copy(decimals = decimals.updateValue(value)) + copy( + decimals = decimals.updateValue(value), + wasFilled = false, + ) } } } diff --git a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/utils/CustomCurrencyValidator.kt b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/utils/CustomCurrencyValidator.kt index 373b4640c7..c4876c9914 100644 --- a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/utils/CustomCurrencyValidator.kt +++ b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/utils/CustomCurrencyValidator.kt @@ -12,8 +12,11 @@ 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 com.tangem.utils.coroutines.JobHolder +import com.tangem.utils.coroutines.saveInAndJoin +import kotlinx.coroutines.coroutineScope +import kotlinx.coroutines.flow.* +import kotlinx.coroutines.launch import timber.log.Timber import javax.inject.Inject @@ -25,12 +28,20 @@ internal class CustomCurrencyValidator @Inject constructor( private val checkIsCurrencyNotAddedUseCase: CheckIsCurrencyNotAddedUseCase, ) { - private val state: MutableStateFlow = MutableStateFlow(State.NotStarted) + private val validateFormJobHolder = JobHolder() + private val state: MutableStateFlow = MutableStateFlow( + value = State( + prevValidatedForm = null, + prevFoundOrCreatedCurrency = null, + status = Status.NotStarted, + ), + ) - suspend fun consumeUpdates(block: suspend (State) -> Unit) { - state.collectLatest { s -> - block(s) - } + suspend fun consumeUpdates(block: suspend (Status) -> Unit) { + state + .map { it.status } + .distinctUntilChanged() + .collectLatest { block(it) } } suspend fun validateForm( @@ -38,25 +49,37 @@ internal class CustomCurrencyValidator @Inject constructor( networkId: Network.ID, derivationPath: Network.DerivationPath, formValues: AddCustomTokenForm.Raw, - ) { + ) = coroutineScope { + updateStatus(Status.Validating) + val result = validateTokenFormUseCase( networkId = networkId, formValues = formValues, ) val validatedForm = result.getOrElse { e -> - state.value = State.FormValidationException(e) - return + updateStatus(Status.FormValidationException(e)) + return@coroutineScope } - when (validatedForm) { - is AddCustomTokenForm.Validated.All -> { - findOrCreateCurrency(userWalletId, networkId, derivationPath, validatedForm) - } - is AddCustomTokenForm.Validated.ContractAddress -> { - findToken(userWalletId, networkId, derivationPath, validatedForm) + if (state.value.prevValidatedForm == validatedForm) { + return@coroutineScope + } else { + state.update { state -> + state.copy(prevValidatedForm = validatedForm) } } + + launch { + when (validatedForm) { + is AddCustomTokenForm.Validated.All -> { + findOrCreateCurrency(userWalletId, networkId, derivationPath, validatedForm) + } + is AddCustomTokenForm.Validated.ContractAddress -> { + findToken(userWalletId, networkId, derivationPath, validatedForm) + } + } + }.saveInAndJoin(validateFormJobHolder) } suspend fun createCoin(userWalletId: UserWalletId, networkId: Network.ID, derivationPath: Network.DerivationPath) { @@ -69,7 +92,16 @@ internal class CustomCurrencyValidator @Inject constructor( derivationPath: Network.DerivationPath, validatedForm: AddCustomTokenForm.Validated.All, ) { - state.value = State.SearchingToken + val currentState = state.value + if (currentState.prevFoundOrCreatedCurrency is CryptoCurrency.Token && + currentState.prevFoundOrCreatedCurrency.contractAddress == validatedForm.contractAddress + ) { + // No need to search for token again if contract address is not changed + createCurrency(userWalletId, networkId, derivationPath, validatedForm) + return + } + + updateStatus(Status.SearchingToken) val foundToken = findTokenUseCase( userWalletId = userWalletId, @@ -80,7 +112,7 @@ internal class CustomCurrencyValidator @Inject constructor( when (e) { is FindTokenException.DataError -> { Timber.e(e.cause, "Unable to find custom currency") - state.value = State.UnexpectedException(e.cause) + updateStatus(Status.UnexpectedException(e.cause)) return } is FindTokenException.NotFound -> { @@ -102,7 +134,7 @@ internal class CustomCurrencyValidator @Inject constructor( derivationPath: Network.DerivationPath, validatedForm: AddCustomTokenForm.Validated.ContractAddress, ) { - state.value = State.SearchingToken + updateStatus(Status.SearchingToken) val token = findTokenUseCase( userWalletId = userWalletId, @@ -110,15 +142,16 @@ internal class CustomCurrencyValidator @Inject constructor( networkId = networkId, derivationPath = derivationPath, ).getOrElse { e -> - state.value = when (e) { + val newStatus = when (e) { is FindTokenException.DataError -> { Timber.e(e.cause, "Unable to find custom currency") - State.UnexpectedException(e.cause) + Status.UnexpectedException(e.cause) } is FindTokenException.NotFound -> { - State.TokenNotFound + Status.TokenNotFound } } + updateStatus(newStatus) return } @@ -138,7 +171,7 @@ internal class CustomCurrencyValidator @Inject constructor( formValues = validatedForm, ).getOrElse { e -> Timber.e(e, "Unable to create custom currency") - state.value = State.UnexpectedException(e) + updateStatus(Status.UnexpectedException(e)) return } @@ -151,6 +184,9 @@ internal class CustomCurrencyValidator @Inject constructor( fillForm: Boolean, isCustom: Boolean, ) { + val currentStatus = state.value.status + if (currentStatus is Status.Validated && currentStatus.currency == currency) return + val isNotAdded = checkIsCurrencyNotAddedUseCase( userWalletId = userWalletId, networkId = currency.network.id, @@ -161,54 +197,59 @@ internal class CustomCurrencyValidator @Inject constructor( }, ).getOrElse { e -> Timber.e(e, "Unable to check if currency is already added") - state.value = State.UnexpectedException(e) + updateStatus(Status.UnexpectedException(e)) return } - state.value = State.Validated( - currency = currency, - fillForm = fillForm, - isAlreadyAdded = !isNotAdded, - isCustom = isCustom, - ) + state.update { state -> + state.copy( + status = Status.Validated( + currency = currency, + fillForm = fillForm, + isAlreadyAdded = !isNotAdded, + isCustom = isCustom, + ), + prevFoundOrCreatedCurrency = currency, + ) + } } - sealed class State { - - abstract val isFinished: Boolean - - data object NotStarted : State() { - override val isFinished: Boolean = false + private fun updateStatus(status: Status) { + state.update { state -> + state.copy(status = status) } + } - data object SearchingToken : State() { - override val isFinished: Boolean = false - } + data class State( + val prevValidatedForm: AddCustomTokenForm.Validated?, + val prevFoundOrCreatedCurrency: CryptoCurrency?, + val status: Status, + ) + + sealed class Status { + + data object NotStarted : Status() + + data object SearchingToken : Status() + + data object Validating : Status() data class Validated( val currency: CryptoCurrency, val fillForm: Boolean, val isAlreadyAdded: Boolean, val isCustom: Boolean, - ) : State() { - override val isFinished: Boolean = true - } + ) : Status() data class FormValidationException( val exceptions: List, - ) : State() { - override val isFinished: Boolean = true - } + ) : Status() - data object TokenNotFound : State() { - override val isFinished: Boolean = true - } + data object TokenNotFound : Status() data class UnexpectedException( val cause: Throwable, - ) : State() { - override val isFinished: Boolean = true - } + ) : Status() } } \ No newline at end of file diff --git a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/utils/ui/CustomCurrencyFormOperations.kt b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/utils/ui/CustomCurrencyFormOperations.kt index 71ecd3cd8d..ae05953156 100644 --- a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/utils/ui/CustomCurrencyFormOperations.kt +++ b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/utils/ui/CustomCurrencyFormOperations.kt @@ -38,6 +38,7 @@ internal fun TextInputFieldUM.updateValue( internal fun CustomTokenFormUM.updateWithProgress( showProgress: Boolean, + isWasFilled: Boolean = this.tokenForm?.wasFilled ?: false, canAddToken: Boolean = this.canAddToken, clearNotifications: Boolean = false, clearFieldErrors: Boolean = false, @@ -64,6 +65,7 @@ internal fun CustomTokenFormUM.updateWithProgress( isEnabled = !showProgress && !disableSecondaryFields, clearError = clearFieldErrors, ), + wasFilled = isWasFilled, ) } }