From 3bd03920182f16f3a8ccd1bd5317de3874f255c2 Mon Sep 17 00:00:00 2001 From: Tangem Date: Fri, 9 Feb 2024 10:31:06 +0100 Subject: [PATCH] Updated on 2026-08-14 --- ...lidator.kt => ContractAddressValidator.kt} | 2 +- .../viewmodels/AddCustomTokenViewModel.kt | 8 +- core/res/src/main/res/values/strings.xml | 1 - .../tokens/ValidateContractAddressUseCase.kt | 6 +- features/manage-tokens/impl/build.gradle.kts | 2 + .../common/analytics/ManageTokens.kt | 102 ++++++++++++++ .../common/state/NetworkItemState.kt | 4 +- .../customtokens/state/TextFieldState.kt | 1 + ...stomTokenStateToCryptoCurrencyConverter.kt | 2 +- ...ntractAddressToCustomTokenDataConverter.kt | 4 + .../state/factory/CustomTokensStateFactory.kt | 6 + .../FoundTokenToCustomTokenDataConverter.kt | 4 + .../previewdata/AddCustomTokenPreviewData.kt | 1 + .../customtokens/ui/TokenTextField.kt | 16 ++- .../viewmodels/CustomTokensClickIntents.kt | 9 ++ .../viewmodels/CustomTokensViewModel.kt | 125 +++++++++++++++++- .../managetokens/state/ManageTokensState.kt | 1 + .../state/factory/ManageTokensStateFactory.kt | 5 +- .../ChooseNetworkStatePreviewData.kt | 4 +- .../ManageTokensStatePreviewData.kt | 1 + .../managetokens/ui/ManageTokensScreen.kt | 31 +++++ .../viewmodels/ManageTokensClickIntents.kt | 7 +- .../viewmodels/ManageTokensUiEvents.kt | 6 + .../viewmodels/ManageTokensViewModel.kt | 66 ++++++--- gradle/dependencies.toml | 2 +- 25 files changed, 371 insertions(+), 45 deletions(-) rename app/src/main/java/com/tangem/tap/features/customtoken/impl/presentation/validators/{ContactAddressValidator.kt => ContractAddressValidator.kt} (97%) create mode 100644 features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/common/analytics/ManageTokens.kt create mode 100644 features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/viewmodels/ManageTokensUiEvents.kt diff --git a/app/src/main/java/com/tangem/tap/features/customtoken/impl/presentation/validators/ContactAddressValidator.kt b/app/src/main/java/com/tangem/tap/features/customtoken/impl/presentation/validators/ContractAddressValidator.kt similarity index 97% rename from app/src/main/java/com/tangem/tap/features/customtoken/impl/presentation/validators/ContactAddressValidator.kt rename to app/src/main/java/com/tangem/tap/features/customtoken/impl/presentation/validators/ContractAddressValidator.kt index b522a922b1..5a8c8bf385 100644 --- a/app/src/main/java/com/tangem/tap/features/customtoken/impl/presentation/validators/ContactAddressValidator.kt +++ b/app/src/main/java/com/tangem/tap/features/customtoken/impl/presentation/validators/ContractAddressValidator.kt @@ -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 { 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 fae60b48c3..0faab87b40 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 @@ -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 { 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, ) diff --git a/core/res/src/main/res/values/strings.xml b/core/res/src/main/res/values/strings.xml index d384d8f946..636726e3ca 100644 --- a/core/res/src/main/res/values/strings.xml +++ b/core/res/src/main/res/values/strings.xml @@ -285,7 +285,6 @@ 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. Not original or primary blockchain the token is hosted Non-native networks - Blockchain the cryptocurrency was initially created Networks Choose networks Wallet diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/ValidateContractAddressUseCase.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/ValidateContractAddressUseCase.kt index 361a79c45c..5ae4755564 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/ValidateContractAddressUseCase.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/ValidateContractAddressUseCase.kt @@ -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) } }, diff --git a/features/manage-tokens/impl/build.gradle.kts b/features/manage-tokens/impl/build.gradle.kts index 692109ece2..e388043baa 100644 --- a/features/manage-tokens/impl/build.gradle.kts +++ b/features/manage-tokens/impl/build.gradle.kts @@ -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) diff --git a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/common/analytics/ManageTokens.kt b/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/common/analytics/ManageTokens.kt new file mode 100644 index 0000000000..3a527de7f2 --- /dev/null +++ b/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/common/analytics/ManageTokens.kt @@ -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 = 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"), + } +} \ No newline at end of file diff --git a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/common/state/NetworkItemState.kt b/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/common/state/NetworkItemState.kt index 7f57f95a84..6255c750f9 100644 --- a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/common/state/NetworkItemState.kt +++ b/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/common/state/NetworkItemState.kt @@ -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, diff --git a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/customtokens/state/TextFieldState.kt b/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/customtokens/state/TextFieldState.kt index 090d4a2a22..3014818228 100644 --- a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/customtokens/state/TextFieldState.kt +++ b/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/customtokens/state/TextFieldState.kt @@ -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 diff --git a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/customtokens/state/factory/AddCustomTokenStateToCryptoCurrencyConverter.kt b/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/customtokens/state/factory/AddCustomTokenStateToCryptoCurrencyConverter.kt index 94f5828c6c..15e17fc927 100644 --- a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/customtokens/state/factory/AddCustomTokenStateToCryptoCurrencyConverter.kt +++ b/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/customtokens/state/factory/AddCustomTokenStateToCryptoCurrencyConverter.kt @@ -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 diff --git a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/customtokens/state/factory/ContractAddressToCustomTokenDataConverter.kt b/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/customtokens/state/factory/ContractAddressToCustomTokenDataConverter.kt index b6ab812200..422f3c4f4b 100644 --- a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/customtokens/state/factory/ContractAddressToCustomTokenDataConverter.kt +++ b/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/customtokens/state/factory/ContractAddressToCustomTokenDataConverter.kt @@ -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, ), ) } diff --git a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/customtokens/state/factory/CustomTokensStateFactory.kt b/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/customtokens/state/factory/CustomTokensStateFactory.kt index 8a72ab97bf..f32d3f3e88 100644 --- a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/customtokens/state/factory/CustomTokensStateFactory.kt +++ b/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/customtokens/state/factory/CustomTokensStateFactory.kt @@ -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, diff --git a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/customtokens/state/factory/FoundTokenToCustomTokenDataConverter.kt b/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/customtokens/state/factory/FoundTokenToCustomTokenDataConverter.kt index ebe6b1bee2..3aa06bdd9f 100644 --- a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/customtokens/state/factory/FoundTokenToCustomTokenDataConverter.kt +++ b/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/customtokens/state/factory/FoundTokenToCustomTokenDataConverter.kt @@ -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, ), ) } diff --git a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/customtokens/state/previewdata/AddCustomTokenPreviewData.kt b/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/customtokens/state/previewdata/AddCustomTokenPreviewData.kt index 998c00a3e7..9be15589aa 100644 --- a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/customtokens/state/previewdata/AddCustomTokenPreviewData.kt +++ b/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/customtokens/state/previewdata/AddCustomTokenPreviewData.kt @@ -38,6 +38,7 @@ internal object AddCustomTokenPreviewData { value = "0x4ace7262705b68bcba5b91de96889349394", isEnabled = false, onValueChange = {}, + onFocusExit = {}, ), nameTextField = TextFieldState.Loading, symbolTextField = TextFieldState.Loading, diff --git a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/customtokens/ui/TokenTextField.kt b/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/customtokens/ui/TokenTextField.kt index a615d22738..e5b7eef118 100644 --- a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/customtokens/ui/TokenTextField.kt +++ b/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/customtokens/ui/TokenTextField.kt @@ -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()) { diff --git a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/customtokens/viewmodels/CustomTokensClickIntents.kt b/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/customtokens/viewmodels/CustomTokensClickIntents.kt index 6601f3e270..2dd2151beb 100644 --- a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/customtokens/viewmodels/CustomTokensClickIntents.kt +++ b/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/customtokens/viewmodels/CustomTokensClickIntents.kt @@ -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() diff --git a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/customtokens/viewmodels/CustomTokensViewModel.kt b/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/customtokens/viewmodels/CustomTokensViewModel.kt index cc265a1bc2..2ccbd92705 100644 --- a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/customtokens/viewmodels/CustomTokensViewModel.kt +++ b/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/customtokens/viewmodels/CustomTokensViewModel.kt @@ -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() } diff --git a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/state/ManageTokensState.kt b/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/state/ManageTokensState.kt index 1ee0ecebce..8b4c05217c 100644 --- a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/state/ManageTokensState.kt +++ b/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/state/ManageTokensState.kt @@ -16,6 +16,7 @@ internal data class ManageTokensState( val selectedToken: TokenItemState.Loaded? = null, val showChooseWalletScreen: Boolean = false, val event: StateEvent, + val onEmptySearchResult: (String) -> Unit, ) data class AddCustomTokenButton( diff --git a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/state/factory/ManageTokensStateFactory.kt b/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/state/factory/ManageTokensStateFactory.kt index bf81e5bc4c..a32b887e26 100644 --- a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/state/factory/ManageTokensStateFactory.kt +++ b/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/state/factory/ManageTokensStateFactory.kt @@ -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, private val clickIntents: ManageTokensClickIntents, + private val uiIntents: ManageTokensUiEvents, ) { fun getInitialState(tokens: Flow>): 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) diff --git a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/state/previewdata/ChooseNetworkStatePreviewData.kt b/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/state/previewdata/ChooseNetworkStatePreviewData.kt index e9fedc1587..f66b65bc1b 100644 --- a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/state/previewdata/ChooseNetworkStatePreviewData.kt +++ b/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/state/previewdata/ChooseNetworkStatePreviewData.kt @@ -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, diff --git a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/state/previewdata/ManageTokensStatePreviewData.kt b/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/state/previewdata/ManageTokensStatePreviewData.kt index 6ea8316ba5..5d141ddbaa 100644 --- a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/state/previewdata/ManageTokensStatePreviewData.kt +++ b/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/state/previewdata/ManageTokensStatePreviewData.kt @@ -19,6 +19,7 @@ internal object ManageTokensStatePreviewData { derivationNotification = DerivationNotificationStatePreviewData.state, event = consumedEvent(), chooseWalletState = ChooseWalletStatePreviewData.state, + onEmptySearchResult = {}, ) val loadingState: ManageTokensState diff --git a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/ui/ManageTokensScreen.kt b/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/ui/ManageTokensScreen.kt index 1bbf0c61a9..fb67526346 100644 --- a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/ui/ManageTokensScreen.kt +++ b/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/ui/ManageTokensScreen.kt @@ -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, + 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) { diff --git a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/viewmodels/ManageTokensClickIntents.kt b/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/viewmodels/ManageTokensClickIntents.kt index a86b2cd1bb..2b6ba5b3bd 100644 --- a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/viewmodels/ManageTokensClickIntents.kt +++ b/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/viewmodels/ManageTokensClickIntents.kt @@ -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() diff --git a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/viewmodels/ManageTokensUiEvents.kt b/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/viewmodels/ManageTokensUiEvents.kt new file mode 100644 index 0000000000..a5ce66e8d5 --- /dev/null +++ b/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/viewmodels/ManageTokensUiEvents.kt @@ -0,0 +1,6 @@ +package com.tangem.managetokens.presentation.managetokens.viewmodels + +internal interface ManageTokensUiEvents { + + fun onEmptySearchResult(query: String) +} \ No newline at end of file diff --git a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/viewmodels/ManageTokensViewModel.kt b/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/viewmodels/ManageTokensViewModel.kt index ae04c6d7cd..e0630d462e 100644 --- a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/viewmodels/ManageTokensViewModel.kt +++ b/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/viewmodels/ManageTokensViewModel.kt @@ -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> = emptyMap() + private var currenciesToGenerateAddresses: Map> = emptyMap() private val selectedAppCurrencyFlow: StateFlow = 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)) + } } \ No newline at end of file diff --git a/gradle/dependencies.toml b/gradle/dependencies.toml index bdb562b050..266de7e848 100644 --- a/gradle/dependencies.toml +++ b/gradle/dependencies.toml @@ -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