From b6a69d7e697a6e49571c4bf2b613b3bcfa6d1e7a Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 8 Jul 2024 12:09:47 +0100 Subject: [PATCH 1/8] Updated on 2026-08-14 --- .../presentation/wallet/domain/UserWalletExt.kt | 5 +++-- .../wallet/domain/WalletAdditionalInfoFactory.kt | 15 --------------- 2 files changed, 3 insertions(+), 17 deletions(-) diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/UserWalletExt.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/UserWalletExt.kt index ab7e236835..a78ded4bf0 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/UserWalletExt.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/UserWalletExt.kt @@ -7,8 +7,9 @@ fun UserWallet.getCardsCount(): Int? { return if (isMultiCurrency) { when (val status = scanResponse.card.backupStatus) { is CardDTO.BackupStatus.Active -> status.cardCount + 1 - is CardDTO.BackupStatus.CardLinked -> status.cardCount + 1 - is CardDTO.BackupStatus.NoBackup -> 1 + is CardDTO.BackupStatus.NoBackup, + is CardDTO.BackupStatus.CardLinked, + -> 1 null -> 1 // Multi-currency wallet without backup function. Example, 4.12 } } else { diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/WalletAdditionalInfoFactory.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/WalletAdditionalInfoFactory.kt index 3c61413f63..1f34e460f2 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/WalletAdditionalInfoFactory.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/WalletAdditionalInfoFactory.kt @@ -6,7 +6,6 @@ import com.tangem.core.ui.extensions.stringReference import com.tangem.core.ui.extensions.wrappedList import com.tangem.core.ui.utils.BigDecimalFormatter import com.tangem.domain.common.util.cardTypesResolver -import com.tangem.domain.models.scan.CardDTO import com.tangem.domain.wallets.models.UserWallet import com.tangem.feature.wallet.impl.R import com.tangem.feature.wallet.presentation.wallet.state.model.WalletAdditionalInfo @@ -108,18 +107,4 @@ internal object WalletAdditionalInfoFactory { WalletAdditionalInfo(hideable = true, content = TextReference.Str(value = amount.orEmpty())) } } - - private fun UserWallet.getCardsCount(): Int? { - return if (isMultiCurrency) { - when (val status = scanResponse.card.backupStatus) { - is CardDTO.BackupStatus.Active -> status.cardCount + 1 - is CardDTO.BackupStatus.CardLinked, - is CardDTO.BackupStatus.NoBackup, - null, - -> 1 - } - } else { - null - } - } } \ No newline at end of file From 90486a54fce52ca1e3f18245f5a0c7c3e6635ed5 Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 8 Jul 2024 13:59:55 +0400 Subject: [PATCH 2/8] Updated on 2026-08-14 --- .../impl/domain/CustomTokenInteractor.kt | 2 +- .../domain/DefaultCustomTokenInteractor.kt | 23 +++++++++-------- .../viewmodels/AddCustomTokenViewModel.kt | 25 +++++++++++++------ .../tokens/AddCryptoCurrenciesUseCase.kt | 21 ++++++++-------- .../domain/tokens/error/AddCurrencyError.kt | 6 ----- 5 files changed, 41 insertions(+), 36 deletions(-) delete mode 100644 domain/tokens/src/main/kotlin/com/tangem/domain/tokens/error/AddCurrencyError.kt diff --git a/app/src/main/java/com/tangem/tap/features/customtoken/impl/domain/CustomTokenInteractor.kt b/app/src/main/java/com/tangem/tap/features/customtoken/impl/domain/CustomTokenInteractor.kt index 16a0cb98a5..22bea5bf11 100644 --- a/app/src/main/java/com/tangem/tap/features/customtoken/impl/domain/CustomTokenInteractor.kt +++ b/app/src/main/java/com/tangem/tap/features/customtoken/impl/domain/CustomTokenInteractor.kt @@ -17,5 +17,5 @@ interface CustomTokenInteractor { /** Save token [customCurrency] */ @Throws(TangemError::class) - suspend fun saveToken(customCurrency: CustomCurrency) + suspend fun saveToken(customCurrency: CustomCurrency): Result } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/customtoken/impl/domain/DefaultCustomTokenInteractor.kt b/app/src/main/java/com/tangem/tap/features/customtoken/impl/domain/DefaultCustomTokenInteractor.kt index de73abac25..ad210d7625 100644 --- a/app/src/main/java/com/tangem/tap/features/customtoken/impl/domain/DefaultCustomTokenInteractor.kt +++ b/app/src/main/java/com/tangem/tap/features/customtoken/impl/domain/DefaultCustomTokenInteractor.kt @@ -1,5 +1,7 @@ package com.tangem.tap.features.customtoken.impl.domain +import arrow.core.getOrElse +import arrow.core.raise.result import com.tangem.blockchain.common.Blockchain import com.tangem.blockchainsdk.utils.toNetworkId import com.tangem.data.tokens.utils.CryptoCurrencyFactory @@ -15,7 +17,6 @@ import com.tangem.tap.domain.model.Currency import com.tangem.tap.features.customtoken.impl.domain.models.FoundToken import com.tangem.tap.proxy.redux.DaggerGraphState import com.tangem.tap.store -import timber.log.Timber /** * Default implementation of custom token interactor @@ -45,16 +46,18 @@ class DefaultCustomTokenInteractor( ) } - override suspend fun saveToken(customCurrency: CustomCurrency) { - val userWallet = getSelectedWalletSyncUseCase().fold(ifLeft = { return }, ifRight = { it }) - val currency = Currency.fromCustomCurrency(customCurrency) - - val currencies = listOfNotNull(element = currency.toCryptoCurrency(userWallet.scanResponse)) - derivePublicKeysUseCase(userWalletId = userWallet.walletId, currencies = currencies) - .onRight { - addCryptoCurrenciesUseCase(userWalletId = userWallet.walletId, currencies = currencies) + override suspend fun saveToken(customCurrency: CustomCurrency): Result { + return result { + val userWallet = getSelectedWalletSyncUseCase().getOrElse { + error("Failed to get selected wallet: $it") } - .onLeft { Timber.e("Failed to derive public keys: $it") } + + val currency = Currency.fromCustomCurrency(customCurrency) + val currencies = listOfNotNull(element = currency.toCryptoCurrency(userWallet.scanResponse)) + + derivePublicKeysUseCase(userWalletId = userWallet.walletId, currencies = currencies).bind() + addCryptoCurrenciesUseCase(userWalletId = userWallet.walletId, currencies = currencies).bind() + } } private fun Currency.toCryptoCurrency(scanResponse: ScanResponse): CryptoCurrency? { diff --git a/app/src/main/java/com/tangem/tap/features/customtoken/impl/presentation/viewmodels/AddCustomTokenViewModel.kt b/app/src/main/java/com/tangem/tap/features/customtoken/impl/presentation/viewmodels/AddCustomTokenViewModel.kt index d725069217..5f523da11c 100644 --- a/app/src/main/java/com/tangem/tap/features/customtoken/impl/presentation/viewmodels/AddCustomTokenViewModel.kt +++ b/app/src/main/java/com/tangem/tap/features/customtoken/impl/presentation/viewmodels/AddCustomTokenViewModel.kt @@ -886,17 +886,26 @@ internal class AddCustomTokenViewModel @Inject constructor( analyticsSender.sendWhenAddTokenButtonClicked(currency) - viewModelScope.launch(dispatchers.io) { - val oldButtonState = uiState.floatingButton + viewModelScope.launch { uiState = uiState.copySealed( - floatingButton = uiState.floatingButton.copy(isEnabled = false, showProgress = true), + floatingButton = uiState.floatingButton.copy( + isEnabled = false, + showProgress = true, + ), ) - runCatching { featureInteractor.saveToken(currency) } + + val result = featureInteractor.saveToken(currency) + + uiState = uiState.copySealed( + floatingButton = uiState.floatingButton.copy( + isEnabled = true, + showProgress = false, + ), + ) + + result .onSuccess { featureRouter.openWalletScreen() } - .onFailure { - uiState = uiState.copySealed(floatingButton = oldButtonState) - Timber.e(it) - } + .onFailure { Timber.e(it, "Unable to save custom token") } } } } diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/AddCryptoCurrenciesUseCase.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/AddCryptoCurrenciesUseCase.kt index 9febf735fe..e8c6f0b7a8 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/AddCryptoCurrenciesUseCase.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/AddCryptoCurrenciesUseCase.kt @@ -5,7 +5,6 @@ import arrow.core.raise.Raise import arrow.core.raise.catch import arrow.core.raise.either import arrow.core.toNonEmptyListOrNull -import com.tangem.domain.tokens.error.AddCurrencyError import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.domain.tokens.model.Network import com.tangem.domain.tokens.repository.CurrenciesRepository @@ -32,9 +31,9 @@ class AddCryptoCurrenciesUseCase( * * @param userWalletId The ID of the user's wallet. * @param currency Cryptocurrency to add. - * @return Either an [AddCurrencyError] or [Unit] indicating the success of the operation. + * @return Either an [Throwable] or [Unit] indicating the success of the operation. */ - suspend operator fun invoke(userWalletId: UserWalletId, currency: CryptoCurrency): Either { + suspend operator fun invoke(userWalletId: UserWalletId, currency: CryptoCurrency): Either { return invoke(userWalletId, listOf(currency)) } @@ -47,13 +46,13 @@ class AddCryptoCurrenciesUseCase( * @param userWalletId The ID of the user's wallet. * @param cryptoCurrency Token to add. * @param network Network where we add - * @return Either an [AddCurrencyError] or [Unit] indicating the success of the operation. + * @return Either an [Throwable] or [Unit] indicating the success of the operation. */ suspend operator fun invoke( userWalletId: UserWalletId, cryptoCurrency: CryptoCurrency.Token, network: Network, - ): Either = either { + ): Either = either { val tokenToAdd = currenciesRepository.createTokenCurrency(cryptoCurrency = cryptoCurrency, network = network) invoke(userWalletId = userWalletId, currencies = listOf(tokenToAdd)) } @@ -66,14 +65,14 @@ class AddCryptoCurrenciesUseCase( * * @param userWalletId The ID of the user's wallet. * @param currencies The list of cryptocurrencies to add. - * @return Either an [AddCurrencyError] or [Unit] indicating the success of the operation. + * @return Either an [Throwable] or [Unit] indicating the success of the operation. */ suspend operator fun invoke( userWalletId: UserWalletId, currencies: List, - ): Either = either { + ): Either = either { val existingCurrencies = catch({ currenciesRepository.getMultiCurrencyWalletCurrenciesSync(userWalletId) }) { - raise(AddCurrencyError.DataError(it)) + raise(it) } val currenciesToAdd = currencies .filterNot(existingCurrencies::contains) @@ -81,7 +80,7 @@ class AddCryptoCurrenciesUseCase( ?: return@either catch({ currenciesRepository.addCurrencies(userWalletId, currenciesToAdd) }) { - raise(AddCurrencyError.DataError(it)) + raise(it) } refreshUpdatedNetworks(userWalletId, currenciesToAdd, existingCurrencies) @@ -91,7 +90,7 @@ class AddCryptoCurrenciesUseCase( * Refreshes the network statuses for tokens that have corresponding coins in the * [existingCurrencies] list. */ - private suspend fun Raise.refreshUpdatedNetworks( + private suspend fun Raise.refreshUpdatedNetworks( userWalletId: UserWalletId, currenciesToAdd: List, existingCurrencies: List, @@ -114,7 +113,7 @@ class AddCryptoCurrenciesUseCase( ) }, ) { - raise(AddCurrencyError.DataError(it)) + raise(it) } } diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/error/AddCurrencyError.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/error/AddCurrencyError.kt deleted file mode 100644 index 87255e52f7..0000000000 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/error/AddCurrencyError.kt +++ /dev/null @@ -1,6 +0,0 @@ -package com.tangem.domain.tokens.error - -sealed class AddCurrencyError { - - data class DataError(val cause: Throwable) : AddCurrencyError() -} \ No newline at end of file From 8b247ae29128fae69f214e6f8946249206dc3055 Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 8 Jul 2024 15:46:58 +0400 Subject: [PATCH 3/8] Updated on 2026-08-14 --- .../domain/card/DefaultResetCardUseCase.kt | 61 +++++++++++-------- .../domain/card/models/ResetCardError.kt | 6 +- 2 files changed, 40 insertions(+), 27 deletions(-) diff --git a/app/src/main/java/com/tangem/tap/domain/card/DefaultResetCardUseCase.kt b/app/src/main/java/com/tangem/tap/domain/card/DefaultResetCardUseCase.kt index 244dd02b7b..966f9613e9 100644 --- a/app/src/main/java/com/tangem/tap/domain/card/DefaultResetCardUseCase.kt +++ b/app/src/main/java/com/tangem/tap/domain/card/DefaultResetCardUseCase.kt @@ -1,15 +1,15 @@ package com.tangem.tap.domain.card import arrow.core.Either -import arrow.core.left +import arrow.core.raise.Raise import arrow.core.raise.either -import arrow.core.right +import arrow.fx.coroutines.ResourceScope +import arrow.fx.coroutines.resourceScope import com.tangem.common.CompletionResult import com.tangem.common.UserCodeType import com.tangem.common.core.TangemError import com.tangem.common.core.TangemSdkError import com.tangem.common.core.UserCodeRequestPolicy -import com.tangem.common.doOnResult import com.tangem.domain.card.ResetCardUseCase import com.tangem.domain.card.models.ResetCardError import com.tangem.domain.models.scan.CardDTO @@ -20,36 +20,44 @@ internal class DefaultResetCardUseCase( private val tangemSdkManager: TangemSdkManager, ) : ResetCardUseCase { - override suspend fun invoke(card: CardDTO): Either = either { - enterRequiredAccessCode(card) { - resetToFactorySettings( + override suspend fun invoke(card: CardDTO): Either = resourceScope { + either { + withUserCodeRequestPolicy(card) + + tangemSdkManager.resetToFactorySettings( cardId = card.cardId, allowsRequestAccessCodeFromRepository = true, - ) + ).bind(raise = this) } - .mapToEither() } override suspend fun invoke( cardNumber: Int, card: CardDTO, userWalletId: UserWalletId, - ): Either = either { - enterRequiredAccessCode(card) { - resetBackupCard(cardNumber = cardNumber, userWalletId = userWalletId) + ): Either = resourceScope { + either { + withUserCodeRequestPolicy(card) + + tangemSdkManager.resetBackupCard( + cardNumber = cardNumber, + userWalletId = userWalletId, + ).bind(raise = this) } - .mapToEither() } - private suspend fun enterRequiredAccessCode( - card: CardDTO, - task: suspend TangemSdkManager.() -> CompletionResult<*>, - ): CompletionResult<*> { - val policyBeforeReset = tangemSdkManager.userCodeRequestPolicy - requestMandatoryAccessCodeEntry(card) + private suspend fun ResourceScope.withUserCodeRequestPolicy(card: CardDTO) { + install( + acquire = { + val policyBeforeReset = tangemSdkManager.userCodeRequestPolicy + requestMandatoryAccessCodeEntry(card) - return tangemSdkManager.task() - .doOnResult { tangemSdkManager.setUserCodeRequestPolicy(policyBeforeReset) } + policyBeforeReset + }, + release = { prevPolicy, _ -> + tangemSdkManager.setUserCodeRequestPolicy(prevPolicy) + }, + ) } private fun requestMandatoryAccessCodeEntry(card: CardDTO) { @@ -66,14 +74,19 @@ internal class DefaultResetCardUseCase( } } - private fun CompletionResult<*>.mapToEither(): Either { + private fun CompletionResult<*>.bind(raise: Raise) { return when (this) { - is CompletionResult.Failure -> error.mapToDomainError().left() - is CompletionResult.Success -> Unit.right() + is CompletionResult.Failure -> { + val domainError = error.mapToDomainError() + + raise.raise(domainError) + } + is CompletionResult.Success -> { /* no-op */ + } } } private fun TangemError.mapToDomainError(): ResetCardError { - return if (this is TangemSdkError.UserCancelled) ResetCardError.UserCanceled else ResetCardError.AnotherSdkError + return if (this is TangemSdkError.UserCancelled) ResetCardError.UserCanceled else ResetCardError.SdkError } } \ No newline at end of file diff --git a/domain/card/src/main/kotlin/com/tangem/domain/card/models/ResetCardError.kt b/domain/card/src/main/kotlin/com/tangem/domain/card/models/ResetCardError.kt index 7db8690e89..eb3c61288d 100644 --- a/domain/card/src/main/kotlin/com/tangem/domain/card/models/ResetCardError.kt +++ b/domain/card/src/main/kotlin/com/tangem/domain/card/models/ResetCardError.kt @@ -1,8 +1,8 @@ package com.tangem.domain.card.models -sealed interface ResetCardError { +sealed class ResetCardError { - data object UserCanceled : ResetCardError + data object UserCanceled : ResetCardError() - data object AnotherSdkError : ResetCardError + data object SdkError : ResetCardError() } \ No newline at end of file From 3d07a7dd18222831105924c36abbd9925aea0a60 Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 8 Jul 2024 15:58:11 +0400 Subject: [PATCH 4/8] Updated on 2026-08-14 --- .../send/impl/presentation/viewmodel/SendViewModel.kt | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/viewmodel/SendViewModel.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/viewmodel/SendViewModel.kt index 456d91b7c6..30ce55fb24 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/viewmodel/SendViewModel.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/viewmodel/SendViewModel.kt @@ -894,13 +894,16 @@ internal class SendViewModel @Inject constructor( } private suspend fun sendTransaction(txData: TransactionData) { - sendTransactionUseCase( + val result = sendTransactionUseCase( txData = txData, userWallet = userWallet, network = cryptoCurrency.network, - ).fold( + ) + + uiState = stateFactory.getSendingStateUpdate(isSending = false) + + result.fold( ifLeft = { error -> - uiState = stateFactory.getSendingStateUpdate(isSending = false) uiState = eventStateFactory.getSendTransactionErrorState( error = error, onConsume = { uiState = eventStateFactory.onConsumeEventState() }, @@ -908,7 +911,6 @@ internal class SendViewModel @Inject constructor( analyticsEventHandler.send(SendAnalyticEvents.TransactionError(cryptoCurrency.symbol)) }, ifRight = { - uiState = stateFactory.getSendingStateUpdate(isSending = false) updateTransactionStatus(txData) addTokenToWalletIfNeeded() scheduleUpdates() From c9af7551a77e7ce01bf3bb6943c1bcfd5d4a5cf2 Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 8 Jul 2024 16:32:03 +0400 Subject: [PATCH 5/8] Updated on 2026-08-14 --- .../send/impl/presentation/SendFragment.kt | 6 +- .../presentation/viewmodel/SendViewModel.kt | 187 +++++++++--------- 2 files changed, 98 insertions(+), 95 deletions(-) diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/SendFragment.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/SendFragment.kt index 1c7d7f9cee..4eb34fce88 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/SendFragment.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/SendFragment.kt @@ -2,6 +2,7 @@ package com.tangem.features.send.impl.presentation import android.os.Bundle import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue import androidx.compose.ui.Modifier import androidx.fragment.app.viewModels import androidx.lifecycle.compose.collectAsStateWithLifecycle @@ -61,8 +62,11 @@ internal class SendFragment : ComposeFragment() { SystemBarsEffect { setSystemBarsColor(systemBarsColor) } + val currentState = viewModel.stateRouter.currentState.collectAsStateWithLifecycle() - SendScreen(viewModel.uiState, currentState.value) + val uiState by viewModel.uiState.collectAsStateWithLifecycle() + + SendScreen(uiState, currentState.value) } override fun onDestroy() { diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/viewmodel/SendViewModel.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/viewmodel/SendViewModel.kt index 30ce55fb24..622ca294c5 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/viewmodel/SendViewModel.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/viewmodel/SendViewModel.kt @@ -1,9 +1,6 @@ package com.tangem.features.send.impl.presentation.viewmodel import android.os.SystemClock -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.setValue import androidx.lifecycle.* import arrow.core.Either import arrow.core.getOrElse @@ -123,7 +120,7 @@ internal class SendViewModel @Inject constructor( private val stateFactory = SendStateFactory( clickIntents = this, stateRouterProvider = Provider { stateRouter }, - currentStateProvider = Provider { uiState }, + currentStateProvider = Provider { uiState.value }, userWalletProvider = Provider { userWallet }, appCurrencyProvider = Provider(selectedAppCurrencyFlow::value), cryptoCurrencyStatusProvider = Provider { cryptoCurrencyStatus }, @@ -133,7 +130,7 @@ internal class SendViewModel @Inject constructor( private val recipientStateFactory = RecipientSendFactory( stateRouterProvider = Provider { stateRouter }, - currentStateProvider = Provider { uiState }, + currentStateProvider = Provider { uiState.value }, cryptoCurrencyStatusProvider = Provider { cryptoCurrencyStatus }, isUtxoConsolidationAvailableProvider = Provider { isUtxoConsolidationAvailable }, validateWalletMemoUseCase = validateWalletMemoUseCase, @@ -141,14 +138,14 @@ internal class SendViewModel @Inject constructor( private val amountStateFactory = AmountStateFactory( stateRouterProvider = Provider { stateRouter }, - currentStateProvider = Provider { uiState }, + currentStateProvider = Provider { uiState.value }, cryptoCurrencyStatusProvider = Provider { cryptoCurrencyStatus }, ) private val feeStateFactory = FeeStateFactory( clickIntents = this, stateRouterProvider = Provider { stateRouter }, - currentStateProvider = Provider { uiState }, + currentStateProvider = Provider { uiState.value }, feeCryptoCurrencyStatusProvider = Provider { feeCryptoCurrencyStatus }, appCurrencyProvider = Provider(selectedAppCurrencyFlow::value), isFeeApproximateUseCase = isFeeApproximateUseCase, @@ -157,13 +154,13 @@ internal class SendViewModel @Inject constructor( private val eventStateFactory = SendEventStateFactory( clickIntents = this, stateRouterProvider = Provider { stateRouter }, - currentStateProvider = Provider { uiState }, + currentStateProvider = Provider { uiState.value }, cryptoCurrencyStatusProvider = Provider { cryptoCurrencyStatus }, feeStateFactory = feeStateFactory, ) private val feeNotificationFactory = FeeNotificationFactory( - currentStateProvider = Provider { uiState }, + currentStateProvider = Provider { uiState.value }, stateRouterProvider = Provider { stateRouter }, clickIntents = this, ) @@ -171,7 +168,7 @@ internal class SendViewModel @Inject constructor( private val sendNotificationFactory = SendNotificationFactory( cryptoCurrencyStatusProvider = Provider { cryptoCurrencyStatus }, coinCryptoCurrencyStatusProvider = Provider { coinCryptoCurrencyStatus }, - currentStateProvider = Provider { uiState }, + currentStateProvider = Provider { uiState.value }, userWalletProvider = Provider { userWallet }, stateRouterProvider = Provider { stateRouter }, isSubtractAvailableProvider = Provider { isAmountSubtractAvailable }, @@ -186,15 +183,16 @@ internal class SendViewModel @Inject constructor( private val sendScreenAnalyticSender by lazy(LazyThreadSafetyMode.NONE) { SendScreenAnalyticSender( stateRouterProvider = Provider { stateRouter }, - currentStateProvider = Provider { uiState }, + currentStateProvider = Provider { uiState.value }, analyticsEventHandler = analyticsEventHandler, cryptoCurrencyProvider = Provider { cryptoCurrency }, ) } // todo convert to StateFlow - var uiState: SendUiState by mutableStateOf(stateFactory.getInitialState()) - private set + val uiState: MutableStateFlow = MutableStateFlow( + value = stateFactory.getInitialState(), + ) private var userWallet: UserWallet by Delegates.notNull() private var userWallets: List = emptyList() @@ -272,7 +270,7 @@ internal class SendViewModel @Inject constructor( .conflate() .distinctUntilChanged() .onEach { - uiState = stateFactory.getOnHideBalanceState(isBalanceHidden = it.isBalanceHidden) + uiState.value = stateFactory.getOnHideBalanceState(isBalanceHidden = it.isBalanceHidden) } .launchIn(viewModelScope) .saveIn(balanceHidingJobHolder) @@ -373,15 +371,15 @@ internal class SendViewModel @Inject constructor( feeCryptoCurrencyStatus = feeCurrencyStatus subscribeOnQRScannerResult() when { - uiState.sendState?.isSuccess == true -> return + uiState.value.sendState?.isSuccess == true -> return transactionId != null && amount != null && destinationAddress != null -> { loadFee() - uiState = stateFactory.getReadyState(amount, destinationAddress, memo) + uiState.value = stateFactory.getReadyState(amount, destinationAddress, memo) stateRouter.showSend() updateNotifications() } else -> { - uiState = stateFactory.getReadyState() + uiState.value = stateFactory.getReadyState() getWalletsAndRecent() stateRouter.showRecipient() updateNotifications() @@ -405,9 +403,9 @@ internal class SendViewModel @Inject constructor( } }.onSuccess { result -> userWallets = result - uiState = recipientStateFactory.onLoadedWalletsList(wallets = userWallets) + uiState.value = recipientStateFactory.onLoadedWalletsList(wallets = userWallets) }.onFailure { - uiState = recipientStateFactory.onLoadedWalletsList(wallets = emptyList()) + uiState.value = recipientStateFactory.onLoadedWalletsList(wallets = emptyList()) } } } @@ -445,7 +443,7 @@ internal class SendViewModel @Inject constructor( pageSize = RECENT_TX_SIZE, ).getOrElse { emptyList() } } - uiState = recipientStateFactory.onLoadedHistoryList(txHistory = txHistoryList) + uiState.value = recipientStateFactory.onLoadedHistoryList(txHistory = txHistoryList) } private fun onStateActive() { @@ -456,7 +454,7 @@ internal class SendViewModel @Inject constructor( SendUiStateType.EditFee, -> loadFee() SendUiStateType.Send -> { - uiState = stateFactory.getIsAmountSubtractedState(isAmountSubtractAvailable) + uiState.value = stateFactory.getIsAmountSubtractedState(isAmountSubtractAvailable) sendIdleTimer = SystemClock.elapsedRealtime() } else -> Unit @@ -469,7 +467,7 @@ internal class SendViewModel @Inject constructor( sendNotificationFactory.create() .conflate() .distinctUntilChanged() - .onEach { uiState = stateFactory.getSendNotificationState(notifications = it) } + .onEach { uiState.value = stateFactory.getSendNotificationState(notifications = it) } .flowOn(dispatchers.main) .launchIn(viewModelScope) .saveIn(sendNotificationsJobHolder) @@ -479,7 +477,7 @@ internal class SendViewModel @Inject constructor( feeNotificationFactory.create() .conflate() .distinctUntilChanged() - .onEach { uiState = feeStateFactory.getFeeNotificationState(notifications = it) } + .onEach { uiState.value = feeStateFactory.getFeeNotificationState(notifications = it) } .flowOn(dispatchers.main) .launchIn(viewModelScope) .saveIn(feeNotificationsJobHolder) @@ -489,7 +487,7 @@ internal class SendViewModel @Inject constructor( override fun popBackStack() = stateRouter.popBackStack() override fun onBackClick() { cancelFeeRequest() - stateRouter.onBackClick(isSuccess = uiState.sendState?.isSuccess == true) + stateRouter.onBackClick(isSuccess = uiState.value.sendState?.isSuccess == true) } override fun onCloseClick() { @@ -505,8 +503,8 @@ internal class SendViewModel @Inject constructor( override fun onNextClick(isFromEdit: Boolean) { val currentState = stateRouter.currentState.value - uiState = stateFactory.syncEditStates(isFromEdit = isFromEdit) - sendScreenAnalyticSender.send(currentState.type, uiState) + uiState.value = stateFactory.syncEditStates(isFromEdit = isFromEdit) + sendScreenAnalyticSender.send(currentState.type, uiState.value) when (currentState.type) { SendUiStateType.Fee, SendUiStateType.EditFee, @@ -531,9 +529,9 @@ internal class SendViewModel @Inject constructor( } override fun onFailedTxEmailClick(errorMessage: String) { - val recipient = uiState.recipientState?.addressTextField?.value - val feeValue = uiState.feeState?.fee?.amount?.value - val amountValue = uiState.amountState?.amountTextField?.cryptoAmount?.value + val recipient = uiState.value.recipientState?.addressTextField?.value + val feeValue = uiState.value.feeState?.fee?.amount?.value + val amountValue = uiState.value.amountState?.amountTextField?.cryptoAmount?.value val receivingAmount = if (amountValue != null && feeValue != null) { checkAndCalculateSubtractedAmount( @@ -541,7 +539,7 @@ internal class SendViewModel @Inject constructor( cryptoCurrencyStatus = cryptoCurrencyStatus, amountValue = amountValue, feeValue = feeValue, - reduceAmountBy = uiState.sendState?.reduceAmountBy ?: BigDecimal.ZERO, + reduceAmountBy = uiState.value.sendState?.reduceAmountBy ?: BigDecimal.ZERO, ) } else { null @@ -562,20 +560,20 @@ internal class SendViewModel @Inject constructor( innerRouter.openTokenDetails(userWalletId, currency) private fun onFeeNext(): Boolean { - val feeState = uiState.getFeeState(stateRouter.isEditState) + val feeState = uiState.value.getFeeState(stateRouter.isEditState) val feeSelectorState = feeState?.feeSelectorState as? FeeSelectorState.Content ?: return false if (checkIfFeeTooLow(feeSelectorState)) { - uiState = eventStateFactory.getFeeTooLowAlert( - onConsume = { uiState = eventStateFactory.onConsumeEventState() }, + uiState.value = eventStateFactory.getFeeTooLowAlert( + onConsume = { uiState.value = eventStateFactory.onConsumeEventState() }, ) return true } return checkIfFeeTooHigh( feeSelectorState = feeSelectorState, onShow = { diff -> - uiState = eventStateFactory.getFeeTooHighAlert( + uiState.value = eventStateFactory.getFeeTooHighAlert( diff = diff, - onConsume = { uiState = eventStateFactory.onConsumeEventState() }, + onConsume = { uiState.value = eventStateFactory.onConsumeEventState() }, ) }, ) @@ -607,20 +605,20 @@ internal class SendViewModel @Inject constructor( // region amount state clicks override fun onCurrencyChangeClick(isFiat: Boolean) { - uiState = amountStateFactory.getOnCurrencyChangedState(isFiat) + uiState.value = amountStateFactory.getOnCurrencyChangedState(isFiat) } override fun onAmountValueChange(value: String) { - uiState = amountStateFactory.getOnAmountValueChange(value) + uiState.value = amountStateFactory.getOnAmountValueChange(value) } override fun onMaxValueClick() { - uiState = amountStateFactory.getOnMaxAmountClick() + uiState.value = amountStateFactory.getOnMaxAmountClick() analyticsEventHandler.send(SendAnalyticEvents.MaxAmountButtonClicked) } override fun onAmountPasteTriggerDismiss() { - uiState = amountStateFactory.getOnAmountPastedTriggerDismiss() + uiState.value = amountStateFactory.getOnAmountPastedTriggerDismiss() } // endregion @@ -629,10 +627,10 @@ internal class SendViewModel @Inject constructor( override fun onRecipientAddressValueChange(value: String, type: EnterAddressSource?) { viewModelScope.launch { if (!checkIfXrpAddressValue(value)) { - uiState = recipientStateFactory.onRecipientAddressValueChange(value, isValuePasted = type != null) - uiState = recipientStateFactory.getOnRecipientAddressValidationStarted() + uiState.value = recipientStateFactory.onRecipientAddressValueChange(value, isValuePasted = type != null) + uiState.value = recipientStateFactory.getOnRecipientAddressValidationStarted() val isValidAddress = validateAddress(value) - uiState = recipientStateFactory.getOnRecipientAddressValidState(value, isValidAddress) + uiState.value = recipientStateFactory.getOnRecipientAddressValidState(value, isValidAddress) type?.let { analyticsEventHandler.send( SendAnalyticEvents.AddressEntered( @@ -649,11 +647,11 @@ internal class SendViewModel @Inject constructor( override fun onRecipientMemoValueChange(value: String, isValuePasted: Boolean) { viewModelScope.launch { if (!checkIfXrpAddressValue(value)) { - uiState = recipientStateFactory.getOnRecipientMemoValueChange(value, isValuePasted) - uiState = recipientStateFactory.getOnRecipientAddressValidationStarted() - val recipientState = uiState.getRecipientState(stateRouter.isEditState) + uiState.value = recipientStateFactory.getOnRecipientMemoValueChange(value, isValuePasted) + uiState.value = recipientStateFactory.getOnRecipientAddressValidationStarted() + val recipientState = uiState.value.getRecipientState(stateRouter.isEditState) val maybeValidAddress = validateAddress(recipientState?.addressTextField?.value.orEmpty()) - uiState = recipientStateFactory.getOnRecipientMemoValidState(value, maybeValidAddress.isRight()) + uiState.value = recipientStateFactory.getOnRecipientMemoValidState(value, maybeValidAddress.isRight()) } }.saveIn(memoValidationJobHolder) } @@ -675,21 +673,22 @@ internal class SendViewModel @Inject constructor( private suspend fun checkIfXrpAddressValue(value: String): Boolean { return BlockchainUtils.decodeRippleXAddress(value, cryptoCurrency.network.id.value)?.let { decodedAddress -> - uiState = + uiState.value = recipientStateFactory.onRecipientAddressValueChange(value, isXAddress = true, isValuePasted = true) - uiState = recipientStateFactory.getOnXAddressMemoState() + uiState.value = recipientStateFactory.getOnXAddressMemoState() val isValidAddress = validateAddress(decodedAddress.address) - uiState = recipientStateFactory.getOnRecipientAddressValidState(decodedAddress.address, isValidAddress) + uiState.value = + recipientStateFactory.getOnRecipientAddressValidState(decodedAddress.address, isValidAddress) true } ?: false } private fun onEnteredValidAddress(isNotValid: Boolean) { - uiState = recipientStateFactory.getHiddenRecentListState(isNotValid = isNotValid) + uiState.value = recipientStateFactory.getHiddenRecentListState(isNotValid = isNotValid) } private fun autoNextFromRecipient(type: EnterAddressSource?, isValidAddress: Boolean) { - val memo = uiState.getRecipientState(stateRouter.isEditState)?.memoTextField?.value + val memo = uiState.value.getRecipientState(stateRouter.isEditState)?.memoTextField?.value val isValidMemo = validateMemo(memo) val isRecent = type == EnterAddressSource.RecentAddress @@ -701,7 +700,7 @@ internal class SendViewModel @Inject constructor( override fun feeReload() = loadFee() override fun onFeeSelectorClick(feeType: FeeType) { - uiState = feeStateFactory.onFeeSelectedState(feeType) + uiState.value = feeStateFactory.onFeeSelectedState(feeType) updateFeeNotifications() if (feeType == FeeType.Custom) { analyticsEventHandler.send(SendAnalyticEvents.CustomFeeButtonClicked) @@ -709,7 +708,7 @@ internal class SendViewModel @Inject constructor( } override fun onCustomFeeValueChange(index: Int, value: String) { - uiState = feeStateFactory.onCustomFeeValueChange(index, value) + uiState.value = feeStateFactory.onCustomFeeValueChange(index, value) updateFeeNotifications() } @@ -725,14 +724,14 @@ internal class SendViewModel @Inject constructor( private fun loadFee() { viewModelScope.launch { - val isShowStatus = uiState.feeState?.fee == null + val isShowStatus = uiState.value.feeState?.fee == null if (isShowStatus) { - uiState = feeStateFactory.onFeeOnLoadingState() + uiState.value = feeStateFactory.onFeeOnLoadingState() updateNotifications() } val result = callFeeUseCase()?.fold( ifRight = { - uiState = feeStateFactory.onFeeOnLoadedState(it) + uiState.value = feeStateFactory.onFeeOnLoadedState(it) sendIdleTimer = SystemClock.elapsedRealtime() }, ifLeft = { @@ -748,7 +747,7 @@ internal class SendViewModel @Inject constructor( } private fun onFeeLoadFailed(isShowStatus: Boolean) { - if (isShowStatus) uiState = feeStateFactory.onFeeOnErrorState() + if (isShowStatus) uiState.value = feeStateFactory.onFeeOnErrorState() } private suspend fun checkIfSubtractAvailable() { @@ -767,8 +766,8 @@ internal class SendViewModel @Inject constructor( private suspend fun callFeeUseCase(): Either? { val isFromConfirmation = stateRouter.currentState.value.isFromConfirmation - val amountState = uiState.getAmountState(isFromConfirmation) ?: return null - val recipientState = uiState.getRecipientState(isFromConfirmation) ?: return null + val amountState = uiState.value.getAmountState(isFromConfirmation) ?: return null + val recipientState = uiState.value.getRecipientState(isFromConfirmation) ?: return null val amount = amountState.amountTextField.cryptoAmount.value ?: return null return getFeeUseCase.invoke( @@ -782,10 +781,10 @@ internal class SendViewModel @Inject constructor( // region send state clicks override fun onSendClick() { - val sendState = uiState.sendState ?: return + val sendState = uiState.value.sendState ?: return if (sendState.isSuccess) popBackStack() - uiState = stateFactory.getSendingStateUpdate(isSending = true) + uiState.value = stateFactory.getSendingStateUpdate(isSending = true) if (SystemClock.elapsedRealtime() - sendIdleTimer < CHECK_FEE_UPDATE_DELAY) { verifyAndSendTransaction() } else { @@ -795,22 +794,22 @@ internal class SendViewModel @Inject constructor( } override fun showAmount() { - uiState = stateFactory.syncEditStates(isFromEdit = false) + uiState.value = stateFactory.syncEditStates(isFromEdit = false) stateRouter.showAmount(isFromConfirmation = true) setNeverToShowTapHelp() analyticsEventHandler.send(SendAnalyticEvents.ScreenReopened(SendScreenSource.Amount)) } override fun showRecipient() { - uiState = stateFactory.syncEditStates(isFromEdit = false) + uiState.value = stateFactory.syncEditStates(isFromEdit = false) stateRouter.showRecipient(isFromConfirmation = true) - uiState = stateFactory.getHiddenTapHelpState() + uiState.value = stateFactory.getHiddenTapHelpState() setNeverToShowTapHelp() analyticsEventHandler.send(SendAnalyticEvents.ScreenReopened(SendScreenSource.Address)) } override fun showFee() { - uiState = stateFactory.syncEditStates(isFromEdit = false) + uiState.value = stateFactory.syncEditStates(isFromEdit = false) stateRouter.showFee(isFromConfirmation = true) setNeverToShowTapHelp() analyticsEventHandler.send(SendAnalyticEvents.ScreenReopened(SendScreenSource.Fee)) @@ -821,7 +820,7 @@ internal class SendViewModel @Inject constructor( } override fun onExploreClick() { - val sendState = uiState.sendState ?: return + val sendState = uiState.value.sendState ?: return analyticsEventHandler.send(SendAnalyticEvents.ExploreButtonClicked) innerRouter.openUrl(sendState.txUrl) } @@ -836,7 +835,7 @@ internal class SendViewModel @Inject constructor( reduceAmountTo: BigDecimal?, clazz: Class, ) { - uiState = when { + uiState.value = when { reduceAmountBy != null && reduceAmountByDiff != null -> amountStateFactory.getOnAmountReduceByState( reduceAmountBy = reduceAmountBy, reduceAmountByDiff = reduceAmountByDiff, @@ -845,20 +844,20 @@ internal class SendViewModel @Inject constructor( else -> return } - uiState = sendNotificationFactory.dismissNotificationState(clazz) + uiState.value = sendNotificationFactory.dismissNotificationState(clazz) updateNotifications() } override fun onNotificationCancel(clazz: Class) { - uiState = sendNotificationFactory.dismissNotificationState(clazz = clazz, isIgnored = true) + uiState.value = sendNotificationFactory.dismissNotificationState(clazz = clazz, isIgnored = true) } private fun verifyAndSendTransaction() { - val recipient = uiState.recipientState?.addressTextField?.value ?: return - val feeState = uiState.feeState ?: return + val recipient = uiState.value.recipientState?.addressTextField?.value ?: return + val feeState = uiState.value.feeState ?: return val fee = feeState.fee ?: return - val memo = uiState.recipientState?.memoTextField?.value - val amountValue = uiState.amountState?.amountTextField?.cryptoAmount?.value ?: return + val memo = uiState.value.recipientState?.memoTextField?.value + val amountValue = uiState.value.amountState?.amountTextField?.cryptoAmount?.value ?: return val feeValue = fee.amount.value ?: return val receivingAmount = checkAndCalculateSubtractedAmount( @@ -866,7 +865,7 @@ internal class SendViewModel @Inject constructor( cryptoCurrencyStatus = cryptoCurrencyStatus, amountValue = amountValue, feeValue = feeValue, - reduceAmountBy = uiState.sendState?.reduceAmountBy ?: BigDecimal.ZERO, + reduceAmountBy = uiState.value.sendState?.reduceAmountBy ?: BigDecimal.ZERO, ) viewModelScope.launch { @@ -880,10 +879,10 @@ internal class SendViewModel @Inject constructor( ).fold( ifLeft = { Timber.e(it) - uiState = stateFactory.getSendingStateUpdate(isSending = false) - uiState = eventStateFactory.getGenericErrorState( + uiState.value = stateFactory.getSendingStateUpdate(isSending = false) + uiState.value = eventStateFactory.getGenericErrorState( error = it, - onConsume = { uiState = eventStateFactory.onConsumeEventState() }, + onConsume = { uiState.value = eventStateFactory.onConsumeEventState() }, ) }, ifRight = { txData -> @@ -900,13 +899,13 @@ internal class SendViewModel @Inject constructor( network = cryptoCurrency.network, ) - uiState = stateFactory.getSendingStateUpdate(isSending = false) + uiState.value = stateFactory.getSendingStateUpdate(isSending = false) result.fold( ifLeft = { error -> - uiState = eventStateFactory.getSendTransactionErrorState( + uiState.value = eventStateFactory.getSendTransactionErrorState( error = error, - onConsume = { uiState = eventStateFactory.onConsumeEventState() }, + onConsume = { uiState.value = eventStateFactory.onConsumeEventState() }, ) analyticsEventHandler.send(SendAnalyticEvents.TransactionError(cryptoCurrency.symbol)) }, @@ -922,7 +921,7 @@ internal class SendViewModel @Inject constructor( private fun addTokenToWalletIfNeeded() { if (cryptoCurrency !is CryptoCurrency.Token) return - val recipientState = uiState.getRecipientState(stateRouter.isEditState) ?: return + val recipientState = uiState.value.getRecipientState(stateRouter.isEditState) ?: return val destinationAddress = recipientState.addressTextField.value val receivingUserWallet = userWallets.firstOrNull { it.address == destinationAddress } ?: return @@ -941,7 +940,7 @@ internal class SendViewModel @Inject constructor( userWalletId = userWalletId, network = cryptoCurrency.network, ).getOrElse { "" } - uiState = stateFactory.getTransactionSendState(txData, txUrl) + uiState.value = stateFactory.getTransactionSendState(txData, txUrl) } private fun scheduleUpdates() { @@ -959,7 +958,7 @@ internal class SendViewModel @Inject constructor( } private fun onCheckFeeUpdate() { - val sendState = uiState.sendState ?: return + val sendState = uiState.value.sendState ?: return val isSuccess = sendState.isSuccess val noErrorNotifications = sendState.notifications.none { it is SendNotification.Error } @@ -967,30 +966,30 @@ internal class SendViewModel @Inject constructor( viewModelScope.launch { val feeUpdatedState = callFeeUseCase()?.fold( ifRight = { - uiState = stateFactory.getSendingStateUpdate(isSending = false) + uiState.value = stateFactory.getSendingStateUpdate(isSending = false) eventStateFactory.getFeeUpdatedAlert( fee = it, - onConsume = { uiState = eventStateFactory.onConsumeEventState() }, + onConsume = { uiState.value = eventStateFactory.onConsumeEventState() }, onFeeNotIncreased = { - uiState = stateFactory.getSendingStateUpdate(isSending = true) + uiState.value = stateFactory.getSendingStateUpdate(isSending = true) verifyAndSendTransaction() }, ) }, ifLeft = { - uiState = stateFactory.getSendingStateUpdate(isSending = false) + uiState.value = stateFactory.getSendingStateUpdate(isSending = false) eventStateFactory.getFeeUnreachableErrorState( - onConsume = { uiState = eventStateFactory.onConsumeEventState() }, + onConsume = { uiState.value = eventStateFactory.onConsumeEventState() }, ) }, ) - uiState = if (feeUpdatedState != null) { + uiState.value = if (feeUpdatedState != null) { feeUpdatedState } else { - uiState = stateFactory.getSendingStateUpdate(isSending = false) + uiState.value = stateFactory.getSendingStateUpdate(isSending = false) eventStateFactory.getFeeUnreachableErrorState( - onConsume = { uiState = eventStateFactory.onConsumeEventState() }, + onConsume = { uiState.value = eventStateFactory.onConsumeEventState() }, ) } } @@ -1001,12 +1000,12 @@ internal class SendViewModel @Inject constructor( viewModelScope.launch { neverShowTapHelpUseCase() } - uiState = stateFactory.getHiddenTapHelpState() + uiState.value = stateFactory.getHiddenTapHelpState() } private fun showErrorAlert() { - uiState = eventStateFactory.getGenericErrorState( - onConsume = { uiState = eventStateFactory.onConsumeEventState() }, + uiState.value = eventStateFactory.getGenericErrorState( + onConsume = { uiState.value = eventStateFactory.onConsumeEventState() }, ) } // endregion From 2f325fa46f9b0303128dfe3f62d0188ef942e603 Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 8 Jul 2024 16:50:43 +0400 Subject: [PATCH 6/8] Updated on 2026-08-14 --- .../presentation/AddCustomTokenFragment.kt | 7 +- .../viewmodels/AddCustomTokenViewModel.kt | 539 ++++++++++-------- 2 files changed, 298 insertions(+), 248 deletions(-) diff --git a/app/src/main/java/com/tangem/tap/features/customtoken/impl/presentation/AddCustomTokenFragment.kt b/app/src/main/java/com/tangem/tap/features/customtoken/impl/presentation/AddCustomTokenFragment.kt index da295c57c5..f2b19a713d 100644 --- a/app/src/main/java/com/tangem/tap/features/customtoken/impl/presentation/AddCustomTokenFragment.kt +++ b/app/src/main/java/com/tangem/tap/features/customtoken/impl/presentation/AddCustomTokenFragment.kt @@ -2,9 +2,11 @@ package com.tangem.tap.features.customtoken.impl.presentation import androidx.compose.foundation.layout.systemBarsPadding import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalLifecycleOwner import androidx.hilt.navigation.compose.hiltViewModel +import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.tangem.core.ui.UiDependencies import com.tangem.core.ui.components.SystemBarsEffect import com.tangem.core.ui.res.TangemTheme @@ -34,9 +36,12 @@ internal class AddCustomTokenFragment : ComposeFragment() { SystemBarsEffect { setSystemBarsColor(color = statusBarColor) } + + val state by viewModel.uiState.collectAsStateWithLifecycle() + AddCustomTokenScreen( modifier = Modifier.systemBarsPadding(), - stateHolder = viewModel.uiState, + stateHolder = state, ) } } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/customtoken/impl/presentation/viewmodels/AddCustomTokenViewModel.kt b/app/src/main/java/com/tangem/tap/features/customtoken/impl/presentation/viewmodels/AddCustomTokenViewModel.kt index 5f523da11c..3b9c1f3f5d 100644 --- a/app/src/main/java/com/tangem/tap/features/customtoken/impl/presentation/viewmodels/AddCustomTokenViewModel.kt +++ b/app/src/main/java/com/tangem/tap/features/customtoken/impl/presentation/viewmodels/AddCustomTokenViewModel.kt @@ -1,9 +1,6 @@ package com.tangem.tap.features.customtoken.impl.presentation.viewmodels import androidx.compose.foundation.text.KeyboardOptions -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.setValue import androidx.compose.ui.text.input.ImeAction import androidx.compose.ui.text.input.KeyboardType import androidx.lifecycle.DefaultLifecycleObserver @@ -49,6 +46,9 @@ import com.tangem.wallet.R import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.collections.immutable.persistentListOf import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.update +import kotlinx.coroutines.flow.updateAndGet import kotlinx.coroutines.launch import timber.log.Timber import javax.inject.Inject @@ -85,8 +85,7 @@ internal class AddCustomTokenViewModel @Inject constructor( private var currentCryptoCurrencies: List = emptyList() /** Screen state */ - var uiState by mutableStateOf(getInitialUiState()) - private set + val uiState: MutableStateFlow = MutableStateFlow(value = getInitialUiState()) private var foundToken: FoundToken? = null @@ -376,44 +375,48 @@ internal class AddCustomTokenViewModel @Inject constructor( } .onSuccess { token -> foundToken = token - uiState = uiState.copySealed( - form = uiState.form.copy( - contractAddressInputField = uiState.form.contractAddressInputField.copy( - isLoading = false, - ), - networkSelectorField = uiState.form.networkSelectorField.copy( - selectedItem = formStateBuilder.createNetworkSelectorItem( - blockchain = Blockchain.fromNetworkId(token.network.id) - ?: Blockchain.Unknown, + uiState.update { state -> + state.copySealed( + form = state.form.copy( + contractAddressInputField = state.form.contractAddressInputField.copy( + isLoading = false, + ), + networkSelectorField = state.form.networkSelectorField.copy( + selectedItem = formStateBuilder.createNetworkSelectorItem( + blockchain = Blockchain.fromNetworkId(token.network.id) + ?: Blockchain.Unknown, + ), + ), + tokenNameInputField = state.form.tokenNameInputField.copy( + value = token.name, + isEnabled = false, + ), + tokenSymbolInputField = state.form.tokenSymbolInputField.copy( + value = token.symbol, + isEnabled = false, + ), + decimalsInputField = state.form.decimalsInputField.copy( + value = token.network.decimalCount, + isEnabled = false, ), ), - tokenNameInputField = uiState.form.tokenNameInputField.copy( - value = token.name, - isEnabled = false, - ), - tokenSymbolInputField = uiState.form.tokenSymbolInputField.copy( - value = token.symbol, - isEnabled = false, - ), - decimalsInputField = uiState.form.decimalsInputField.copy( - value = token.network.decimalCount, - isEnabled = false, - ), - ), - ) + ) + } } .onFailure { foundToken = null - uiState = uiState.copySealed( - form = uiState.form.copy( - contractAddressInputField = uiState.form.contractAddressInputField.copy( - isLoading = false, + uiState.update { state -> + state.copySealed( + form = state.form.copy( + contractAddressInputField = state.form.contractAddressInputField.copy( + isLoading = false, + ), + tokenNameInputField = state.form.tokenNameInputField.copy(isEnabled = true), + tokenSymbolInputField = state.form.tokenSymbolInputField.copy(isEnabled = true), + decimalsInputField = state.form.decimalsInputField.copy(isEnabled = true), ), - tokenNameInputField = uiState.form.tokenNameInputField.copy(isEnabled = true), - tokenSymbolInputField = uiState.form.tokenSymbolInputField.copy(isEnabled = true), - decimalsInputField = uiState.form.decimalsInputField.copy(isEnabled = true), - ), - ) + ) + } Timber.e(it) } @@ -423,28 +426,31 @@ internal class AddCustomTokenViewModel @Inject constructor( } private fun isDerivationPathSelected(): Boolean { - return with(uiState.form.derivationPathSelectorField?.selectedItem?.blockchain) { - this != null && this != Blockchain.Unknown || - uiState.form.derivationPathSelectorField?.selectedItem?.type == DerivationPathSelectorType.CUSTOM && - !uiState.warnings.contains(AddCustomTokenWarning.WrongDerivationPath) - } + val blockchain = uiState.value.form.derivationPathSelectorField?.selectedItem?.blockchain + val selectorType = uiState.value.form.derivationPathSelectorField?.selectedItem?.type + + return blockchain != null && blockchain != Blockchain.Unknown || + selectorType == DerivationPathSelectorType.CUSTOM && + !uiState.value.warnings.contains(AddCustomTokenWarning.WrongDerivationPath) } private fun updateWarnings() { - uiState = uiState.copySealed( - warnings = buildSet { - when (getCustomTokenType()) { - CustomTokenType.TOKEN -> { - addAll(getTokenWarningSet()) - } + uiState.update { state -> + state.copySealed( + warnings = buildSet { + when (getCustomTokenType()) { + CustomTokenType.TOKEN -> { + addAll(getTokenWarningSet()) + } - CustomTokenType.BLOCKCHAIN -> { - if (isCustomTokenAlreadyAdded()) add(AddCustomTokenWarning.TokenAlreadyAdded) - if (isDerivationPathSelected()) add(AddCustomTokenWarning.PotentialScamToken) + CustomTokenType.BLOCKCHAIN -> { + if (isCustomTokenAlreadyAdded()) add(AddCustomTokenWarning.TokenAlreadyAdded) + if (isDerivationPathSelected()) add(AddCustomTokenWarning.PotentialScamToken) + } } - } - }, - ) + }, + ) + } } private fun getCustomTokenType(): CustomTokenType { @@ -456,24 +462,24 @@ internal class AddCustomTokenViewModel @Inject constructor( } private fun isAnyTokenFieldsFilled(): Boolean { - return with(uiState.form) { + return with(uiState.value.form) { contractAddressInputField.value.isNotEmpty() || tokenNameInputField.value.isNotEmpty() || tokenSymbolInputField.value.isNotEmpty() || decimalsInputField.value.isNotEmpty() } } private fun isAllTokenFieldsFilled(): Boolean { - return with(uiState.form) { + return with(uiState.value.form) { contractAddressInputField.value.isNotEmpty() && tokenNameInputField.value.isNotEmpty() && tokenSymbolInputField.value.isNotEmpty() && decimalsInputField.value.isNotEmpty() } } private fun getTokenWarningSet(): Set { - val networkSelectorValue = uiState.form.networkSelectorField.selectedItem.blockchain + val networkSelectorValue = uiState.value.form.networkSelectorField.selectedItem.blockchain val isContractAddressFieldEmpty = ContractAddressValidator.validate( - address = uiState.form.contractAddressInputField.value, + address = uiState.value.form.contractAddressInputField.value, blockchain = networkSelectorValue, ).let { it is ContractAddressValidatorResult.Error && it.type == AddCustomTokenError.FIELD_IS_EMPTY @@ -507,66 +513,70 @@ internal class AddCustomTokenViewModel @Inject constructor( } private fun isNetworkSelected(): Boolean { - return uiState.form.networkSelectorField.selectedItem.blockchain != Blockchain.Unknown + return uiState.value.form.networkSelectorField.selectedItem.blockchain != Blockchain.Unknown } private fun updateFloatingButton() { - uiState = updateStateWithDerivationError(uiState) + uiState.update { state -> + updateStateWithDerivationError(state) + } if (isCustomTokenAlreadyAdded()) { - uiState = uiState.copySealed( - warnings = uiState.warnings + AddCustomTokenWarning.TokenAlreadyAdded, - floatingButton = uiState.floatingButton.copy(isEnabled = false), - ) + uiState.update { state -> + state.copySealed( + warnings = state.warnings + AddCustomTokenWarning.TokenAlreadyAdded, + floatingButton = state.floatingButton.copy(isEnabled = false), + ) + } return } - val isCorrectDerivationInput = !uiState.warnings.contains(AddCustomTokenWarning.WrongDerivationPath) - val state = when { - isAllTokenFieldsFilled() && isNetworkSelected() -> { - val networkSelectorValue = uiState.form.networkSelectorField.selectedItem.blockchain - val error = ContractAddressValidator.validate( - address = uiState.form.contractAddressInputField.value, - blockchain = networkSelectorValue, - ) + uiState.update { state -> + val isCorrectDerivationInput = !state.warnings.contains(AddCustomTokenWarning.WrongDerivationPath) + val updatedState = when { + isAllTokenFieldsFilled() && isNetworkSelected() -> { + val networkSelectorValue = state.form.networkSelectorField.selectedItem.blockchain + val error = ContractAddressValidator.validate( + address = state.form.contractAddressInputField.value, + blockchain = networkSelectorValue, + ) - val isSupportedToken = getSelectedWalletSyncUseCase().fold( - ifLeft = { false }, - ifRight = { - it.scanResponse.card.canHandleToken( - blockchain = networkSelectorValue, - cardTypesResolver = it.scanResponse.cardTypesResolver, - ) - }, - ) - - uiState.copySealed( - floatingButton = uiState.floatingButton.copy( - isEnabled = error is ContractAddressValidatorResult.Success && - isSupportedToken && isCorrectDerivationInput, - ), - ) - } - - isAnyTokenFieldsFilled() -> { - uiState.copySealed(floatingButton = uiState.floatingButton.copy(isEnabled = false)) - } - - else -> { - uiState.copySealed( - floatingButton = uiState.floatingButton.copy( - isEnabled = if (isNetworkSelected()) { - !isBlockchainAlreadyAdded() && isCorrectDerivationInput - } else { - false + val isSupportedToken = getSelectedWalletSyncUseCase().fold( + ifLeft = { false }, + ifRight = { + it.scanResponse.card.canHandleToken( + blockchain = networkSelectorValue, + cardTypesResolver = it.scanResponse.cardTypesResolver, + ) }, - ), - ) - } - } + ) - uiState = state.copySealed( - warnings = state.warnings - AddCustomTokenWarning.TokenAlreadyAdded, - ) + state.copySealed( + floatingButton = state.floatingButton.copy( + isEnabled = error is ContractAddressValidatorResult.Success && + isSupportedToken && isCorrectDerivationInput, + ), + ) + } + isAnyTokenFieldsFilled() -> { + state.copySealed(floatingButton = state.floatingButton.copy(isEnabled = false)) + } + else -> { + state.copySealed( + floatingButton = state.floatingButton.copy( + isEnabled = if (isNetworkSelected()) { + !isBlockchainAlreadyAdded() && isCorrectDerivationInput + } else { + false + }, + ), + ) + } + } + + updatedState.copySealed( + warnings = updatedState.warnings - AddCustomTokenWarning.TokenAlreadyAdded, + ) + } } private fun updateStateWithDerivationError(uiState: AddCustomTokenStateHolder): AddCustomTokenStateHolder { @@ -599,11 +609,11 @@ internal class AddCustomTokenViewModel @Inject constructor( } private fun isTokenAlreadyAdded(): Boolean { - val networkSelectorValue = uiState.form.networkSelectorField.selectedItem.blockchain + val networkSelectorValue = uiState.value.form.networkSelectorField.selectedItem.blockchain val networkId = Blockchain.fromNetworkId(networkSelectorValue.toNetworkId())?.id val contractAddress = convertTokenAddress( blockchain = networkSelectorValue, - address = uiState.form.contractAddressInputField.value, + address = uiState.value.form.contractAddressInputField.value, ) ?: return false // invalid address can't be "already added" return currentCryptoCurrencies @@ -628,7 +638,7 @@ internal class AddCustomTokenViewModel @Inject constructor( return currentCryptoCurrencies .filterIsInstance() .any { coin -> - coin.network.id.value == uiState.form.networkSelectorField.selectedItem.blockchain.id && + coin.network.id.value == uiState.value.form.networkSelectorField.selectedItem.blockchain.id && coin.network.derivationPath.value == getDerivationPath()?.rawPath } } @@ -637,39 +647,43 @@ internal class AddCustomTokenViewModel @Inject constructor( when { isNetworkSelected() && type == AddCustomTokenError.INVALID_CONTRACT_ADDRESS -> { val isAnotherTokenFieldsFilled = isAnyTokenFieldsFilled() - uiState = uiState.copySealed( - form = uiState.form.copy( - contractAddressInputField = uiState.form.contractAddressInputField.copy( - isError = true, - error = TextReference.Res( - id = R.string.custom_token_creation_error_invalid_contract_address, + uiState.update { state -> + state.copySealed( + form = state.form.copy( + contractAddressInputField = state.form.contractAddressInputField.copy( + isError = true, + error = TextReference.Res( + id = R.string.custom_token_creation_error_invalid_contract_address, + ), + ), + tokenNameInputField = state.form.tokenNameInputField.copy( + isEnabled = isAnotherTokenFieldsFilled, + ), + tokenSymbolInputField = state.form.tokenSymbolInputField.copy( + isEnabled = isAnotherTokenFieldsFilled, + ), + decimalsInputField = state.form.decimalsInputField.copy( + isEnabled = isAnotherTokenFieldsFilled, ), ), - tokenNameInputField = uiState.form.tokenNameInputField.copy( - isEnabled = isAnotherTokenFieldsFilled, - ), - tokenSymbolInputField = uiState.form.tokenSymbolInputField.copy( - isEnabled = isAnotherTokenFieldsFilled, - ), - decimalsInputField = uiState.form.decimalsInputField.copy( - isEnabled = isAnotherTokenFieldsFilled, - ), - ), - ) + ) + } } !isNetworkSelected() || type == AddCustomTokenError.FIELD_IS_EMPTY -> { - uiState = uiState.copySealed( - form = uiState.form.copy( - contractAddressInputField = uiState.form.contractAddressInputField.copy(isError = false), - tokenNameInputField = uiState.form.tokenNameInputField.copy(value = "", isEnabled = false), - tokenSymbolInputField = uiState.form.tokenSymbolInputField.copy( - value = "", - isEnabled = false, + uiState.update { state -> + state.copySealed( + form = state.form.copy( + contractAddressInputField = state.form.contractAddressInputField.copy(isError = false), + tokenNameInputField = state.form.tokenNameInputField.copy(value = "", isEnabled = false), + tokenSymbolInputField = state.form.tokenSymbolInputField.copy( + value = "", + isEnabled = false, + ), + decimalsInputField = state.form.decimalsInputField.copy(value = "", isEnabled = false), ), - decimalsInputField = uiState.form.decimalsInputField.copy(value = "", isEnabled = false), - ), - ) + ) + } } else -> Unit @@ -677,11 +691,11 @@ internal class AddCustomTokenViewModel @Inject constructor( } private fun getDerivationPath(): DerivationPath? { - return when (uiState.form.derivationPathSelectorField?.selectedItem?.type) { + return when (uiState.value.form.derivationPathSelectorField?.selectedItem?.type) { DerivationPathSelectorType.CUSTOM -> - createDerivationPathOrNull(uiState.form.derivationPathInputField?.value ?: "") + createDerivationPathOrNull(uiState.value.form.derivationPathInputField?.value ?: "") else -> - getDerivationPathForBlockchain(uiState.form.derivationPathSelectorField?.selectedItem?.blockchain) + getDerivationPathForBlockchain(uiState.value.form.derivationPathSelectorField?.selectedItem?.blockchain) } } @@ -704,7 +718,7 @@ internal class AddCustomTokenViewModel @Inject constructor( ) val derivationNetwork = if (blockchain == Blockchain.Unknown) { - uiState.form.networkSelectorField.selectedItem.blockchain + uiState.value.form.networkSelectorField.selectedItem.blockchain } else { blockchain } @@ -739,13 +753,15 @@ internal class AddCustomTokenViewModel @Inject constructor( } fun onContactAddressValueChange(enteredValue: String) { - uiState = uiState.copySealed( - form = uiState.form.copy( - contractAddressInputField = uiState.form.contractAddressInputField.copy(value = enteredValue), - ), - ) + uiState.update { state -> + state.copySealed( + form = state.form.copy( + contractAddressInputField = state.form.contractAddressInputField.copy(value = enteredValue), + ), + ) + } - val selectedNetwork = uiState.form.networkSelectorField.selectedItem.blockchain + val selectedNetwork = uiState.value.form.networkSelectorField.selectedItem.blockchain val validatorResult = ContractAddressValidator.validate( address = enteredValue, blockchain = selectedNetwork, @@ -753,14 +769,16 @@ internal class AddCustomTokenViewModel @Inject constructor( when (validatorResult) { is ContractAddressValidatorResult.Success -> { - uiState = uiState.copySealed( - form = uiState.form.copy( - contractAddressInputField = uiState.form.contractAddressInputField.copy( - isError = false, - isLoading = true, + uiState.update { state -> + state.copySealed( + form = state.form.copy( + contractAddressInputField = state.form.contractAddressInputField.copy( + isError = false, + isLoading = true, + ), ), - ), - ) + ) + } updateForm(address = enteredValue, selectedNetwork = selectedNetwork) } @@ -773,76 +791,92 @@ internal class AddCustomTokenViewModel @Inject constructor( } fun onNetworkSelectorItemClick(index: Int) { - val selectedItem = requireNotNull(uiState.form.networkSelectorField.items.getOrNull(index)) - uiState = uiState.copySealed( - form = uiState.form.copy( - networkSelectorField = uiState.form.networkSelectorField.copy( - selectedItem = selectedItem, + val state = uiState.updateAndGet { state -> + val selectedItem = requireNotNull(state.form.networkSelectorField.items.getOrNull(index)) + state.copySealed( + form = state.form.copy( + networkSelectorField = state.form.networkSelectorField.copy( + selectedItem = selectedItem, + ), + showTokenFields = selectedItem.blockchain.canHandleTokens() && + // workaround cause in Terra we support only 1 token + selectedItem.blockchain != Blockchain.TerraV1, ), - showTokenFields = selectedItem.blockchain.canHandleTokens() && - // workaround cause in Terra we support only 1 token - selectedItem.blockchain != Blockchain.TerraV1, - ), - ) - onContactAddressValueChange(uiState.form.contractAddressInputField.value) + ) + } + + onContactAddressValueChange(state.form.contractAddressInputField.value) } fun onTokenNameValueChange(enteredValue: String) { - uiState = uiState.copySealed( - form = uiState.form.copy( - tokenNameInputField = uiState.form.tokenNameInputField.copy(value = enteredValue), - ), - ) + uiState.update { state -> + state.copySealed( + form = state.form.copy( + tokenNameInputField = state.form.tokenNameInputField.copy(value = enteredValue), + ), + ) + } + updateFloatingButton() } fun onTokenSymbolValueChange(enteredValue: String) { - uiState = uiState.copySealed( - form = uiState.form.copy( - tokenSymbolInputField = uiState.form.tokenSymbolInputField.copy(value = enteredValue), - ), - ) + uiState.update { state -> + state.copySealed( + form = state.form.copy( + tokenSymbolInputField = state.form.tokenSymbolInputField.copy(value = enteredValue), + ), + ) + } updateFloatingButton() } fun onDecimalsValueChange(enteredValue: String) { - uiState = uiState.copySealed( - form = uiState.form.copy( - decimalsInputField = uiState.form.decimalsInputField.copy(value = enteredValue), - ), - ) + uiState.update { state -> + state.copySealed( + form = state.form.copy( + decimalsInputField = state.form.decimalsInputField.copy(value = enteredValue), + ), + ) + } updateFloatingButton() } fun onDerivationPathSelectorItemClick(index: Int) { - val derivationSelector = requireNotNull(uiState.form.derivationPathSelectorField) - val selected = requireNotNull(derivationSelector.items.getOrNull(index)) - val derivationInputField = requireNotNull(uiState.form.derivationPathInputField) - uiState = uiState.copySealed( - form = uiState.form.copy( - derivationPathSelectorField = derivationSelector.copy( - selectedItem = selected, + uiState.update { state -> + val derivationSelector = requireNotNull(state.form.derivationPathSelectorField) + val selected = requireNotNull(derivationSelector.items.getOrNull(index)) + val derivationInputField = requireNotNull(state.form.derivationPathInputField) + + state.copySealed( + form = state.form.copy( + derivationPathSelectorField = derivationSelector.copy( + selectedItem = selected, + ), + derivationPathInputField = derivationInputField.copy( + showField = selected.type == DerivationPathSelectorType.CUSTOM, + ), ), - derivationPathInputField = derivationInputField.copy( - showField = selected.type == DerivationPathSelectorType.CUSTOM, - ), - ), - ) + ) + } + updateFloatingButton() } fun onDerivationPathValueChange(enteredValue: String) { - uiState = uiState.copySealed( - form = uiState.form.copy( - derivationPathInputField = uiState.form.derivationPathInputField?.copy(value = enteredValue), - ), - ) + uiState.update { state -> + state.copySealed( + form = state.form.copy( + derivationPathInputField = state.form.derivationPathInputField?.copy(value = enteredValue), + ), + ) + } updateFloatingButton() } fun onAddCustomTokenClick() { if (!isNetworkSelected()) return - val blockchain = uiState.form.networkSelectorField.selectedItem.blockchain + val blockchain = uiState.value.form.networkSelectorField.selectedItem.blockchain when (getSupportBlockchainType(blockchain)) { SupportBlockchainType.SUPPORTED -> { /* no-op */ @@ -861,15 +895,16 @@ internal class AddCustomTokenViewModel @Inject constructor( CustomTokenType.TOKEN -> { val contractAddress = convertTokenAddress( blockchain = blockchain, - address = foundToken?.network?.contractAddress ?: uiState.form.contractAddressInputField.value, + address = foundToken?.network?.contractAddress ?: uiState.value.form.contractAddressInputField + .value, ) ?: error("Contract address is invalid") // impossible to add a token with invalid address CustomCurrency.CustomToken( token = Token( - name = uiState.form.tokenNameInputField.value, - symbol = uiState.form.tokenSymbolInputField.value, + name = uiState.value.form.tokenNameInputField.value, + symbol = uiState.value.form.tokenSymbolInputField.value, contractAddress = contractAddress, - decimals = requireNotNull(uiState.form.decimalsInputField.value.toIntOrNull()), + decimals = requireNotNull(uiState.value.form.decimalsInputField.value.toIntOrNull()), id = foundToken?.id, ), network = blockchain, @@ -887,21 +922,25 @@ internal class AddCustomTokenViewModel @Inject constructor( analyticsSender.sendWhenAddTokenButtonClicked(currency) viewModelScope.launch { - uiState = uiState.copySealed( - floatingButton = uiState.floatingButton.copy( - isEnabled = false, - showProgress = true, - ), - ) + uiState.update { state -> + state.copySealed( + floatingButton = state.floatingButton.copy( + isEnabled = false, + showProgress = true, + ), + ) + } val result = featureInteractor.saveToken(currency) - uiState = uiState.copySealed( - floatingButton = uiState.floatingButton.copy( - isEnabled = true, - showProgress = false, - ), - ) + uiState.update { state -> + state.copySealed( + floatingButton = state.floatingButton.copy( + isEnabled = true, + showProgress = false, + ), + ) + } result .onSuccess { featureRouter.openWalletScreen() } @@ -917,7 +956,7 @@ internal class AddCustomTokenViewModel @Inject constructor( Blockchain.Cardano -> { cardanoTokenAddressConverter.convertToFingerprint( address = address, - symbol = uiState.form.tokenSymbolInputField.value, + symbol = uiState.value.form.tokenSymbolInputField.value, ) } else -> address @@ -927,46 +966,52 @@ internal class AddCustomTokenViewModel @Inject constructor( private inner class TestActionsHandler { fun onClearAddressButtonClick() { - uiState = uiState.copySealed( - form = uiState.form.copy( - contractAddressInputField = uiState.form.contractAddressInputField.copy( - value = "", - isLoading = false, - isError = false, - error = null, - ), - ), - ) - } - - fun onResetButtonClick() { - with(uiState.form) { - uiState = uiState.copySealed( - form = uiState.form.copy( - contractAddressInputField = contractAddressInputField.copy( + uiState.update { state -> + state.copySealed( + form = state.form.copy( + contractAddressInputField = state.form.contractAddressInputField.copy( value = "", isLoading = false, isError = false, error = null, ), - networkSelectorField = networkSelectorField.copy( - selectedItem = formStateBuilder.createNetworkSelectorItem(blockchain = Blockchain.Unknown), - ), - tokenNameInputField = tokenNameInputField.copy(value = "", isEnabled = false), - tokenSymbolInputField = tokenSymbolInputField.copy(value = "", isEnabled = false), - decimalsInputField = decimalsInputField.copy(value = "", isEnabled = false), - derivationPathSelectorField = derivationPathSelectorField?.copy( - isEnabled = true, - selectedItem = formStateBuilder.createDerivationPathSelectorAdditionalItem( - blockchain = Blockchain.Unknown, - type = DerivationPathSelectorType.DEFAULT, - derivationPath = "", - ), - ), ), ) } } + + fun onResetButtonClick() { + uiState.update { state -> + with(state.form) { + state.copySealed( + form = state.form.copy( + contractAddressInputField = contractAddressInputField.copy( + value = "", + isLoading = false, + isError = false, + error = null, + ), + networkSelectorField = networkSelectorField.copy( + selectedItem = formStateBuilder.createNetworkSelectorItem( + blockchain = Blockchain.Unknown, + ), + ), + tokenNameInputField = tokenNameInputField.copy(value = "", isEnabled = false), + tokenSymbolInputField = tokenSymbolInputField.copy(value = "", isEnabled = false), + decimalsInputField = decimalsInputField.copy(value = "", isEnabled = false), + derivationPathSelectorField = derivationPathSelectorField?.copy( + isEnabled = true, + selectedItem = formStateBuilder.createDerivationPathSelectorAdditionalItem( + blockchain = Blockchain.Unknown, + type = DerivationPathSelectorType.DEFAULT, + derivationPath = "", + ), + ), + ), + ) + } + } + } } private companion object { From 6025d8ab6796a06ce089c4943a03ed0d1913cb45 Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 8 Jul 2024 14:58:16 +0100 Subject: [PATCH 7/8] Updated on 2026-08-14 --- .../tokendetails/viewmodels/TokenDetailsViewModel.kt | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/viewmodels/TokenDetailsViewModel.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/viewmodels/TokenDetailsViewModel.kt index c2ff51d2d6..4d115db24e 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/viewmodels/TokenDetailsViewModel.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/viewmodels/TokenDetailsViewModel.kt @@ -538,7 +538,13 @@ internal class TokenDetailsViewModel @Inject constructor( ifRight = { it }, ) if (extendedKey.isNotBlank()) { - router.share(extendedKey) + hapticManager.vibrateMeduim() + clipboardManager.setText(text = extendedKey) + internalUiState.value = stateFactory.getStateAndTriggerEvent( + state = internalUiState.value, + errorMessage = resourceReference(R.string.wallet_notification_address_copied), + setUiState = { internalUiState.value = it }, + ) } } } From e2c5e34762fefb14b32e872a45b9e32cd8ab7dc8 Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 8 Jul 2024 17:57:59 +0300 Subject: [PATCH 8/8] Updated on 2026-08-14 --- .../viewmodels/AddCustomTokenViewModel.kt | 20 +++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/app/src/main/java/com/tangem/tap/features/customtoken/impl/presentation/viewmodels/AddCustomTokenViewModel.kt b/app/src/main/java/com/tangem/tap/features/customtoken/impl/presentation/viewmodels/AddCustomTokenViewModel.kt index 3b9c1f3f5d..df99426334 100644 --- a/app/src/main/java/com/tangem/tap/features/customtoken/impl/presentation/viewmodels/AddCustomTokenViewModel.kt +++ b/app/src/main/java/com/tangem/tap/features/customtoken/impl/presentation/viewmodels/AddCustomTokenViewModel.kt @@ -951,16 +951,20 @@ internal class AddCustomTokenViewModel @Inject constructor( /** Convert [address] to single address for specific [blockchain] or return null if invalid */ private fun convertTokenAddress(blockchain: Blockchain, address: String): String? { - return when (blockchain) { - Blockchain.Hedera, Blockchain.HederaTestnet -> hederaAddressConverter.convertToTokenId(address) - Blockchain.Cardano -> { - cardanoTokenAddressConverter.convertToFingerprint( - address = address, - symbol = uiState.value.form.tokenSymbolInputField.value, - ) + return runCatching { + when (blockchain) { + Blockchain.Hedera, Blockchain.HederaTestnet -> hederaAddressConverter.convertToTokenId(address) + Blockchain.Cardano -> { + // TODO: [REDACTED_JIRA] + cardanoTokenAddressConverter.convertToFingerprint( + address = address, + symbol = uiState.value.form.tokenSymbolInputField.value, + ) + } + else -> address } - else -> address } + .getOrNull() } private inner class TestActionsHandler {