Updated on 2026-08-14
This commit is contained in:
parent
de449519a3
commit
64ac275eb3
14 changed files with 518 additions and 66 deletions
|
|
@ -35,12 +35,40 @@ sealed class ManageTokens(
|
|||
params: Map<String, String> = 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<String, String> = 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,
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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<CoinsResponse.Coin, FoundToken> {
|
||||
|
||||
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"),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
|
|
@ -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<AddCustomTokenViewModel>()
|
||||
val viewModel = hiltViewModel<AddCustomTokenViewModel>().apply {
|
||||
LocalLifecycleOwner.current.lifecycle.addObserver(this)
|
||||
}
|
||||
|
||||
TangemTheme {
|
||||
AddCustomTokenScreen(stateHolder = viewModel.uiState)
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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() {
|
||||
|
|
|
|||
|
|
@ -14,41 +14,36 @@ class AddCustomTokenService(
|
|||
private val supportedTokenNetworkIds: List<String>,
|
||||
) {
|
||||
|
||||
suspend fun findToken(
|
||||
contractAddress: String,
|
||||
networkId: String? = null,
|
||||
active: Boolean? = null,
|
||||
): List<CoinsResponse.Coin> = withContext(dispatchers.io) {
|
||||
runCatching {
|
||||
tangemTechApi.getCoins(
|
||||
contractAddress = contractAddress,
|
||||
networkIds = selectNetworksForSearch(networkId),
|
||||
active = active,
|
||||
)
|
||||
suspend fun findToken(contractAddress: String, networkId: String?): List<CoinsResponse.Coin> {
|
||||
return withContext(dispatchers.io) {
|
||||
runCatching {
|
||||
tangemTechApi.getCoins(
|
||||
contractAddress = contractAddress,
|
||||
networkIds = selectNetworksForSearch(networkId),
|
||||
)
|
||||
}
|
||||
.fold(
|
||||
onSuccess = { response ->
|
||||
var coinsList = mutableListOf<CoinsResponse.Coin>()
|
||||
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<CoinsResponse.Coin>()
|
||||
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 {
|
||||
|
|
|
|||
|
|
@ -342,7 +342,6 @@ internal class AddCustomTokenHub : BaseStoreHub<AddCustomTokenState>("AddCustomT
|
|||
}
|
||||
}
|
||||
|
||||
@Suppress("MagicNumber")
|
||||
private suspend fun requestInfoAboutToken(contractAddress: String): List<CoinsResponse.Coin> {
|
||||
val tangemTechServiceManager = requireNotNull(hubState.tangemTechServiceManager)
|
||||
dispatchOnMain(Screen.UpdateTokenFields(listOf(ContractAddress to ViewStates.TokenField(isLoading = true))))
|
||||
|
|
@ -354,7 +353,7 @@ internal class AddCustomTokenHub : BaseStoreHub<AddCustomTokenState>("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<AddCustomTokenState>("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),
|
||||
)
|
||||
|
|
|
|||
|
|
@ -64,8 +64,6 @@ data class AddCustomTokenState(
|
|||
|
||||
fun getError(id: FieldId): AddCustomTokenError? = formErrors[id]
|
||||
|
||||
fun hasError(id: FieldId): Boolean = formErrors[id] != null
|
||||
|
||||
inline fun <reified T> visitDataConverter(converter: FieldDataConverter<T>): 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 =
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue