Updated on 2026-08-14
This commit is contained in:
parent
1771ce7847
commit
3bd0392018
25 changed files with 371 additions and 45 deletions
|
|
@ -10,7 +10,7 @@ import com.tangem.domain.AddCustomTokenError
|
|||
*
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
object ContactAddressValidator {
|
||||
object ContractAddressValidator {
|
||||
|
||||
/** Validate a [address] using [blockchain] */
|
||||
fun validate(address: String, blockchain: Blockchain): ContractAddressValidatorResult {
|
||||
|
|
@ -33,7 +33,7 @@ import com.tangem.tap.features.customtoken.impl.presentation.models.AddCustomTok
|
|||
import com.tangem.tap.features.customtoken.impl.presentation.models.AddCustomTokenSelectorField.SelectorItem
|
||||
import com.tangem.tap.features.customtoken.impl.presentation.routers.CustomTokenRouter
|
||||
import com.tangem.tap.features.customtoken.impl.presentation.states.AddCustomTokenStateHolder
|
||||
import com.tangem.tap.features.customtoken.impl.presentation.validators.ContactAddressValidator
|
||||
import com.tangem.tap.features.customtoken.impl.presentation.validators.ContractAddressValidator
|
||||
import com.tangem.tap.features.customtoken.impl.presentation.validators.ContractAddressValidatorResult
|
||||
import com.tangem.tap.features.details.ui.cardsettings.TextReference
|
||||
import com.tangem.utils.coroutines.AppCoroutineDispatcherProvider
|
||||
|
|
@ -461,7 +461,7 @@ internal class AddCustomTokenViewModel @Inject constructor(
|
|||
private fun getTokenWarningSet(): Set<AddCustomTokenWarning> {
|
||||
val networkSelectorValue = uiState.form.networkSelectorField.selectedItem.blockchain
|
||||
|
||||
val isContractAddressFieldEmpty = ContactAddressValidator.validate(
|
||||
val isContractAddressFieldEmpty = ContractAddressValidator.validate(
|
||||
address = uiState.form.contractAddressInputField.value,
|
||||
blockchain = networkSelectorValue,
|
||||
).let {
|
||||
|
|
@ -513,7 +513,7 @@ internal class AddCustomTokenViewModel @Inject constructor(
|
|||
val state = when {
|
||||
isAllTokenFieldsFilled() && isNetworkSelected() -> {
|
||||
val networkSelectorValue = uiState.form.networkSelectorField.selectedItem.blockchain
|
||||
val error = ContactAddressValidator.validate(
|
||||
val error = ContractAddressValidator.validate(
|
||||
address = uiState.form.contractAddressInputField.value,
|
||||
blockchain = networkSelectorValue,
|
||||
)
|
||||
|
|
@ -727,7 +727,7 @@ internal class AddCustomTokenViewModel @Inject constructor(
|
|||
)
|
||||
|
||||
val selectedNetwork = uiState.form.networkSelectorField.selectedItem.blockchain
|
||||
val validatorResult = ContactAddressValidator.validate(
|
||||
val validatorResult = ContractAddressValidator.validate(
|
||||
address = enteredValue,
|
||||
blockchain = selectedNetwork,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -285,7 +285,6 @@
|
|||
<string name="manage_tokens_network_selector_non_native_info">Using non-native networks for tokens enables cross-blockchain interoperability, allowing assets to be utilized in diverse decentralized applications and smart contracts across platforms. However, this often involves a custodian or smart contract to hold the original asset securely, introducing centralization and counterparty risk.</string>
|
||||
<string name="manage_tokens_network_selector_non_native_subtitle">Not original or primary blockchain the token is hosted</string>
|
||||
<string name="manage_tokens_network_selector_non_native_title">Non-native networks</string>
|
||||
<string name="manage_tokens_network_selector_other_subtitle">Blockchain the cryptocurrency was initially created</string>
|
||||
<string name="manage_tokens_network_selector_other_title">Networks</string>
|
||||
<string name="manage_tokens_network_selector_title">Choose networks</string>
|
||||
<string name="manage_tokens_network_selector_wallet">Wallet</string>
|
||||
|
|
|
|||
|
|
@ -13,7 +13,11 @@ class ValidateContractAddressUseCase(private val tokensListRepository: TokensLis
|
|||
block = {
|
||||
if (address.isEmpty()) raise(AddCustomTokenError.FieldIsEmpty)
|
||||
|
||||
if (!tokensListRepository.validateAddress(networkId, address)) {
|
||||
if (!tokensListRepository.validateAddress(
|
||||
contractAddress = address,
|
||||
networkId = networkId,
|
||||
)
|
||||
) {
|
||||
raise(AddCustomTokenError.InvalidContractAddress)
|
||||
}
|
||||
},
|
||||
|
|
|
|||
|
|
@ -41,6 +41,8 @@ dependencies {
|
|||
kapt(deps.hilt.kapt)
|
||||
|
||||
/** Core modules */
|
||||
implementation(projects.core.analytics)
|
||||
implementation(projects.core.analytics.models)
|
||||
implementation(projects.core.featuretoggles)
|
||||
implementation(projects.core.navigation)
|
||||
implementation(projects.core.ui)
|
||||
|
|
|
|||
|
|
@ -0,0 +1,102 @@
|
|||
package com.tangem.managetokens.presentation.common.analytics
|
||||
|
||||
import com.tangem.core.analytics.models.AnalyticsEvent
|
||||
import com.tangem.core.analytics.models.AnalyticsParam
|
||||
|
||||
sealed class ManageTokens(
|
||||
event: String,
|
||||
params: Map<String, String> = emptyMap(),
|
||||
) : AnalyticsEvent("Manage Tokens", event, params) {
|
||||
|
||||
class ScreenOpened : ManageTokens("Manage Tokens Screen Opened")
|
||||
|
||||
class TokenIsNotFound(userInput: String) : ManageTokens(
|
||||
event = "Token Is Not Found",
|
||||
params = mapOf("Input" to userInput),
|
||||
)
|
||||
|
||||
class TokenSwitcherChanged(
|
||||
token: String,
|
||||
state: AnalyticsParam.OnOffState,
|
||||
) : ManageTokens(
|
||||
event = "Token Switcher Changed",
|
||||
params = mapOf(
|
||||
"Token" to token,
|
||||
"State" to state.value,
|
||||
),
|
||||
)
|
||||
|
||||
class ButtonAdd(token: String) : ManageTokens(
|
||||
event = "Button - Add",
|
||||
params = mapOf("Token" to token),
|
||||
)
|
||||
|
||||
class ButtonEdit(token: String) : ManageTokens(
|
||||
event = "Button - Edit",
|
||||
params = mapOf("Token" to token),
|
||||
)
|
||||
|
||||
object ButtonChooseWallet : ManageTokens(event = "Button - Choose Wallet")
|
||||
|
||||
class WalletSelected(source: Source) : ManageTokens(
|
||||
event = "Wallet Selected",
|
||||
params = mapOf("Source" to source.name),
|
||||
) {
|
||||
|
||||
enum class Source(name: String) {
|
||||
MainToken("Main Token"),
|
||||
CustomToken("Custom Token"),
|
||||
}
|
||||
}
|
||||
|
||||
object NoticeNonNativeNetworkClicked : ManageTokens(event = "Notice - Non Native Network Clicked")
|
||||
|
||||
class ButtonGenerateAddresses(cardCount: Int) : ManageTokens(
|
||||
event = "Button - Get Addresses",
|
||||
params = mapOf("CardCount" to cardCount.toString()),
|
||||
)
|
||||
|
||||
object ButtonCustomToken : ManageTokens("Button - Custom Token")
|
||||
|
||||
class CustomTokenWasAdded(
|
||||
val derivation: String,
|
||||
val networkId: String,
|
||||
val token: String? = null,
|
||||
val contractAddress: String? = null,
|
||||
) : ManageTokens(
|
||||
event = "Custom Token Was Added",
|
||||
params = mutableMapOf(
|
||||
"Derivation" to derivation,
|
||||
"Network Id" to networkId,
|
||||
).apply {
|
||||
token?.let { put("Token", it) }
|
||||
contractAddress?.let { put("Contract Address", it) }
|
||||
},
|
||||
)
|
||||
|
||||
class CustomTokenNetworkSelected(blockchain: String) : ManageTokens(
|
||||
event = "Custom Token Network Selected",
|
||||
params = mapOf("blockchain" to blockchain),
|
||||
)
|
||||
|
||||
class CustomTokenDerivationSelected(derivation: String) : ManageTokens(
|
||||
event = "Custom Token Derivation Selected",
|
||||
params = mapOf("Derivation" to derivation),
|
||||
)
|
||||
|
||||
class CustomTokenAddress(validated: Boolean) : ManageTokens(
|
||||
"Custom Token Address",
|
||||
params = mapOf("Validation" to if (validated) "Ok" else "Error"),
|
||||
)
|
||||
|
||||
object CustomTokenName : ManageTokens("Custom Token Name")
|
||||
|
||||
object CustomTokenSymbol : ManageTokens("Custom Token Symbol")
|
||||
|
||||
object CustomTokenDecimals : ManageTokens("Custom Token Decimals")
|
||||
|
||||
enum class Derivation(val value: String) {
|
||||
DEFAULT("Default"),
|
||||
CUSTOM("Custom"),
|
||||
}
|
||||
}
|
||||
|
|
@ -38,7 +38,7 @@ internal sealed interface NetworkItemState {
|
|||
* @property onToggleClick lambda be invoked when switch is been toggled
|
||||
*/
|
||||
@Suppress("LongParameterList")
|
||||
class Toggleable(
|
||||
data class Toggleable(
|
||||
override val name: String,
|
||||
override val protocolName: String,
|
||||
override val id: String,
|
||||
|
|
@ -72,7 +72,7 @@ internal sealed interface NetworkItemState {
|
|||
* @property onNetworkClick lambda be invoked when network item is been clicked
|
||||
*
|
||||
*/
|
||||
class Selectable(
|
||||
data class Selectable(
|
||||
override val name: String,
|
||||
override val protocolName: String,
|
||||
val iconResId: Int,
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ internal sealed class TextFieldState {
|
|||
val isEnabled: Boolean,
|
||||
val error: AddCustomTokenWarning? = null,
|
||||
val onValueChange: (String) -> Unit,
|
||||
val onFocusExit: () -> Unit,
|
||||
) : TextFieldState()
|
||||
|
||||
fun isInputValid(): Boolean = this is Editable && value.isNotBlank() && error == null
|
||||
|
|
|
|||
|
|
@ -39,7 +39,7 @@ internal class AddCustomTokenStateToCryptoCurrencyConverter(
|
|||
private fun parseTokenOrNull(tokenData: CustomTokenData?): CryptoCurrencyFactory.Token? {
|
||||
val contractAddress = (tokenData?.contractAddressTextField as? TextFieldState.Editable)?.value
|
||||
val symbol = (tokenData?.symbolTextField as? TextFieldState.Editable)?.value
|
||||
val name = (tokenData?.contractAddressTextField as? TextFieldState.Editable)?.value
|
||||
val name = (tokenData?.nameTextField as? TextFieldState.Editable)?.value
|
||||
val decimals = (tokenData?.decimalsTextField as? TextFieldState.Editable)?.value?.toIntOrNull()
|
||||
return if (
|
||||
!contractAddress.isNullOrBlank() && !symbol.isNullOrBlank() && !name.isNullOrBlank() && decimals != null
|
||||
|
|
|
|||
|
|
@ -14,21 +14,25 @@ internal class ContractAddressToCustomTokenDataConverter(
|
|||
value = value,
|
||||
isEnabled = true,
|
||||
onValueChange = clickIntents::onContractAddressChange,
|
||||
onFocusExit = clickIntents::onContractAddressFocusExit,
|
||||
),
|
||||
nameTextField = TextFieldState.Editable(
|
||||
value = "",
|
||||
isEnabled = true,
|
||||
onValueChange = clickIntents::onTokenNameChange,
|
||||
onFocusExit = clickIntents::onTokenNameFocusExit,
|
||||
),
|
||||
symbolTextField = TextFieldState.Editable(
|
||||
value = "",
|
||||
isEnabled = true,
|
||||
onValueChange = clickIntents::onSymbolChange,
|
||||
onFocusExit = clickIntents::onSymbolFocusExit,
|
||||
),
|
||||
decimalsTextField = TextFieldState.Editable(
|
||||
value = "",
|
||||
isEnabled = true,
|
||||
onValueChange = clickIntents::onDecimalsChange,
|
||||
onFocusExit = clickIntents::onDecimalsFocusExit,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -201,21 +201,25 @@ internal class CustomTokensStateFactory(
|
|||
value = "",
|
||||
isEnabled = true,
|
||||
onValueChange = clickIntents::onContractAddressChange,
|
||||
onFocusExit = clickIntents::onContractAddressFocusExit,
|
||||
),
|
||||
nameTextField = TextFieldState.Editable(
|
||||
value = "",
|
||||
isEnabled = false,
|
||||
onValueChange = clickIntents::onTokenNameChange,
|
||||
onFocusExit = clickIntents::onTokenNameFocusExit,
|
||||
),
|
||||
symbolTextField = TextFieldState.Editable(
|
||||
value = "",
|
||||
isEnabled = false,
|
||||
onValueChange = clickIntents::onSymbolChange,
|
||||
onFocusExit = clickIntents::onSymbolFocusExit,
|
||||
),
|
||||
decimalsTextField = TextFieldState.Editable(
|
||||
value = "",
|
||||
isEnabled = false,
|
||||
onValueChange = clickIntents::onDecimalsChange,
|
||||
onFocusExit = clickIntents::onDecimalsFocusExit,
|
||||
),
|
||||
)
|
||||
} else {
|
||||
|
|
@ -268,6 +272,8 @@ internal class CustomTokensStateFactory(
|
|||
value = contractAddress,
|
||||
isEnabled = true,
|
||||
onValueChange = clickIntents::onContractAddressChange,
|
||||
onFocusExit = clickIntents::onContractAddressFocusExit,
|
||||
|
||||
),
|
||||
nameTextField = TextFieldState.Loading,
|
||||
symbolTextField = TextFieldState.Loading,
|
||||
|
|
|
|||
|
|
@ -15,21 +15,25 @@ internal class FoundTokenToCustomTokenDataConverter(
|
|||
value = value.contractAddress,
|
||||
isEnabled = true,
|
||||
onValueChange = clickIntents::onContractAddressChange,
|
||||
onFocusExit = clickIntents::onContractAddressFocusExit,
|
||||
),
|
||||
nameTextField = TextFieldState.Editable(
|
||||
value = value.name,
|
||||
isEnabled = false,
|
||||
onValueChange = clickIntents::onTokenNameChange,
|
||||
onFocusExit = clickIntents::onTokenNameFocusExit,
|
||||
),
|
||||
symbolTextField = TextFieldState.Editable(
|
||||
value = value.symbol,
|
||||
isEnabled = false,
|
||||
onValueChange = clickIntents::onSymbolChange,
|
||||
onFocusExit = clickIntents::onSymbolFocusExit,
|
||||
),
|
||||
decimalsTextField = TextFieldState.Editable(
|
||||
value = value.decimals.toString(),
|
||||
isEnabled = false,
|
||||
onValueChange = clickIntents::onDecimalsChange,
|
||||
onFocusExit = clickIntents::onDecimalsFocusExit,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -38,6 +38,7 @@ internal object AddCustomTokenPreviewData {
|
|||
value = "0x4ace7262705b68bcba5b91de96889349394",
|
||||
isEnabled = false,
|
||||
onValueChange = {},
|
||||
onFocusExit = {},
|
||||
),
|
||||
nameTextField = TextFieldState.Loading,
|
||||
symbolTextField = TextFieldState.Loading,
|
||||
|
|
|
|||
|
|
@ -6,7 +6,11 @@ import androidx.compose.foundation.text.BasicTextField
|
|||
import androidx.compose.foundation.text.KeyboardOptions
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.focus.onFocusChanged
|
||||
import androidx.compose.ui.graphics.SolidColor
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.input.ImeAction
|
||||
|
|
@ -20,6 +24,11 @@ internal fun TokenTextField(
|
|||
placeholder: String,
|
||||
keyboardType: KeyboardType = KeyboardType.Text,
|
||||
) {
|
||||
val isInitiallyComposed = remember { mutableStateOf(false) }
|
||||
LaunchedEffect(key1 = true) {
|
||||
isInitiallyComposed.value = true
|
||||
}
|
||||
|
||||
BasicTextField(
|
||||
value = state.value,
|
||||
onValueChange = state.onValueChange,
|
||||
|
|
@ -32,7 +41,12 @@ internal fun TokenTextField(
|
|||
),
|
||||
cursorBrush = SolidColor(TangemTheme.colors.icon.primary1),
|
||||
modifier = Modifier
|
||||
.fillMaxWidth(),
|
||||
.fillMaxWidth()
|
||||
.onFocusChanged {
|
||||
if (!it.isFocused && isInitiallyComposed.value) {
|
||||
state.onFocusExit()
|
||||
}
|
||||
},
|
||||
decorationBox = { innerTextField ->
|
||||
Row(modifier = Modifier.fillMaxWidth()) {
|
||||
if (state.value.isEmpty()) {
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ package com.tangem.managetokens.presentation.customtokens.viewmodels
|
|||
import com.tangem.managetokens.presentation.common.state.NetworkItemState
|
||||
import com.tangem.managetokens.presentation.customtokens.state.Derivation
|
||||
|
||||
@Suppress("TooManyFunctions")
|
||||
internal interface CustomTokensClickIntents {
|
||||
|
||||
fun onNetworkSelected(networkItemState: NetworkItemState)
|
||||
|
|
@ -25,6 +26,14 @@ internal interface CustomTokensClickIntents {
|
|||
|
||||
fun onDecimalsChange(input: String)
|
||||
|
||||
fun onContractAddressFocusExit()
|
||||
|
||||
fun onTokenNameFocusExit()
|
||||
|
||||
fun onSymbolFocusExit()
|
||||
|
||||
fun onDecimalsFocusExit()
|
||||
|
||||
fun onDerivationSelected(derivation: Derivation)
|
||||
|
||||
fun onChooseDerivationClick()
|
||||
|
|
|
|||
|
|
@ -6,16 +6,20 @@ import androidx.compose.runtime.setValue
|
|||
import androidx.lifecycle.DefaultLifecycleObserver
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import arrow.core.getOrElse
|
||||
import com.tangem.core.analytics.api.AnalyticsEventHandler
|
||||
import com.tangem.crypto.hdWallet.DerivationPath
|
||||
import com.tangem.crypto.hdWallet.HDWalletError
|
||||
import com.tangem.domain.common.util.derivationStyleProvider
|
||||
import com.tangem.domain.tokens.*
|
||||
import com.tangem.domain.tokens.model.CryptoCurrency
|
||||
import com.tangem.domain.tokens.model.Network
|
||||
import com.tangem.domain.wallets.models.UserWallet
|
||||
import com.tangem.domain.wallets.models.UserWalletId
|
||||
import com.tangem.domain.wallets.usecase.GetSelectedWalletSyncUseCase
|
||||
import com.tangem.domain.wallets.usecase.GetWalletsUseCase
|
||||
import com.tangem.domain.wallets.usecase.SelectWalletUseCase
|
||||
import com.tangem.managetokens.presentation.common.analytics.ManageTokens
|
||||
import com.tangem.managetokens.presentation.common.state.AlertState
|
||||
import com.tangem.managetokens.presentation.common.state.Event
|
||||
import com.tangem.managetokens.presentation.common.state.NetworkItemState
|
||||
|
|
@ -39,7 +43,7 @@ import kotlinx.coroutines.withContext
|
|||
import javax.inject.Inject
|
||||
import kotlin.properties.Delegates
|
||||
|
||||
@Suppress("LongParameterList")
|
||||
@Suppress("LongParameterList", "TooManyFunctions", "LargeClass")
|
||||
@HiltViewModel
|
||||
internal class CustomTokensViewModel @Inject constructor(
|
||||
private val dispatchers: CoroutineDispatcherProvider,
|
||||
|
|
@ -52,6 +56,7 @@ internal class CustomTokensViewModel @Inject constructor(
|
|||
private val validateContractAddressUseCase: ValidateContractAddressUseCase,
|
||||
private val getNetworksSupportedByWallet: GetNetworksSupportedByWallet,
|
||||
private val areTokensSupportedByNetworkUseCase: AreTokensSupportedByNetworkUseCase,
|
||||
private val analyticsEventHandler: AnalyticsEventHandler,
|
||||
) : ViewModel(), CustomTokensClickIntents, DefaultLifecycleObserver {
|
||||
|
||||
private val debouncer = Debouncer()
|
||||
|
|
@ -110,6 +115,7 @@ internal class CustomTokensViewModel @Inject constructor(
|
|||
}
|
||||
|
||||
override fun onNetworkSelected(networkItemState: NetworkItemState) {
|
||||
analyticsEventHandler.send(ManageTokens.CustomTokenNetworkSelected(networkItemState.name))
|
||||
selectNetwork(networkItemState)
|
||||
router.popBackStack()
|
||||
}
|
||||
|
|
@ -136,6 +142,7 @@ internal class CustomTokensViewModel @Inject constructor(
|
|||
}
|
||||
|
||||
override fun onWalletSelected(walletId: String) {
|
||||
analyticsEventHandler.send(ManageTokens.WalletSelected(ManageTokens.WalletSelected.Source.CustomToken))
|
||||
viewModelScope.launch(dispatchers.io) {
|
||||
val userWalletId = UserWalletId(walletId)
|
||||
selectWalletUseCase(userWalletId)
|
||||
|
|
@ -166,6 +173,7 @@ internal class CustomTokensViewModel @Inject constructor(
|
|||
value = input,
|
||||
isEnabled = true,
|
||||
onValueChange = this::onContractAddressChange,
|
||||
onFocusExit = this::onContractAddressFocusExit,
|
||||
),
|
||||
),
|
||||
)
|
||||
|
|
@ -202,9 +210,14 @@ internal class CustomTokensViewModel @Inject constructor(
|
|||
ifLeft = {
|
||||
val tokenData = ContractAddressToCustomTokenDataConverter(this@CustomTokensViewModel)
|
||||
.convert(contractAddress)
|
||||
|
||||
val isButtonEnabled = tokenData.isRequiredInformationProvided()
|
||||
uiState = uiState.copy(
|
||||
tokenData = tokenData,
|
||||
warnings = (uiState.warnings + AddCustomTokenWarning.PotentialScamToken).toPersistentSet(),
|
||||
addTokenButton = uiState.addTokenButton.copy(
|
||||
isEnabled = isButtonEnabled,
|
||||
),
|
||||
)
|
||||
},
|
||||
ifRight = { token ->
|
||||
|
|
@ -215,7 +228,14 @@ internal class CustomTokensViewModel @Inject constructor(
|
|||
contractAddress,
|
||||
)
|
||||
}
|
||||
uiState = uiState.copy(tokenData = tokenData)
|
||||
|
||||
val isButtonEnabled = tokenData.isRequiredInformationProvided()
|
||||
uiState = uiState.copy(
|
||||
tokenData = tokenData,
|
||||
addTokenButton = uiState.addTokenButton.copy(
|
||||
isEnabled = isButtonEnabled,
|
||||
),
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
|
@ -229,8 +249,12 @@ internal class CustomTokensViewModel @Inject constructor(
|
|||
value = input,
|
||||
isEnabled = true,
|
||||
onValueChange = this::onTokenNameChange,
|
||||
onFocusExit = this::onTokenNameFocusExit,
|
||||
),
|
||||
),
|
||||
addTokenButton = uiState.addTokenButton.copy(
|
||||
isEnabled = uiState.tokenData?.isRequiredInformationProvided() == true,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -241,8 +265,12 @@ internal class CustomTokensViewModel @Inject constructor(
|
|||
value = input,
|
||||
isEnabled = true,
|
||||
onValueChange = this::onSymbolChange,
|
||||
onFocusExit = this::onSymbolFocusExit,
|
||||
),
|
||||
),
|
||||
addTokenButton = uiState.addTokenButton.copy(
|
||||
isEnabled = uiState.tokenData?.isRequiredInformationProvided() == true,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -260,14 +288,40 @@ internal class CustomTokensViewModel @Inject constructor(
|
|||
isEnabled = true,
|
||||
onValueChange = this::onDecimalsChange,
|
||||
error = error,
|
||||
onFocusExit = this::onDecimalsFocusExit,
|
||||
),
|
||||
),
|
||||
addTokenButton = uiState.addTokenButton.copy(
|
||||
isEnabled = uiState.tokenData?.isRequiredInformationProvided() == true,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
override fun onContractAddressFocusExit() {
|
||||
val error = (uiState.tokenData?.contractAddressTextField as? TextFieldState.Editable)?.error
|
||||
val validated = error !is AddCustomTokenWarning.InvalidContractAddress
|
||||
analyticsEventHandler.send(ManageTokens.CustomTokenAddress(validated = validated))
|
||||
}
|
||||
|
||||
override fun onTokenNameFocusExit() {
|
||||
analyticsEventHandler.send(ManageTokens.CustomTokenName)
|
||||
}
|
||||
|
||||
override fun onSymbolFocusExit() {
|
||||
analyticsEventHandler.send(ManageTokens.CustomTokenSymbol)
|
||||
}
|
||||
|
||||
override fun onDecimalsFocusExit() {
|
||||
analyticsEventHandler.send(ManageTokens.CustomTokenDecimals)
|
||||
}
|
||||
|
||||
override fun onDerivationSelected(derivation: Derivation) {
|
||||
uiState =
|
||||
uiState.copy(chooseDerivationState = uiState.chooseDerivationState?.copy(selectedDerivation = derivation))
|
||||
derivation.standardType?.let {
|
||||
analyticsEventHandler.send(ManageTokens.CustomTokenDerivationSelected(derivation.networkName))
|
||||
}
|
||||
uiState = uiState.copy(
|
||||
chooseDerivationState = uiState.chooseDerivationState?.copy(selectedDerivation = derivation),
|
||||
)
|
||||
router.popBackStack()
|
||||
}
|
||||
|
||||
|
|
@ -280,6 +334,7 @@ internal class CustomTokensViewModel @Inject constructor(
|
|||
}
|
||||
|
||||
override fun onCustomDerivationChange(input: String) {
|
||||
analyticsEventHandler.send(ManageTokens.CustomTokenDerivationSelected(ManageTokens.Derivation.CUSTOM.value))
|
||||
uiState = uiState.copy(
|
||||
chooseDerivationState = uiState.chooseDerivationState?.copy(
|
||||
enterCustomDerivationState = uiState.chooseDerivationState?.enterCustomDerivationState?.copy(
|
||||
|
|
@ -332,21 +387,77 @@ internal class CustomTokensViewModel @Inject constructor(
|
|||
val cryptoCurrency = AddCustomTokenStateToCryptoCurrencyConverter(
|
||||
selectedWallet.scanResponse.derivationStyleProvider,
|
||||
).convert(uiState)
|
||||
val alreadyAdded =
|
||||
getCurrenciesUseCase(selectedWallet.walletId).getOrNull()?.any { it == cryptoCurrency }
|
||||
if (alreadyAdded == true) {
|
||||
val alreadyAdded = isCryptoCurrencyAlreadyAdded(selectedWallet, cryptoCurrency)
|
||||
if (alreadyAdded) {
|
||||
uiState = stateFactory.getStateAndTriggerEvent(
|
||||
state = uiState,
|
||||
event = Event.ShowAlert(AlertState.TokenAlreadyAdded),
|
||||
setUiState = { uiState = it },
|
||||
)
|
||||
} else {
|
||||
sendTokenAddedEvent(cryptoCurrency)
|
||||
addCryptoCurrenciesUseCase(selectedWallet.walletId, currency = cryptoCurrency)
|
||||
withContext(dispatchers.main) { router.popBackStack() }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun isCryptoCurrencyAlreadyAdded(
|
||||
selectedWallet: UserWallet,
|
||||
cryptoCurrency: CryptoCurrency,
|
||||
): Boolean {
|
||||
val currenciesList = getCurrenciesUseCase(selectedWallet.walletId).getOrElse { emptyList() }
|
||||
return when (cryptoCurrency) {
|
||||
is CryptoCurrency.Coin -> {
|
||||
currenciesList.any {
|
||||
it is CryptoCurrency.Coin &&
|
||||
it.id == cryptoCurrency.id &&
|
||||
it.network.derivationPath == cryptoCurrency.network.derivationPath
|
||||
}
|
||||
}
|
||||
is CryptoCurrency.Token -> {
|
||||
currenciesList.any {
|
||||
(it as? CryptoCurrency.Token)?.let {
|
||||
it.id == cryptoCurrency.id &&
|
||||
it.contractAddress == cryptoCurrency.contractAddress &&
|
||||
it.network.id == cryptoCurrency.network.id &&
|
||||
it.network.derivationPath == cryptoCurrency.network.derivationPath
|
||||
} ?: false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun sendTokenAddedEvent(cryptoCurrency: CryptoCurrency) {
|
||||
val selectedDerivation = uiState.chooseDerivationState?.selectedDerivation
|
||||
|
||||
val derivation = when {
|
||||
selectedDerivation == null -> ManageTokens.Derivation.DEFAULT.value
|
||||
selectedDerivation.networkName.isNotEmpty() -> selectedDerivation.networkName
|
||||
else -> ManageTokens.Derivation.CUSTOM.value
|
||||
}
|
||||
when (cryptoCurrency) {
|
||||
is CryptoCurrency.Token -> {
|
||||
analyticsEventHandler.send(
|
||||
ManageTokens.CustomTokenWasAdded(
|
||||
derivation = derivation,
|
||||
networkId = cryptoCurrency.network.name,
|
||||
contractAddress = cryptoCurrency.contractAddress,
|
||||
token = cryptoCurrency.symbol,
|
||||
),
|
||||
)
|
||||
}
|
||||
is CryptoCurrency.Coin -> {
|
||||
analyticsEventHandler.send(
|
||||
ManageTokens.CustomTokenWasAdded(
|
||||
derivation = derivation,
|
||||
networkId = cryptoCurrency.network.name,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onBack() {
|
||||
router.popBackStack()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@ internal data class ManageTokensState(
|
|||
val selectedToken: TokenItemState.Loaded? = null,
|
||||
val showChooseWalletScreen: Boolean = false,
|
||||
val event: StateEvent<Event>,
|
||||
val onEmptySearchResult: (String) -> Unit,
|
||||
)
|
||||
|
||||
data class AddCustomTokenButton(
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ import com.tangem.managetokens.presentation.common.state.*
|
|||
import com.tangem.managetokens.presentation.common.utils.CurrencyUtils
|
||||
import com.tangem.managetokens.presentation.managetokens.state.*
|
||||
import com.tangem.managetokens.presentation.managetokens.viewmodels.ManageTokensClickIntents
|
||||
import com.tangem.managetokens.presentation.managetokens.viewmodels.ManageTokensUiEvents
|
||||
import com.tangem.utils.Provider
|
||||
import kotlinx.collections.immutable.toPersistentList
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
|
@ -17,6 +18,7 @@ import kotlinx.coroutines.flow.Flow
|
|||
internal class ManageTokensStateFactory(
|
||||
private val currentStateProvider: Provider<ManageTokensState>,
|
||||
private val clickIntents: ManageTokensClickIntents,
|
||||
private val uiIntents: ManageTokensUiEvents,
|
||||
) {
|
||||
|
||||
fun getInitialState(tokens: Flow<PagingData<TokenItemState>>): ManageTokensState {
|
||||
|
|
@ -36,6 +38,7 @@ internal class ManageTokensStateFactory(
|
|||
isLoading = false,
|
||||
event = consumedEvent(),
|
||||
chooseWalletState = ChooseWalletState.NoSelection,
|
||||
onEmptySearchResult = uiIntents::onEmptySearchResult,
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -176,7 +179,7 @@ internal class ManageTokensStateFactory(
|
|||
totalNeeded = totalNeeded,
|
||||
totalWallets = totalWallets,
|
||||
walletsToDerive = walletsToDerive,
|
||||
onGenerateClick = clickIntents::onGenerateDerivationClick,
|
||||
onGenerateClick = clickIntents::onGetAddressesClick,
|
||||
)
|
||||
}
|
||||
return currentStateProvider().copy(derivationNotification = derivationNotificationState)
|
||||
|
|
|
|||
|
|
@ -37,7 +37,7 @@ internal val nonNativeNetworks = listOf(
|
|||
iconResId = mutableStateOf(R.drawable.img_kusama_22),
|
||||
isMainNetwork = false,
|
||||
isAdded = mutableStateOf(true),
|
||||
id = "",
|
||||
id = "1",
|
||||
onToggleClick = { _, _ -> },
|
||||
address = "",
|
||||
decimals = 0,
|
||||
|
|
@ -48,7 +48,7 @@ internal val nonNativeNetworks = listOf(
|
|||
iconResId = mutableStateOf(R.drawable.ic_bsc_16),
|
||||
isMainNetwork = false,
|
||||
isAdded = mutableStateOf(false),
|
||||
id = "",
|
||||
id = "2",
|
||||
onToggleClick = { _, _ -> },
|
||||
address = "",
|
||||
decimals = 0,
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@ internal object ManageTokensStatePreviewData {
|
|||
derivationNotification = DerivationNotificationStatePreviewData.state,
|
||||
event = consumedEvent(),
|
||||
chooseWalletState = ChooseWalletStatePreviewData.state,
|
||||
onEmptySearchResult = {},
|
||||
)
|
||||
|
||||
val loadingState: ManageTokensState
|
||||
|
|
|
|||
|
|
@ -14,6 +14,8 @@ import androidx.compose.ui.Modifier
|
|||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||
import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider
|
||||
import androidx.paging.LoadState
|
||||
import androidx.paging.compose.LazyPagingItems
|
||||
import androidx.paging.compose.collectAsLazyPagingItems
|
||||
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
|
|
@ -77,6 +79,14 @@ private fun Content(state: ManageTokensState) {
|
|||
)
|
||||
}
|
||||
val tokens = state.tokens.collectAsLazyPagingItems()
|
||||
val query = state.searchBarState.query
|
||||
|
||||
TrackPossibleEmptySearchResult(
|
||||
tokens = tokens,
|
||||
query = query,
|
||||
onEmptySearchResult = state.onEmptySearchResult,
|
||||
)
|
||||
|
||||
TokensList(tokens = tokens, addCustomTokenButton = state.addCustomTokenButton)
|
||||
}
|
||||
state.derivationNotification?.let {
|
||||
|
|
@ -92,6 +102,27 @@ private fun Content(state: ManageTokensState) {
|
|||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun TrackPossibleEmptySearchResult(
|
||||
tokens: LazyPagingItems<TokenItemState>,
|
||||
query: String,
|
||||
onEmptySearchResult: (String) -> Unit,
|
||||
) {
|
||||
val wasLoading = remember { mutableStateOf(false) }
|
||||
|
||||
LaunchedEffect(tokens.loadState) {
|
||||
val isLoading = tokens.loadState.refresh == LoadState.Loading
|
||||
val stoppedLoading = wasLoading.value && !isLoading
|
||||
val queryAndTokensCondition = query.isNotEmpty() && tokens.itemSnapshotList.isEmpty()
|
||||
|
||||
if (stoppedLoading && queryAndTokensCondition) {
|
||||
onEmptySearchResult(query)
|
||||
}
|
||||
|
||||
wasLoading.value = isLoading
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ManageTokensBottomSheet(selectedToken: TokenItemState.Loaded, state: ManageTokensState) {
|
||||
if (state.showChooseWalletScreen && state.chooseWalletState is ChooseWalletState.Choose) {
|
||||
|
|
|
|||
|
|
@ -4,23 +4,24 @@ import com.tangem.managetokens.presentation.common.state.NetworkItemState
|
|||
import com.tangem.managetokens.presentation.managetokens.state.TokenItemState
|
||||
|
||||
internal interface ManageTokensClickIntents {
|
||||
|
||||
fun onAddCustomTokensButtonClick()
|
||||
|
||||
fun onSearchQueryChange(query: String)
|
||||
|
||||
fun onSearchActiveChange(active: Boolean)
|
||||
|
||||
fun onTokenItemButtonClick(token: TokenItemState.Loaded)
|
||||
|
||||
fun onGenerateDerivationClick()
|
||||
fun onGetAddressesClick()
|
||||
|
||||
fun onBackClick()
|
||||
|
||||
fun onCloseChooseNetworkScreen()
|
||||
|
||||
fun onNetworkToggleClick(token: TokenItemState.Loaded, network: NetworkItemState.Toggleable)
|
||||
fun onNonNativeNetworkHintClick()
|
||||
|
||||
fun onSelectWalletsClick()
|
||||
fun onNonNativeNetworkHintClick()
|
||||
|
||||
fun onChooseWalletClick()
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,6 @@
|
|||
package com.tangem.managetokens.presentation.managetokens.viewmodels
|
||||
|
||||
internal interface ManageTokensUiEvents {
|
||||
|
||||
fun onEmptySearchResult(query: String)
|
||||
}
|
||||
|
|
@ -9,6 +9,8 @@ import androidx.lifecycle.viewModelScope
|
|||
import androidx.paging.PagingData
|
||||
import androidx.paging.map
|
||||
import arrow.core.getOrElse
|
||||
import com.tangem.core.analytics.api.AnalyticsEventHandler
|
||||
import com.tangem.core.analytics.models.AnalyticsParam
|
||||
import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase
|
||||
import com.tangem.domain.appcurrency.model.AppCurrency
|
||||
import com.tangem.domain.card.DerivePublicKeysUseCase
|
||||
|
|
@ -20,6 +22,7 @@ import com.tangem.domain.wallets.models.UserWalletId
|
|||
import com.tangem.domain.wallets.usecase.GetSelectedWalletSyncUseCase
|
||||
import com.tangem.domain.wallets.usecase.GetWalletsUseCase
|
||||
import com.tangem.domain.wallets.usecase.SelectWalletUseCase
|
||||
import com.tangem.managetokens.presentation.common.analytics.ManageTokens
|
||||
import com.tangem.managetokens.presentation.common.state.AlertState
|
||||
import com.tangem.managetokens.presentation.common.state.Event
|
||||
import com.tangem.managetokens.presentation.common.state.NetworkItemState
|
||||
|
|
@ -58,13 +61,15 @@ internal class ManageTokensViewModel @Inject constructor(
|
|||
private val getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase,
|
||||
private val checkCurrencyCompatibilityUseCase: CheckCurrencyCompatibilityUseCase,
|
||||
private val isCryptoCurrencyCoinCouldHide: IsCryptoCurrencyCoinCouldHideUseCase,
|
||||
) : ViewModel(), ManageTokensClickIntents, DefaultLifecycleObserver {
|
||||
private val analyticsEventHandler: AnalyticsEventHandler,
|
||||
) : ViewModel(), ManageTokensClickIntents, ManageTokensUiEvents, DefaultLifecycleObserver {
|
||||
|
||||
private val debouncer = Debouncer()
|
||||
|
||||
private val stateFactory = ManageTokensStateFactory(
|
||||
currentStateProvider = Provider { uiState },
|
||||
clickIntents = this,
|
||||
uiIntents = this,
|
||||
)
|
||||
|
||||
var router: InnerManageTokensRouter by Delegates.notNull()
|
||||
|
|
@ -80,7 +85,7 @@ internal class ManageTokensViewModel @Inject constructor(
|
|||
|
||||
private var selectedWallet: UserWallet? = null
|
||||
|
||||
private var neededDerivations: Map<UserWalletId, List<CryptoCurrency>> = emptyMap()
|
||||
private var currenciesToGenerateAddresses: Map<UserWalletId, List<CryptoCurrency>> = emptyMap()
|
||||
|
||||
private val selectedAppCurrencyFlow: StateFlow<AppCurrency> = createSelectedAppCurrencyFlow()
|
||||
|
||||
|
|
@ -107,6 +112,8 @@ internal class ManageTokensViewModel @Inject constructor(
|
|||
)
|
||||
|
||||
init {
|
||||
analyticsEventHandler.send(ManageTokens.ScreenOpened())
|
||||
|
||||
viewModelScope.launch(dispatchers.io) {
|
||||
getWalletsUseCase()
|
||||
.distinctUntilChanged()
|
||||
|
|
@ -151,7 +158,7 @@ internal class ManageTokensViewModel @Inject constructor(
|
|||
.distinctUntilChanged()
|
||||
.collectLatest {
|
||||
it.onRight { mapOfMissingDerivations ->
|
||||
neededDerivations = mapOfMissingDerivations
|
||||
currenciesToGenerateAddresses = mapOfMissingDerivations
|
||||
withContext(dispatchers.main) { updateDerivation() }
|
||||
}
|
||||
}
|
||||
|
|
@ -159,8 +166,8 @@ internal class ManageTokensViewModel @Inject constructor(
|
|||
}
|
||||
|
||||
private fun updateDerivation() {
|
||||
val totalNeeded = neededDerivations.values.sumOf { derivations -> derivations.size }
|
||||
val walletsToDerive = neededDerivations.values
|
||||
val totalNeeded = currenciesToGenerateAddresses.values.sumOf { derivations -> derivations.size }
|
||||
val walletsToDerive = currenciesToGenerateAddresses.values
|
||||
.filter { derivations -> derivations.isNotEmpty() }.size
|
||||
uiState = stateFactory.updateDerivationNotification(
|
||||
totalNeeded = totalNeeded,
|
||||
|
|
@ -182,6 +189,7 @@ internal class ManageTokensViewModel @Inject constructor(
|
|||
}
|
||||
|
||||
override fun onAddCustomTokensButtonClick() {
|
||||
analyticsEventHandler.send(ManageTokens.ButtonCustomToken)
|
||||
router.openCustomTokensScreen()
|
||||
}
|
||||
|
||||
|
|
@ -201,6 +209,13 @@ internal class ManageTokensViewModel @Inject constructor(
|
|||
override fun onTokenItemButtonClick(token: TokenItemState.Loaded) {
|
||||
when (token.availableAction.value) {
|
||||
TokenButtonType.ADD, TokenButtonType.EDIT -> {
|
||||
if (token.availableAction.value == TokenButtonType.ADD) {
|
||||
analyticsEventHandler.send(ManageTokens.ButtonAdd(token.currencySymbol))
|
||||
}
|
||||
if (token.availableAction.value == TokenButtonType.EDIT) {
|
||||
analyticsEventHandler.send(ManageTokens.ButtonEdit(token.currencySymbol))
|
||||
}
|
||||
|
||||
uiState = uiState.copy(selectedToken = token)
|
||||
val addedCurrenciesOnWallet = addedCurrenciesByWallet[selectedWallet] ?: listOf()
|
||||
stateFactory.updateTokenNetworksOnTokenSelection(token, addedCurrenciesOnWallet)
|
||||
|
|
@ -219,17 +234,21 @@ internal class ManageTokensViewModel @Inject constructor(
|
|||
}
|
||||
}
|
||||
|
||||
override fun onGenerateDerivationClick() {
|
||||
if (neededDerivations.isNotEmpty()) {
|
||||
override fun onGetAddressesClick() {
|
||||
if (currenciesToGenerateAddresses.isNotEmpty()) {
|
||||
viewModelScope.launch(dispatchers.io) {
|
||||
val walletId = neededDerivations.keys.firstOrNull()
|
||||
val currenciesToDerive = neededDerivations[walletId]
|
||||
if (walletId == null || currenciesToDerive.isNullOrEmpty()) return@launch
|
||||
derivePublicKeysUseCase(walletId, currenciesToDerive)
|
||||
.onRight {
|
||||
updateDerivationNotificationState()
|
||||
fetchTokenListUseCase(userWalletId = walletId)
|
||||
val cardCount = currenciesToGenerateAddresses.count { it.value.isNotEmpty() }
|
||||
analyticsEventHandler.send(ManageTokens.ButtonGenerateAddresses(cardCount))
|
||||
|
||||
currenciesToGenerateAddresses.forEach { (walletId, currenciesToDerive) ->
|
||||
if (currenciesToDerive.isNotEmpty()) {
|
||||
derivePublicKeysUseCase(walletId, currenciesToDerive)
|
||||
.onRight {
|
||||
updateDerivationNotificationState()
|
||||
fetchTokenListUseCase(userWalletId = walletId)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -247,8 +266,14 @@ internal class ManageTokensViewModel @Inject constructor(
|
|||
if (!selectedWallet.isMultiCurrency || selectedWallet.isLocked) return
|
||||
|
||||
if (network.isAdded.value) {
|
||||
analyticsEventHandler.send(
|
||||
ManageTokens.TokenSwitcherChanged(token = token.currencySymbol, AnalyticsParam.OnOffState.Off),
|
||||
)
|
||||
toggleToken(token, network, selectedWallet)
|
||||
} else {
|
||||
analyticsEventHandler.send(
|
||||
ManageTokens.TokenSwitcherChanged(token = token.currencySymbol, AnalyticsParam.OnOffState.On),
|
||||
)
|
||||
viewModelScope.launch(dispatchers.io) {
|
||||
checkCompatibilityAndToggleToken(token, network, selectedWallet)
|
||||
}
|
||||
|
|
@ -348,6 +373,7 @@ internal class ManageTokensViewModel @Inject constructor(
|
|||
}
|
||||
|
||||
override fun onNonNativeNetworkHintClick() {
|
||||
analyticsEventHandler.send(ManageTokens.NoticeNonNativeNetworkClicked)
|
||||
uiState = stateFactory.getStateAndTriggerEvent(
|
||||
state = uiState,
|
||||
event = Event.ShowAlert(AlertState.NonNative),
|
||||
|
|
@ -355,13 +381,8 @@ internal class ManageTokensViewModel @Inject constructor(
|
|||
)
|
||||
}
|
||||
|
||||
override fun onSelectWalletsClick() {
|
||||
uiState = uiState.copy(
|
||||
showChooseWalletScreen = true,
|
||||
)
|
||||
}
|
||||
|
||||
override fun onChooseWalletClick() {
|
||||
analyticsEventHandler.send(ManageTokens.ButtonChooseWallet)
|
||||
uiState = uiState.copy(
|
||||
showChooseWalletScreen = true,
|
||||
)
|
||||
|
|
@ -374,6 +395,7 @@ internal class ManageTokensViewModel @Inject constructor(
|
|||
}
|
||||
|
||||
override fun onWalletSelected(walletId: String) {
|
||||
analyticsEventHandler.send(ManageTokens.WalletSelected(ManageTokens.WalletSelected.Source.MainToken))
|
||||
viewModelScope.launch(dispatchers.io) {
|
||||
selectWalletUseCase(UserWalletId(walletId))
|
||||
}
|
||||
|
|
@ -381,4 +403,8 @@ internal class ManageTokensViewModel @Inject constructor(
|
|||
uiState.selectedToken?.let { onTokenItemButtonClick(it) }
|
||||
uiState = stateFactory.updateSelectedWallet(selectedWalletId = selectedWallet?.walletId?.stringValue)
|
||||
}
|
||||
|
||||
override fun onEmptySearchResult(query: String) {
|
||||
analyticsEventHandler.send(ManageTokens.TokenIsNotFound(query))
|
||||
}
|
||||
}
|
||||
|
|
@ -92,7 +92,7 @@ tangemCardSdk = "develop-324"
|
|||
# endregion Tangem
|
||||
|
||||
# region Tools
|
||||
detektComposeRules = "1.2.2"
|
||||
detektComposeRules = "1.3.0"
|
||||
detekt = "1.22.0"
|
||||
# endregion Tools
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue