From 64ac275eb379037bc997999b2324e51c7de1826d Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 2 May 2023 08:00:37 +0800 Subject: [PATCH] Updated on 2026-08-14 --- .../common/analytics/events/ManageTokens.kt | 33 +++- .../impl/data/DefaultCustomTokenRepository.kt | 57 ++++++ .../data/converters/FoundTokenConverter.kt | 28 +++ .../impl/di/CustomTokenInteractorModule.kt | 41 ++++ .../impl/domain/CustomTokenInteractor.kt | 19 ++ .../impl/domain/CustomTokenRepository.kt | 14 ++ .../domain/DefaultCustomTokenInteractor.kt | 96 ++++++++++ .../impl/domain/models/FoundToken.kt | 23 +++ .../presentation/AddCustomTokenFragment.kt | 5 +- .../viewmodels/AddCustomTokenViewModel.kt | 176 ++++++++++++++++-- .../legacy/AddCustomTokenFragment.kt | 2 +- .../addCustomToken/AddCustomTokenService.kt | 63 +++---- .../addCustomToken/redux/AddCustomTokenHub.kt | 14 +- .../redux/AddCustomTokenState.kt | 13 +- 14 files changed, 518 insertions(+), 66 deletions(-) create mode 100644 app/src/main/java/com/tangem/tap/features/customtoken/impl/data/DefaultCustomTokenRepository.kt create mode 100644 app/src/main/java/com/tangem/tap/features/customtoken/impl/data/converters/FoundTokenConverter.kt create mode 100644 app/src/main/java/com/tangem/tap/features/customtoken/impl/di/CustomTokenInteractorModule.kt create mode 100644 app/src/main/java/com/tangem/tap/features/customtoken/impl/domain/CustomTokenInteractor.kt create mode 100644 app/src/main/java/com/tangem/tap/features/customtoken/impl/domain/CustomTokenRepository.kt create mode 100644 app/src/main/java/com/tangem/tap/features/customtoken/impl/domain/DefaultCustomTokenInteractor.kt create mode 100644 app/src/main/java/com/tangem/tap/features/customtoken/impl/domain/models/FoundToken.kt diff --git a/app/src/main/java/com/tangem/tap/common/analytics/events/ManageTokens.kt b/app/src/main/java/com/tangem/tap/common/analytics/events/ManageTokens.kt index 55ed5f97b9..8e0b8cc5b9 100644 --- a/app/src/main/java/com/tangem/tap/common/analytics/events/ManageTokens.kt +++ b/app/src/main/java/com/tangem/tap/common/analytics/events/ManageTokens.kt @@ -35,12 +35,40 @@ sealed class ManageTokens( params: Map = mapOf(), ) : ManageTokens(event, params) { - class ScreenOpened : ManageTokens("Custom Token Screen Opened") + object ScreenOpened : ManageTokens(event = "Custom Token Screen Opened") - class TokenWasAdded(customCurrency: CustomCurrency) : ManageTokens( + // TODO("Get rid of strong binding (CustomCurrency) + open class TokenWasAdded(customCurrency: CustomCurrency) : ManageTokens( event = "Custom Token Was Added", params = convertToParam(customCurrency), ) { + + data class Token( + val symbol: String, + val derivationPath: String?, + val blockchain: com.tangem.blockchain.common.Blockchain, + val contractAddress: String, + ) : ManageTokens( + event = "Custom Token Was Added", + params = mapOf( + "Token" to symbol, + "Derivation Path" to derivationPath, + "Network Id" to blockchain.currency, + "Contract Address" to contractAddress, + ).filterNotNull(), + ) + + data class Blockchain( + val derivationPath: String?, + val blockchain: com.tangem.blockchain.common.Blockchain, + ) : ManageTokens( + event = "Custom Token Was Added", + params = mapOf( + "Token" to blockchain.currency, + "Derivation Path" to derivationPath, + ).filterNotNull(), + ) + companion object { private fun convertToParam(customCurrency: CustomCurrency): Map = with(customCurrency) { return when (this) { @@ -48,6 +76,7 @@ sealed class ManageTokens( "Token" to network.currency, "Derivation Path" to derivationPath?.rawPath, ).filterNotNull() + is CustomCurrency.CustomToken -> mapOf( "Token" to token.symbol, "Derivation Path" to derivationPath?.rawPath, diff --git a/app/src/main/java/com/tangem/tap/features/customtoken/impl/data/DefaultCustomTokenRepository.kt b/app/src/main/java/com/tangem/tap/features/customtoken/impl/data/DefaultCustomTokenRepository.kt new file mode 100644 index 0000000000..119872604d --- /dev/null +++ b/app/src/main/java/com/tangem/tap/features/customtoken/impl/data/DefaultCustomTokenRepository.kt @@ -0,0 +1,57 @@ +package com.tangem.tap.features.customtoken.impl.data + +import com.tangem.blockchain.common.Blockchain +import com.tangem.datasource.api.tangemTech.TangemTechApi +import com.tangem.domain.common.extensions.supportedBlockchains +import com.tangem.domain.common.extensions.toNetworkId +import com.tangem.tap.features.customtoken.impl.data.converters.FoundTokenConverter +import com.tangem.tap.features.customtoken.impl.domain.CustomTokenRepository +import com.tangem.tap.features.customtoken.impl.domain.models.FoundToken +import com.tangem.tap.proxy.AppStateHolder +import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import kotlinx.coroutines.withContext + +/** + * Default implementation of custom token repository + * + * @property tangemTechApi TangemTech API + * @property dispatchers coroutine dispatchers provider + * @property reduxStateHolder redux state holder + * +[REDACTED_AUTHOR] + */ +class DefaultCustomTokenRepository( + private val tangemTechApi: TangemTechApi, + private val dispatchers: CoroutineDispatcherProvider, + private val reduxStateHolder: AppStateHolder, +) : CustomTokenRepository { + + override suspend fun findToken(address: String, networkId: String?): FoundToken { + val supportedTokenNetworkIds = requireNotNull(reduxStateHolder.scanResponse?.card) + .supportedBlockchains() + .filter(Blockchain::canHandleTokens) + .map(Blockchain::toNetworkId) + + return withContext(dispatchers.io) { + val foundCoin = tangemTechApi.getCoins( + contractAddress = address, + networkIds = networkId ?: supportedTokenNetworkIds.joinToString(separator = ","), + ) + .coins.firstNotNullOfOrNull { coin -> + val networksWithTheSameAddress = coin.networks.filter { network -> + (network.contractAddress != null || network.decimalCount != null) && + network.contractAddress?.equals(address, ignoreCase = true) == true && + supportedTokenNetworkIds.contains(network.networkId) + } + + if (networksWithTheSameAddress.isNotEmpty()) { + coin.copy(networks = networksWithTheSameAddress) + } else { + null + } + } + + foundCoin?.let(FoundTokenConverter::convert) ?: error("Token not found") + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/customtoken/impl/data/converters/FoundTokenConverter.kt b/app/src/main/java/com/tangem/tap/features/customtoken/impl/data/converters/FoundTokenConverter.kt new file mode 100644 index 0000000000..ebd6da9985 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/features/customtoken/impl/data/converters/FoundTokenConverter.kt @@ -0,0 +1,28 @@ +package com.tangem.tap.features.customtoken.impl.data.converters + +import com.tangem.datasource.api.tangemTech.models.CoinsResponse +import com.tangem.tap.features.customtoken.impl.domain.models.FoundToken +import com.tangem.utils.converter.Converter + +/** + * Converter between data model [CoinsResponse.Coin] and domain model [FoundToken] + * +[REDACTED_AUTHOR] + */ +object FoundTokenConverter : Converter { + + override fun convert(value: CoinsResponse.Coin): FoundToken { + return FoundToken( + id = value.id, + name = value.name, + symbol = value.symbol, + network = value.networks.firstOrNull()?.let { network -> + FoundToken.Network( + id = network.networkId, + address = requireNotNull(network.contractAddress), + decimalCount = requireNotNull(network.decimalCount).toString(), + ) + } ?: error("Found token networks is empty"), + ) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/customtoken/impl/di/CustomTokenInteractorModule.kt b/app/src/main/java/com/tangem/tap/features/customtoken/impl/di/CustomTokenInteractorModule.kt new file mode 100644 index 0000000000..236a990d22 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/features/customtoken/impl/di/CustomTokenInteractorModule.kt @@ -0,0 +1,41 @@ +package com.tangem.tap.features.customtoken.impl.di + +import com.tangem.datasource.api.tangemTech.TangemTechApi +import com.tangem.lib.crypto.DerivationManager +import com.tangem.tap.features.customtoken.impl.data.DefaultCustomTokenRepository +import com.tangem.tap.features.customtoken.impl.domain.CustomTokenInteractor +import com.tangem.tap.features.customtoken.impl.domain.DefaultCustomTokenInteractor +import com.tangem.tap.proxy.AppStateHolder +import com.tangem.utils.coroutines.AppCoroutineDispatcherProvider +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.android.components.ViewModelComponent +import dagger.hilt.android.scopes.ViewModelScoped + +/** +[REDACTED_AUTHOR] + */ +@Module +@InstallIn(ViewModelComponent::class) +internal object CustomTokenInteractorModule { + + @Provides + @ViewModelScoped + fun provideCustomTokenInteractor( + tangemTechApi: TangemTechApi, + appCoroutineDispatcherProvider: AppCoroutineDispatcherProvider, + reduxStateHolder: AppStateHolder, + derivationManager: DerivationManager, + ): CustomTokenInteractor { + return DefaultCustomTokenInteractor( + featureRepository = DefaultCustomTokenRepository( + tangemTechApi = tangemTechApi, + dispatchers = appCoroutineDispatcherProvider, + reduxStateHolder = reduxStateHolder, + ), + derivationManager = derivationManager, + reduxStateHolder = reduxStateHolder, + ) + } +} \ No newline at end of file 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 new file mode 100644 index 0000000000..1b5b0fa9cf --- /dev/null +++ b/app/src/main/java/com/tangem/tap/features/customtoken/impl/domain/CustomTokenInteractor.kt @@ -0,0 +1,19 @@ +package com.tangem.tap.features.customtoken.impl.domain + +import com.tangem.blockchain.common.Blockchain +import com.tangem.tap.features.customtoken.impl.domain.models.FoundToken +import com.tangem.tap.features.wallet.models.Currency + +/** + * Custom token interactor + * +[REDACTED_AUTHOR] + */ +interface CustomTokenInteractor { + + /** Find token by [address] and [blockchain] */ + suspend fun findToken(address: String, blockchain: Blockchain): FoundToken + + /** Save token [currency] with contact address [address] */ + suspend fun saveToken(currency: Currency, address: String) +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/customtoken/impl/domain/CustomTokenRepository.kt b/app/src/main/java/com/tangem/tap/features/customtoken/impl/domain/CustomTokenRepository.kt new file mode 100644 index 0000000000..13b5d1c043 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/features/customtoken/impl/domain/CustomTokenRepository.kt @@ -0,0 +1,14 @@ +package com.tangem.tap.features.customtoken.impl.domain + +import com.tangem.tap.features.customtoken.impl.domain.models.FoundToken + +/** + * Custom token repository + * +[REDACTED_AUTHOR] + */ +interface CustomTokenRepository { + + /** Find token by [address] and [networkId] */ + suspend fun findToken(address: String, networkId: String?): FoundToken +} \ 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 new file mode 100644 index 0000000000..6ecbab2382 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/features/customtoken/impl/domain/DefaultCustomTokenInteractor.kt @@ -0,0 +1,96 @@ +package com.tangem.tap.features.customtoken.impl.domain + +import com.tangem.blockchain.common.Blockchain +import com.tangem.common.extensions.guard +import com.tangem.common.flatMap +import com.tangem.domain.common.extensions.toNetworkId +import com.tangem.domain.models.scan.ScanResponse +import com.tangem.lib.crypto.DerivationManager +import com.tangem.lib.crypto.models.Currency.NativeToken +import com.tangem.lib.crypto.models.Currency.NonNativeToken +import com.tangem.tap.features.customtoken.impl.domain.models.FoundToken +import com.tangem.tap.features.wallet.models.Currency +import com.tangem.tap.proxy.AppStateHolder +import com.tangem.tap.scope +import com.tangem.tap.userWalletsListManager +import com.tangem.tap.walletCurrenciesManager +import kotlinx.coroutines.launch +import timber.log.Timber + +/** + * Default implementation of custom token interactor + * + * @property featureRepository feature repository + * @property derivationManager derivation manager + * @property reduxStateHolder redux state holder + * +[REDACTED_AUTHOR] + */ +class DefaultCustomTokenInteractor( + private val featureRepository: CustomTokenRepository, + private val derivationManager: DerivationManager, + private val reduxStateHolder: AppStateHolder, +) : CustomTokenInteractor { + + override suspend fun findToken(address: String, blockchain: Blockchain): FoundToken { + return featureRepository.findToken( + address = address, + networkId = if (blockchain != Blockchain.Unknown) blockchain.toNetworkId() else null, + ) + } + + override suspend fun saveToken(currency: Currency, address: String) { + val hasDerivation = derivationManager.hasDerivation( + networkId = currency.blockchain.toNetworkId(), + derivationPath = requireNotNull(currency.derivationPath), + ) + + if (!hasDerivation) { + derivationManager.deriveMissingBlockchains( + when (currency) { + is Currency.Blockchain -> NativeToken( + id = requireNotNull(currency.coinId), + name = currency.currencyName, + symbol = currency.currencySymbol, + networkId = currency.blockchain.toNetworkId(), + ) + + is Currency.Token -> NonNativeToken( + id = requireNotNull(currency.coinId), + name = currency.currencyName, + symbol = currency.currencySymbol, + networkId = currency.blockchain.toNetworkId(), + contractAddress = address, + decimalCount = currency.decimals, + ) + }, + ) + + submitAdd( + scanResponse = requireNotNull(reduxStateHolder.scanResponse), + currency = currency, + ) + } + } + + private fun submitAdd(scanResponse: ScanResponse, currency: Currency) { + val selectedUserWallet = userWalletsListManager.selectedUserWalletSync.guard { + Timber.e("Unable to add currencies, no user wallet selected") + return + } + scope.launch { + userWalletsListManager.update( + userWalletId = selectedUserWallet.walletId, + update = { userWallet -> + userWallet.copy(scanResponse = scanResponse) + }, + ) + .flatMap { updatedUserWallet -> + walletCurrenciesManager.addCurrencies( + userWallet = updatedUserWallet, + currenciesToAdd = listOf(currency), + ) + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/customtoken/impl/domain/models/FoundToken.kt b/app/src/main/java/com/tangem/tap/features/customtoken/impl/domain/models/FoundToken.kt new file mode 100644 index 0000000000..25c3cb5836 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/features/customtoken/impl/domain/models/FoundToken.kt @@ -0,0 +1,23 @@ +package com.tangem.tap.features.customtoken.impl.domain.models + +/** + * Found token model + * + * @property id id + * @property name name + * @property symbol symbol + * @property network network + * +[REDACTED_AUTHOR] + */ +data class FoundToken(val id: String, val name: String, val symbol: String, val network: Network) { + + /** + * Found token network + * + * @property id id + * @property address address + * @property decimalCount decimal count + */ + data class Network(val id: String, val address: String, val decimalCount: String) +} \ No newline at end of file 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 8967312f0f..ee63fc97e0 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 @@ -5,6 +5,7 @@ import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import androidx.compose.ui.platform.ComposeView +import androidx.compose.ui.platform.LocalLifecycleOwner import androidx.fragment.app.Fragment import androidx.hilt.navigation.compose.hiltViewModel import androidx.transition.TransitionInflater @@ -32,7 +33,9 @@ internal class AddCustomTokenFragment : Fragment() { setContent { isTransitionGroup = true - val viewModel = hiltViewModel() + val viewModel = hiltViewModel().apply { + LocalLifecycleOwner.current.lifecycle.addObserver(this) + } TangemTheme { AddCustomTokenScreen(stateHolder = viewModel.uiState) 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 b3ebc34569..22b13b1216 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 @@ -3,13 +3,23 @@ package com.tangem.tap.features.customtoken.impl.presentation.viewmodels import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue +import androidx.lifecycle.DefaultLifecycleObserver +import androidx.lifecycle.LifecycleOwner import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope import com.tangem.blockchain.common.Blockchain import com.tangem.blockchain.common.DerivationStyle +import com.tangem.blockchain.common.Token +import com.tangem.core.analytics.api.AnalyticsEventHandler +import com.tangem.crypto.hdWallet.DerivationPath import com.tangem.domain.AddCustomTokenError +import com.tangem.domain.common.TapWorkarounds.derivationStyle import com.tangem.domain.common.TapWorkarounds.isTestCard +import com.tangem.domain.common.extensions.fromNetworkId import com.tangem.domain.common.extensions.isSupportedInApp import com.tangem.domain.common.extensions.supportedBlockchains +import com.tangem.tap.common.analytics.events.ManageTokens +import com.tangem.tap.features.customtoken.impl.domain.CustomTokenInteractor import com.tangem.tap.features.customtoken.impl.presentation.models.AddCustomTokenChooseTokenBottomSheet import com.tangem.tap.features.customtoken.impl.presentation.models.AddCustomTokenChooseTokenBottomSheet.TestTokenItem import com.tangem.tap.features.customtoken.impl.presentation.models.AddCustomTokenChooseTokenBottomSheet.TokensCategoryBlock @@ -24,26 +34,37 @@ import com.tangem.tap.features.customtoken.impl.presentation.states.AddCustomTok import com.tangem.tap.features.customtoken.impl.presentation.validators.ContactAddressValidator import com.tangem.tap.features.customtoken.impl.presentation.validators.ContractAddressValidatorResult import com.tangem.tap.features.details.ui.cardsettings.TextReference +import com.tangem.tap.features.wallet.models.Currency import com.tangem.tap.proxy.AppStateHolder +import com.tangem.utils.coroutines.AppCoroutineDispatcherProvider +import com.tangem.utils.coroutines.runCatching import com.tangem.wallet.BuildConfig import com.tangem.wallet.R import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.collections.immutable.persistentListOf +import kotlinx.coroutines.launch +import timber.log.Timber import javax.inject.Inject /** * ViewModel for add custom token screen * - * @param featureRouter feature router - * @property reduxStateHolder redux state holder + * @param featureRouter feature router + * @property featureInteractor feature interactor + * @property dispatchers coroutine dispatchers provider + * @property reduxStateHolder redux state holder + * @property analyticsEventHandler analytics event handler * [REDACTED_AUTHOR] */ @HiltViewModel internal class AddCustomTokenViewModel @Inject constructor( featureRouter: CustomTokenRouter, + private val featureInteractor: CustomTokenInteractor, + private val dispatchers: AppCoroutineDispatcherProvider, private val reduxStateHolder: AppStateHolder, -) : ViewModel() { + private val analyticsEventHandler: AnalyticsEventHandler, +) : ViewModel(), DefaultLifecycleObserver { private val actionsHandler = ActionsHandler(featureRouter) private val testActionsHandler = TestActionsHandler() @@ -52,6 +73,12 @@ internal class AddCustomTokenViewModel @Inject constructor( var uiState by mutableStateOf(getInitialUiState()) private set + private var foundTokenId: String? = null + + override fun onCreate(owner: LifecycleOwner) { + analyticsEventHandler.send(ManageTokens.CustomToken.ScreenOpened) + } + private fun getInitialUiState(): AddCustomTokenStateHolder { return if (BuildConfig.TEST_ACTION_ENABLED) { AddCustomTokenStateHolder.TestContent( @@ -72,7 +99,7 @@ internal class AddCustomTokenViewModel @Inject constructor( TokensCategoryBlock(name = "Common", items = COMMON_TOKENS), TokensCategoryBlock(name = "Solana", items = SOLANA_TOKENS), ), - onTestTokenClick = testActionsHandler::onTestTokenClick, + onTestTokenClick = actionsHandler::onContactAddressValueChange, ), ) } else { @@ -262,24 +289,66 @@ internal class AddCustomTokenViewModel @Inject constructor( } fun onAddCustomTokenClick() { - // TODO("[REDACTED_TASK_KEY] Add processing") + if (uiState.form.networkSelectorField.selectedItem.blockchain != Blockchain.Unknown) { + val selectedNetwork = uiState.form.networkSelectorField.selectedItem.blockchain + + val currency = if (isAnyTokenFieldsFilled() || isAllTokenFieldsFilled()) { + Currency.Token( + token = Token( + name = uiState.form.tokenNameInputField.value, + symbol = uiState.form.tokenSymbolInputField.value, + contractAddress = uiState.form.contractAddressInputField.value, + decimals = uiState.form.decimalsInputField.value.toInt(), + id = foundTokenId, + ), + blockchain = selectedNetwork, + derivationPath = getDerivationPath( + mainNetwork = selectedNetwork, + derivationNetwork = uiState.form.derivationPathSelectorField?.selectedItem?.blockchain, + derivationStyle = reduxStateHolder.scanResponse?.card?.derivationStyle, + )?.rawPath, + ) + } else { + Currency.Blockchain( + blockchain = selectedNetwork, + derivationPath = getDerivationPath( + mainNetwork = selectedNetwork, + derivationNetwork = uiState.form.derivationPathSelectorField?.selectedItem?.blockchain, + derivationStyle = reduxStateHolder.scanResponse?.card?.derivationStyle, + )?.rawPath, + ) + } + + sendOnAddTokenButtonClick(currency = currency, address = uiState.form.contractAddressInputField.value) + + viewModelScope.launch(dispatchers.io) { + featureInteractor.saveToken( + currency = currency, + address = uiState.form.contractAddressInputField.value, + ) + } + } } fun onContactAddressValueChange(enteredValue: String) { with(uiState.form) { + val selectedNetwork = networkSelectorField.selectedItem.blockchain val isValid = ContactAddressValidator.validate( address = enteredValue, - blockchain = networkSelectorField.selectedItem.blockchain, + blockchain = selectedNetwork, ) when (isValid) { is ContractAddressValidatorResult.Success -> { uiState = uiState.copySealed( form = uiState.form.copy( - contractAddressInputField = contractAddressInputField.copy(isError = false), + contractAddressInputField = contractAddressInputField.copy( + isError = false, + isLoading = true, + ), ), ) - // TODO("[REDACTED_TASK_KEY] Add loading custom token information") + updateForm(address = enteredValue, selectedNetwork = selectedNetwork) } is ContractAddressValidatorResult.Error -> { @@ -330,17 +399,99 @@ internal class AddCustomTokenViewModel @Inject constructor( // TODO("[REDACTED_TASK_KEY] Update floating button") } - private fun isAnotherTokenFieldsFilled(): Boolean { + private fun getDerivationPath( + mainNetwork: Blockchain, + derivationNetwork: Blockchain?, + derivationStyle: DerivationStyle?, + ): DerivationPath? { + val network = if (derivationNetwork == Blockchain.Unknown) mainNetwork else derivationNetwork + + return network?.derivationPath( + style = if (derivationNetwork == Blockchain.Unknown) derivationStyle else DerivationStyle.LEGACY, + ) + } + + private fun sendOnAddTokenButtonClick(currency: Currency, address: String) { + when (currency) { + is Currency.Blockchain -> { + analyticsEventHandler.send( + ManageTokens.CustomToken.TokenWasAdded.Blockchain( + derivationPath = currency.derivationPath, + blockchain = currency.blockchain, + ), + ) + } + + is Currency.Token -> { + analyticsEventHandler.send( + ManageTokens.CustomToken.TokenWasAdded.Token( + symbol = currency.currencySymbol, + derivationPath = currency.derivationPath, + blockchain = currency.blockchain, + contractAddress = address, + ), + ) + } + } + } + + private fun isAnyTokenFieldsFilled(): Boolean { return with(uiState.form) { contractAddressInputField.value.isNotEmpty() || tokenNameInputField.value.isNotEmpty() || tokenSymbolInputField.value.isNotEmpty() || decimalsInputField.value.isNotEmpty() } } + private fun isAllTokenFieldsFilled(): Boolean { + return with(uiState.form) { + contractAddressInputField.value.isNotEmpty() && tokenNameInputField.value.isNotEmpty() && + tokenSymbolInputField.value.isNotEmpty() && decimalsInputField.value.isNotEmpty() + } + } + + private fun updateForm(address: String, selectedNetwork: Blockchain) { + viewModelScope.launch(dispatchers.main) { + runCatching(dispatchers.io) { + featureInteractor.findToken(address = address, blockchain = selectedNetwork) + } + .onSuccess { token -> + with(uiState.form) { + uiState = uiState.copySealed( + form = copy( + contractAddressInputField = contractAddressInputField.copy(isLoading = false), + networkSelectorField = networkSelectorField.copy( + selectedItem = createNetworkSelectorItem( + blockchain = Blockchain.fromNetworkId(token.network.id) + ?: Blockchain.Unknown, + ), + ), + tokenNameInputField = tokenNameInputField.copy( + value = token.name, + isEnabled = false, + ), + tokenSymbolInputField = tokenSymbolInputField.copy( + value = token.symbol, + isEnabled = false, + ), + decimalsInputField = decimalsInputField.copy( + value = token.network.decimalCount, + isEnabled = false, + ), + ), + ) + } + } + .onFailure { + foundTokenId = null + Timber.e(it) + } + } + } + private fun handleContractAddressErrorValidation(type: AddCustomTokenError) { with(uiState.form) { val isNetworkSelectorFilled = networkSelectorField.selectedItem.blockchain != Blockchain.Unknown - val isAnotherTokenFieldsFilled = isAnotherTokenFieldsFilled() + val isAnotherTokenFieldsFilled = isAnyTokenFieldsFilled() when { isNetworkSelectorFilled && type == AddCustomTokenError.InvalidContractAddress -> { @@ -432,11 +583,6 @@ internal class AddCustomTokenViewModel @Inject constructor( ) } } - - @Suppress("UnusedPrivateMember") - fun onTestTokenClick(address: String) { - // TODO("[REDACTED_TASK_KEY] Add processing") - } } private companion object { diff --git a/app/src/main/java/com/tangem/tap/features/customtoken/legacy/AddCustomTokenFragment.kt b/app/src/main/java/com/tangem/tap/features/customtoken/legacy/AddCustomTokenFragment.kt index 21567f588c..280a46e758 100644 --- a/app/src/main/java/com/tangem/tap/features/customtoken/legacy/AddCustomTokenFragment.kt +++ b/app/src/main/java/com/tangem/tap/features/customtoken/legacy/AddCustomTokenFragment.kt @@ -32,7 +32,7 @@ class AddCustomTokenFragment : BaseStoreFragment(R.layout.view_compose_fragment) override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) - Analytics.send(ManageTokens.CustomToken.ScreenOpened()) + Analytics.send(ManageTokens.CustomToken.ScreenOpened) } override fun subscribeToStore() { diff --git a/domain/legacy/src/main/java/com/tangem/domain/features/addCustomToken/AddCustomTokenService.kt b/domain/legacy/src/main/java/com/tangem/domain/features/addCustomToken/AddCustomTokenService.kt index 2296da1914..1a5fc65e8c 100644 --- a/domain/legacy/src/main/java/com/tangem/domain/features/addCustomToken/AddCustomTokenService.kt +++ b/domain/legacy/src/main/java/com/tangem/domain/features/addCustomToken/AddCustomTokenService.kt @@ -14,41 +14,36 @@ class AddCustomTokenService( private val supportedTokenNetworkIds: List, ) { - suspend fun findToken( - contractAddress: String, - networkId: String? = null, - active: Boolean? = null, - ): List = withContext(dispatchers.io) { - runCatching { - tangemTechApi.getCoins( - contractAddress = contractAddress, - networkIds = selectNetworksForSearch(networkId), - active = active, - ) + suspend fun findToken(contractAddress: String, networkId: String?): List { + return withContext(dispatchers.io) { + runCatching { + tangemTechApi.getCoins( + contractAddress = contractAddress, + networkIds = selectNetworksForSearch(networkId), + ) + } + .fold( + onSuccess = { response -> + var coinsList = mutableListOf() + response.coins.forEach { coin -> + val networksWithTheSameAddress = coin.networks + .filter { it.contractAddress != null || it.decimalCount != null } + .filter { it.contractAddress?.equals(contractAddress, ignoreCase = true) == true } + .filter { supportedTokenNetworkIds.contains(it.networkId) } + if (networksWithTheSameAddress.isNotEmpty()) { + val newToken = coin.copy(networks = networksWithTheSameAddress) + coinsList.add(newToken) + } + } + if (coinsList.size > 1) { + // https://tangem.slack.com/archives/GMXC6PP71/p1649672562078679 + coinsList = mutableListOf(coinsList[0]) + } + coinsList + }, + onFailure = { emptyList() }, + ) } - .onSuccess { response -> - var coinsList = mutableListOf() - response.coins.forEach { coin -> - val networksWithTheSameAddress = coin.networks - .filter { it.contractAddress != null || it.decimalCount != null } - .filter { it.contractAddress?.equals(contractAddress, ignoreCase = true) == true } - .filter { supportedTokenNetworkIds.contains(it.networkId) } - if (networksWithTheSameAddress.isNotEmpty()) { - val newToken = coin.copy(networks = networksWithTheSameAddress) - coinsList.add(newToken) - } - } - if (coinsList.size > 1) { - // https://tangem.slack.com/archives/GMXC6PP71/p1649672562078679 - coinsList = mutableListOf(coinsList[0]) - } - return@withContext coinsList - } - .onFailure { - return@withContext emptyList() - } - - error("Unreachable code because runCatching must return result") } private fun selectNetworksForSearch(networkId: String?): String { diff --git a/domain/legacy/src/main/java/com/tangem/domain/features/addCustomToken/redux/AddCustomTokenHub.kt b/domain/legacy/src/main/java/com/tangem/domain/features/addCustomToken/redux/AddCustomTokenHub.kt index 72eeb341bc..17f110b952 100644 --- a/domain/legacy/src/main/java/com/tangem/domain/features/addCustomToken/redux/AddCustomTokenHub.kt +++ b/domain/legacy/src/main/java/com/tangem/domain/features/addCustomToken/redux/AddCustomTokenHub.kt @@ -342,7 +342,6 @@ internal class AddCustomTokenHub : BaseStoreHub("AddCustomT } } - @Suppress("MagicNumber") private suspend fun requestInfoAboutToken(contractAddress: String): List { val tangemTechServiceManager = requireNotNull(hubState.tangemTechServiceManager) dispatchOnMain(Screen.UpdateTokenFields(listOf(ContractAddress to ViewStates.TokenField(isLoading = true)))) @@ -354,7 +353,7 @@ internal class AddCustomTokenHub : BaseStoreHub("AddCustomT // simulate loading effect. It would be better if the delay would only run if tokenManager.checkAddress() // got the result faster than 500ms and the delay would only be the difference between them. - delay(500) + delay(timeMillis = 500) val result = tangemTechServiceManager.findToken(contractAddress, selectedNetworkId) @@ -450,7 +449,7 @@ internal class AddCustomTokenHub : BaseStoreHub("AddCustomT enableDisableTokenDetailFields(false) } - private suspend fun enableDisableTokenDetailFields(isEnabled: Boolean = true) { + private suspend fun enableDisableTokenDetailFields(isEnabled: Boolean) { val state = hubState val action = Screen.UpdateTokenFields( listOf( @@ -590,17 +589,18 @@ private class AddCustomTokenReducer( is OnCreate -> { val card = requireNotNull(globalState.scanResponse?.card) val supportedTokenNetworkIds = card.supportedBlockchains() - .filter { it.canHandleTokens() } - .map { it.toNetworkId() } + .filter(Blockchain::canHandleTokens) + .map(Blockchain::toNetworkId) + val tangemTechServiceManager = AddCustomTokenService( tangemTechApi = globalState.networkServices.tangemTechService.api, dispatchers = AppCoroutineDispatcherProvider(), supportedTokenNetworkIds = supportedTokenNetworkIds, ) - val form = Form(AddCustomTokenState.createFormFields(card, CustomTokenType.Blockchain)) + state.copy( cardDerivationStyle = card.derivationStyle, - form = form, + form = Form(AddCustomTokenState.createFormFields(card, CustomTokenType.Blockchain)), tangemTechServiceManager = tangemTechServiceManager, screenState = createInitialScreenState(card.settings.isHDWalletAllowed), ) diff --git a/domain/legacy/src/main/java/com/tangem/domain/features/addCustomToken/redux/AddCustomTokenState.kt b/domain/legacy/src/main/java/com/tangem/domain/features/addCustomToken/redux/AddCustomTokenState.kt index 545339f567..3377646d25 100644 --- a/domain/legacy/src/main/java/com/tangem/domain/features/addCustomToken/redux/AddCustomTokenState.kt +++ b/domain/legacy/src/main/java/com/tangem/domain/features/addCustomToken/redux/AddCustomTokenState.kt @@ -64,8 +64,6 @@ data class AddCustomTokenState( fun getError(id: FieldId): AddCustomTokenError? = formErrors[id] - fun hasError(id: FieldId): Boolean = formErrors[id] != null - inline fun visitDataConverter(converter: FieldDataConverter): T { form.visitDataConverter(converter) return converter.getConvertedData() @@ -116,9 +114,12 @@ data class AddCustomTokenState( return network.data.value != Blockchain.Unknown } - fun getCustomTokenType(): CustomTokenType = when { - tokensAnyFieldsIsFilled() || tokensFieldsIsFilled() -> CustomTokenType.Token - else -> CustomTokenType.Blockchain + fun getCustomTokenType(): CustomTokenType { + return if (tokensAnyFieldsIsFilled() || tokensFieldsIsFilled()) { + CustomTokenType.Token + } else { + CustomTokenType.Blockchain + } } fun gatherUserToken(): CustomCurrency.CustomToken? = try { @@ -264,7 +265,7 @@ data class AddCustomTokenState( private var builder: StringBuilder = StringBuilder() override fun convert(action: Action, stateHolder: DomainState): String? { - val action = action as? AddCustomTokenAction ?: return null + if (action !is AddCustomTokenAction) return null val state = stateHolder.addCustomTokensState val fieldConverter =