diff --git a/app/src/main/java/com/tangem/tap/features/home/HomeViewModel.kt b/app/src/main/java/com/tangem/tap/features/home/HomeViewModel.kt index dd335deba7..0df4db4753 100644 --- a/app/src/main/java/com/tangem/tap/features/home/HomeViewModel.kt +++ b/app/src/main/java/com/tangem/tap/features/home/HomeViewModel.kt @@ -5,6 +5,7 @@ import androidx.lifecycle.viewModelScope import com.google.firebase.analytics.ktx.analytics import com.google.firebase.ktx.Firebase import com.tangem.common.routing.AppRoute +import com.tangem.common.routing.AppRoute.ManageTokens.Source import com.tangem.core.analytics.Analytics import com.tangem.core.analytics.api.AnalyticsEventHandler import com.tangem.core.analytics.models.AnalyticsParam @@ -67,7 +68,7 @@ internal class HomeViewModel @Inject constructor( analyticsEventHandler.send(IntroductionProcess.ButtonTokensList()) store.dispatch(TokensAction.SetArgs.ReadAccess) - store.dispatchNavigationAction { push(AppRoute.ManageTokens()) } + store.dispatchNavigationAction { push(AppRoute.ManageTokens(Source.STORIES)) } } private fun scanCard() { diff --git a/app/src/main/java/com/tangem/tap/routing/utils/ChildFactory.kt b/app/src/main/java/com/tangem/tap/routing/utils/ChildFactory.kt index 6d8ea8f746..0996998748 100644 --- a/app/src/main/java/com/tangem/tap/routing/utils/ChildFactory.kt +++ b/app/src/main/java/com/tangem/tap/routing/utils/ChildFactory.kt @@ -11,6 +11,7 @@ import com.tangem.features.details.component.DetailsComponent import com.tangem.features.disclaimer.api.components.DisclaimerComponent import com.tangem.features.managetokens.ManageTokensToggles import com.tangem.features.managetokens.component.ManageTokensComponent +import com.tangem.features.managetokens.component.ManageTokensSource import com.tangem.features.markets.details.MarketsTokenDetailsComponent import com.tangem.features.pushnotifications.api.featuretoggles.PushNotificationsFeatureToggles import com.tangem.features.pushnotifications.api.navigation.PushNotificationsRouter @@ -126,9 +127,15 @@ internal class ChildFactory @Inject constructor( } is AppRoute.ManageTokens -> { if (manageTokensToggles.isFeatureEnabled) { + val source = when (route.source) { + AppRoute.ManageTokens.Source.SETTINGS -> ManageTokensSource.SETTINGS + AppRoute.ManageTokens.Source.ONBOARDING -> ManageTokensSource.ONBOARDING + AppRoute.ManageTokens.Source.STORIES -> ManageTokensSource.STORIES + } + route.asComponentChild( contextProvider = contextProvider(route, contextFactory), - params = ManageTokensComponent.Params(route.userWalletId), + params = ManageTokensComponent.Params(route.userWalletId, source), componentFactory = manageTokensComponentFactory, ) } else { diff --git a/common/routing/src/main/kotlin/com/tangem/common/routing/AppRoute.kt b/common/routing/src/main/kotlin/com/tangem/common/routing/AppRoute.kt index 0d8cee2df5..88a0c3f804 100644 --- a/common/routing/src/main/kotlin/com/tangem/common/routing/AppRoute.kt +++ b/common/routing/src/main/kotlin/com/tangem/common/routing/AppRoute.kt @@ -179,9 +179,16 @@ sealed class AppRoute(val path: String) : Route { @Serializable data class ManageTokens( + val source: Source, val userWalletId: UserWalletId? = null, - ) : AppRoute(path = "/manage_tokens/$userWalletId"), RouteBundleParams { + ) : AppRoute(path = "${source.name.lowercase()}/manage_tokens/$userWalletId"), RouteBundleParams { override fun getBundle(): Bundle = bundle(serializer()) + + enum class Source { + STORIES, + ONBOARDING, + SETTINGS, + } } @Serializable diff --git a/core/analytics/build.gradle.kts b/core/analytics/build.gradle.kts index c61c8e7af2..99c29cd55d 100644 --- a/core/analytics/build.gradle.kts +++ b/core/analytics/build.gradle.kts @@ -11,7 +11,7 @@ dependencies { kapt(deps.hilt.kapt) /** Analytics - Models */ - implementation(projects.core.analytics.models) + api(projects.core.analytics.models) /** Domain */ implementation(projects.domain.analytics) diff --git a/core/analytics/models/src/main/java/com/tangem/core/analytics/models/AnalyticsParam.kt b/core/analytics/models/src/main/java/com/tangem/core/analytics/models/AnalyticsParam.kt index f9911c81f8..5fc6c4e12c 100644 --- a/core/analytics/models/src/main/java/com/tangem/core/analytics/models/AnalyticsParam.kt +++ b/core/analytics/models/src/main/java/com/tangem/core/analytics/models/AnalyticsParam.kt @@ -22,9 +22,19 @@ sealed class AnalyticsParam { data object Closed : RateApp("Close") } - sealed class OnOffState(val value: String) { - data object On : OnOffState("On") - data object Off : OnOffState("Off") + enum class OnOffState(val value: String) { + On("On"), + Off("Off"), + ; + + companion object { + + fun from(enabled: Boolean): String { + val state = if (enabled) On else Off + + return state.value + } + } } sealed class OrganizeSortType(val value: String) { @@ -135,6 +145,22 @@ sealed class AnalyticsParam { class SingleCurrency(currencyName: String) : WalletType(currencyName) } + enum class Validation(val value: String) { + + OK(value = "Ok"), + ERROR(value = "Error"), + ; + + companion object { + + fun from(isValid: Boolean): String { + val status = if (isValid) OK else ERROR + + return status.value + } + } + } + companion object Key { const val BLOCKCHAIN = "blockchain" const val TOKEN_PARAM = "Token" @@ -158,6 +184,9 @@ sealed class AnalyticsParam { const val VALIDATION = "Validation" const val BLOCKCHAIN_EXCEPTION_HOST = "exception_host" const val BLOCKCHAIN_SELECTED_HOST = "selected_host" + const val INPUT = "Input" + const val COUNT = "Count" + const val DERIVATION = "Derivation" // region swap const val TOKEN_CATEGORY = "Token" diff --git a/core/analytics/src/main/java/com/tangem/core/analytics/DummyAnalyticsEventHandler.kt b/core/analytics/src/main/java/com/tangem/core/analytics/DummyAnalyticsEventHandler.kt new file mode 100644 index 0000000000..170158e0ec --- /dev/null +++ b/core/analytics/src/main/java/com/tangem/core/analytics/DummyAnalyticsEventHandler.kt @@ -0,0 +1,11 @@ +package com.tangem.core.analytics + +import com.tangem.core.analytics.api.AnalyticsEventHandler +import com.tangem.core.analytics.models.AnalyticsEvent + +class DummyAnalyticsEventHandler : AnalyticsEventHandler { + + override fun send(event: AnalyticsEvent) { + /* no-op */ + } +} \ No newline at end of file diff --git a/core/res/src/main/res/values-ru/strings.xml b/core/res/src/main/res/values-ru/strings.xml index dbbe6ece73..c7e2221f9d 100644 --- a/core/res/src/main/res/values-ru/strings.xml +++ b/core/res/src/main/res/values-ru/strings.xml @@ -349,6 +349,7 @@ Кошелёк не поддерживает более одной сети Чтобы купить, обменять или получить данный токен, вам нужно добавить его к себе в портфель Этот актив недоступен + Этот токен не доступен для данного кошелька Добавить в портфель Добавить Доступные сети diff --git a/core/res/src/main/res/values/strings.xml b/core/res/src/main/res/values/strings.xml index 210c100c90..a75c4fce68 100644 --- a/core/res/src/main/res/values/strings.xml +++ b/core/res/src/main/res/values/strings.xml @@ -345,6 +345,7 @@ The wallet doesn\'t support more than one network To buy, exchange, or receive this asset, add it to your portfolio This asset is not available + This asset is not available for this wallet Add to portfolio Add Available networks diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/appbar/TopAppBarButton.kt b/core/ui/src/main/java/com/tangem/core/ui/components/appbar/TopAppBarButton.kt index 160e0148da..0f2f4b0523 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/appbar/TopAppBarButton.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/appbar/TopAppBarButton.kt @@ -13,6 +13,7 @@ import com.tangem.core.ui.res.TangemTheme @Composable fun TopAppBarButton(button: TopAppBarButtonUM, tint: Color, modifier: Modifier = Modifier) { IconButton( + enabled = button.enabled, modifier = modifier.size(TangemTheme.dimens.size32), onClick = button.onIconClicked, ) { diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/appbar/models/TopAppBarButtonUM.kt b/core/ui/src/main/java/com/tangem/core/ui/components/appbar/models/TopAppBarButtonUM.kt index 61e3f25d39..7104e25cf9 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/appbar/models/TopAppBarButtonUM.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/appbar/models/TopAppBarButtonUM.kt @@ -6,14 +6,18 @@ import com.tangem.core.ui.R data class TopAppBarButtonUM( @DrawableRes val iconRes: Int, val onIconClicked: () -> Unit, + val enabled: Boolean = true, ) { @Suppress("FunctionName") companion object { - fun Back(onBackClicked: () -> Unit) = TopAppBarButtonUM( + fun Back(onBackClicked: () -> Unit) = Back(true, onBackClicked) + + fun Back(enabled: Boolean = true, onBackClicked: () -> Unit) = TopAppBarButtonUM( iconRes = R.drawable.ic_back_24, onIconClicked = onBackClicked, + enabled = enabled, ) } } \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/transactions/Transaction.kt b/core/ui/src/main/java/com/tangem/core/ui/components/transactions/Transaction.kt index 91efa949e6..af0ff0f89b 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/transactions/Transaction.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/transactions/Transaction.kt @@ -20,8 +20,10 @@ import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider +import androidx.constraintlayout.compose.ChainStyle import androidx.constraintlayout.compose.ConstraintLayout import androidx.constraintlayout.compose.Dimension +import androidx.constraintlayout.compose.Visibility import com.tangem.core.ui.R import com.tangem.core.ui.components.CircleShimmer import com.tangem.core.ui.components.RectangleShimmer @@ -68,6 +70,9 @@ fun Transaction(state: TransactionState, isBalanceHidden: Boolean, modifier: Mod ConstraintLayout(modifier = Modifier.fillMaxWidth()) { val (iconItem, titleItem, subtitleItem, amountItem, timestampItem) = createRefs() + createVerticalChain(titleItem, subtitleItem, chainStyle = ChainStyle.Spread) + createVerticalChain(amountItem, timestampItem, chainStyle = ChainStyle.Spread) + Icon( state = state, modifier = Modifier @@ -102,6 +107,7 @@ fun Transaction(state: TransactionState, isBalanceHidden: Boolean, modifier: Mod end = TangemTheme.dimens.spacing4, ) .constrainAs(subtitleItem) { + visibility = state.isGoneIf { subtitle.isNullOrEmpty() } top.linkTo(titleItem.bottom) bottom.linkTo(parent.bottom) start.linkTo(iconItem.end) @@ -114,6 +120,7 @@ fun Transaction(state: TransactionState, isBalanceHidden: Boolean, modifier: Mod state = state, isBalanceHidden = isBalanceHidden, modifier = Modifier.constrainAs(amountItem) { + visibility = state.isGoneIf { amount.isEmpty() } top.linkTo(parent.top) bottom.linkTo(timestampItem.top) start.linkTo(titleItem.end) @@ -315,6 +322,10 @@ private fun LockedContent(modifier: Modifier = Modifier) { ) } +private fun TransactionState.isGoneIf(goneCondition: TransactionState.Content.() -> Boolean): Visibility { + return if ((this as? TransactionState.Content)?.goneCondition() == true) Visibility.Gone else Visibility.Visible +} + @Preview(showBackground = true, widthDp = 368) @Preview(showBackground = true, widthDp = 368, uiMode = Configuration.UI_MODE_NIGHT_YES) @Composable diff --git a/core/ui/src/main/res/drawable/ic_transaction_history_claim_rewards_24.xml b/core/ui/src/main/res/drawable/ic_transaction_history_claim_rewards_24.xml new file mode 100644 index 0000000000..8fb3cae98f --- /dev/null +++ b/core/ui/src/main/res/drawable/ic_transaction_history_claim_rewards_24.xml @@ -0,0 +1,11 @@ + + + + + diff --git a/core/ui/src/main/res/drawable/ic_transaction_history_staking.xml b/core/ui/src/main/res/drawable/ic_transaction_history_staking_24.xml similarity index 100% rename from core/ui/src/main/res/drawable/ic_transaction_history_staking.xml rename to core/ui/src/main/res/drawable/ic_transaction_history_staking_24.xml diff --git a/core/ui/src/main/res/drawable/ic_transaction_history_unstaking.xml b/core/ui/src/main/res/drawable/ic_transaction_history_unstaking_24.xml similarity index 100% rename from core/ui/src/main/res/drawable/ic_transaction_history_unstaking.xml rename to core/ui/src/main/res/drawable/ic_transaction_history_unstaking_24.xml diff --git a/domain/legacy/src/main/java/com/tangem/domain/walletmanager/utils/SdkTransactionHistoryItemConverter.kt b/domain/legacy/src/main/java/com/tangem/domain/walletmanager/utils/SdkTransactionHistoryItemConverter.kt index 0df50f5899..220172ebc0 100644 --- a/domain/legacy/src/main/java/com/tangem/domain/walletmanager/utils/SdkTransactionHistoryItemConverter.kt +++ b/domain/legacy/src/main/java/com/tangem/domain/walletmanager/utils/SdkTransactionHistoryItemConverter.kt @@ -45,22 +45,25 @@ internal class SdkTransactionHistoryItemConverter( private fun SdkTransactionHistoryItem.AddressType.toDomain(): TxHistoryItem.AddressType = when (this) { is SdkTransactionHistoryItem.AddressType.Contract -> TxHistoryItem.AddressType.Contract(address) is SdkTransactionHistoryItem.AddressType.User -> TxHistoryItem.AddressType.User(address) + is SdkTransactionHistoryItem.AddressType.Validator -> TxHistoryItem.AddressType.Validator(address) } - private fun SdkTransactionHistoryItem.extractInteractionAddressType(): TxHistoryItem.InteractionAddressType { - return when (type) { + private fun SdkTransactionHistoryItem.extractInteractionAddressType(): TxHistoryItem.InteractionAddressType? { + return when (val transactionType = type) { SdkTransactionHistoryItem.TransactionType.Transfer -> if (isOutgoing) { mapToInteractionAddressType(destinationType = destinationType) } else { mapToInteractionAddressType(sourceType = sourceType) } - is SdkTransactionHistoryItem.TransactionType.TronStakingTransactionType -> { - TxHistoryItem.InteractionAddressType.Staking - } is SdkTransactionHistoryItem.TransactionType.ContractMethod, is SdkTransactionHistoryItem.TransactionType.ContractMethodName, - -> mapToInteractionAddressType(destinationType) + -> mapToInteractionAddressType(destinationType = destinationType) + + is SdkTransactionHistoryItem.TransactionType.TronStakingTransactionType.VoteWitnessContract -> { + TxHistoryItem.InteractionAddressType.Validator(address = transactionType.validatorAddress) + } + else -> null } } @@ -78,6 +81,9 @@ internal class SdkTransactionHistoryItemConverter( is TransactionHistoryItem.AddressType.User -> TxHistoryItem.InteractionAddressType.User( destinationType.addressType.address, ) + is TransactionHistoryItem.AddressType.Validator -> TxHistoryItem.InteractionAddressType.Validator( + destinationType.addressType.address, + ) } } } @@ -89,7 +95,9 @@ internal class SdkTransactionHistoryItemConverter( is TransactionHistoryItem.SourceType.Multiple -> TxHistoryItem.InteractionAddressType.Multiple( sourceType.addresses, ) - is TransactionHistoryItem.SourceType.Single -> TxHistoryItem.InteractionAddressType.User(sourceType.address) + is TransactionHistoryItem.SourceType.Single -> { + TxHistoryItem.InteractionAddressType.User(sourceType.address) + } } } } \ No newline at end of file diff --git a/domain/legacy/src/main/java/com/tangem/domain/walletmanager/utils/SdkTransactionTypeConverter.kt b/domain/legacy/src/main/java/com/tangem/domain/walletmanager/utils/SdkTransactionTypeConverter.kt index e62cd5b7eb..306c18cf6a 100644 --- a/domain/legacy/src/main/java/com/tangem/domain/walletmanager/utils/SdkTransactionTypeConverter.kt +++ b/domain/legacy/src/main/java/com/tangem/domain/walletmanager/utils/SdkTransactionTypeConverter.kt @@ -27,9 +27,12 @@ internal class SdkTransactionTypeConverter( TxHistoryItem.TransactionType.TronStakingTransactionType.Unstake } is TransactionType.TronStakingTransactionType.VoteWitnessContract -> { - TxHistoryItem.TransactionType.TronStakingTransactionType.Vote + TxHistoryItem.TransactionType.TronStakingTransactionType.Vote(value.validatorAddress) } is TransactionType.TronStakingTransactionType.WithdrawBalanceContract -> { + TxHistoryItem.TransactionType.TronStakingTransactionType.ClaimRewards + } + is TransactionType.TronStakingTransactionType.WithdrawExpireUnfreezeContract -> { TxHistoryItem.TransactionType.TronStakingTransactionType.Withdraw } } diff --git a/domain/txhistory/models/src/main/kotlin/com/tangem/domain/txhistory/models/TxHistoryItem.kt b/domain/txhistory/models/src/main/kotlin/com/tangem/domain/txhistory/models/TxHistoryItem.kt index 68677bdd63..f96905a1ed 100644 --- a/domain/txhistory/models/src/main/kotlin/com/tangem/domain/txhistory/models/TxHistoryItem.kt +++ b/domain/txhistory/models/src/main/kotlin/com/tangem/domain/txhistory/models/TxHistoryItem.kt @@ -8,7 +8,7 @@ data class TxHistoryItem( val isOutgoing: Boolean, val destinationType: DestinationType, val sourceType: SourceType, - val interactionAddressType: InteractionAddressType, + val interactionAddressType: InteractionAddressType?, val status: TransactionStatus, val type: TransactionType, val amount: BigDecimal, @@ -30,6 +30,7 @@ data class TxHistoryItem( data class User(override val address: String) : AddressType() data class Contract(override val address: String) : AddressType() + data class Validator(override val address: String) : AddressType() } sealed interface TransactionType { @@ -40,10 +41,11 @@ data class TxHistoryItem( data class Operation(val name: String) : TransactionType sealed interface TronStakingTransactionType : TransactionType { - data object Vote : TronStakingTransactionType - data object Withdraw : TronStakingTransactionType + data class Vote(val validatorAddress: String) : TronStakingTransactionType + data object ClaimRewards : TronStakingTransactionType data object Stake : TronStakingTransactionType data object Unstake : TronStakingTransactionType + data object Withdraw : TronStakingTransactionType } } @@ -54,7 +56,7 @@ data class TxHistoryItem( } sealed class InteractionAddressType { - data object Staking : InteractionAddressType() + data class Validator(val address: String) : InteractionAddressType() data class User(val address: String) : InteractionAddressType() data class Contract(val address: String) : InteractionAddressType() data class Multiple(val addresses: List) : InteractionAddressType() diff --git a/features/manage-tokens/api/src/main/kotlin/com/tangem/features/managetokens/component/AddCustomTokenComponent.kt b/features/manage-tokens/api/src/main/kotlin/com/tangem/features/managetokens/component/AddCustomTokenComponent.kt index 4539527ce5..0eebb14aa3 100644 --- a/features/manage-tokens/api/src/main/kotlin/com/tangem/features/managetokens/component/AddCustomTokenComponent.kt +++ b/features/manage-tokens/api/src/main/kotlin/com/tangem/features/managetokens/component/AddCustomTokenComponent.kt @@ -8,6 +8,7 @@ interface AddCustomTokenComponent : ComposableBottomSheetComponent { data class Params( val userWalletId: UserWalletId, + val source: ManageTokensSource, val onDismiss: () -> Unit, val onCurrencyAdded: () -> Unit, ) diff --git a/features/manage-tokens/api/src/main/kotlin/com/tangem/features/managetokens/component/ManageTokensComponent.kt b/features/manage-tokens/api/src/main/kotlin/com/tangem/features/managetokens/component/ManageTokensComponent.kt index c327e0ac05..abb4b91de9 100644 --- a/features/manage-tokens/api/src/main/kotlin/com/tangem/features/managetokens/component/ManageTokensComponent.kt +++ b/features/manage-tokens/api/src/main/kotlin/com/tangem/features/managetokens/component/ManageTokensComponent.kt @@ -6,7 +6,10 @@ import com.tangem.domain.wallets.models.UserWalletId interface ManageTokensComponent : ComposableContentComponent { - data class Params(val userWalletId: UserWalletId?) + data class Params( + val userWalletId: UserWalletId?, + val source: ManageTokensSource, + ) interface Factory : ComponentFactory } \ No newline at end of file diff --git a/features/manage-tokens/api/src/main/kotlin/com/tangem/features/managetokens/component/ManageTokensSource.kt b/features/manage-tokens/api/src/main/kotlin/com/tangem/features/managetokens/component/ManageTokensSource.kt new file mode 100644 index 0000000000..b55fac8e8c --- /dev/null +++ b/features/manage-tokens/api/src/main/kotlin/com/tangem/features/managetokens/component/ManageTokensSource.kt @@ -0,0 +1,7 @@ +package com.tangem.features.managetokens.component + +enum class ManageTokensSource { + STORIES, + ONBOARDING, + SETTINGS, +} \ No newline at end of file diff --git a/features/manage-tokens/impl/build.gradle.kts b/features/manage-tokens/impl/build.gradle.kts index a506043ad2..f018c97984 100644 --- a/features/manage-tokens/impl/build.gradle.kts +++ b/features/manage-tokens/impl/build.gradle.kts @@ -20,6 +20,7 @@ dependencies { implementation(projects.core.ui) implementation(projects.common.routing) implementation(projects.core.featuretoggles) + implementation(projects.core.analytics) /* Project - Domain */ implementation(projects.domain.manageTokens) diff --git a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/analytics/CustomTokenAnalyticsEvent.kt b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/analytics/CustomTokenAnalyticsEvent.kt new file mode 100644 index 0000000000..b753960993 --- /dev/null +++ b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/analytics/CustomTokenAnalyticsEvent.kt @@ -0,0 +1,77 @@ +package com.tangem.features.managetokens.analytics + +import com.tangem.core.analytics.models.AnalyticsEvent +import com.tangem.core.analytics.models.AnalyticsParam +import com.tangem.features.managetokens.component.ManageTokensSource + +internal sealed class CustomTokenAnalyticsEvent( + event: String, + params: Map = mapOf(), +) : AnalyticsEvent( + category = "Manage Tokens / Custom", + event = event, + params = params, +) { + + class ScreenOpened(source: ManageTokensSource) : CustomTokenAnalyticsEvent( + event = "Custom Token Screen Opened", + params = mapOf(AnalyticsParam.Key.SOURCE to source.name), + ) + + class CustomTokenWasAdded( + currencySymbol: String, + derivationPath: String, + source: ManageTokensSource, + ) : CustomTokenAnalyticsEvent( + event = "Custom Token Was Added", + params = mapOf( + AnalyticsParam.Key.TOKEN_PARAM to currencySymbol, + AnalyticsParam.Key.DERIVATION to derivationPath, + AnalyticsParam.Key.SOURCE to source.name, + ), + ) + + class NetworkSelected(networkName: String, source: ManageTokensSource) : CustomTokenAnalyticsEvent( + event = "Custom Token Network Selected", + params = mapOf( + AnalyticsParam.Key.BLOCKCHAIN to networkName, + AnalyticsParam.Key.SOURCE to source.name, + ), + ) + + class DerivationSelected(derivationName: String, source: ManageTokensSource) : CustomTokenAnalyticsEvent( + event = "Custom Token Derivation Selected", + params = mapOf( + AnalyticsParam.Key.DERIVATION to derivationName, + AnalyticsParam.Key.SOURCE to source.name, + ), + ) + + class Address(isValid: Boolean, source: ManageTokensSource) : CustomTokenAnalyticsEvent( + event = "Custom Token Address", + params = mapOf( + AnalyticsParam.Key.VALIDATION to AnalyticsParam.Validation.from(isValid), + AnalyticsParam.Key.SOURCE to source.name, + ), + ) + + class Name(source: ManageTokensSource) : CustomTokenAnalyticsEvent( + event = "Custom Token Name", + params = mapOf(AnalyticsParam.Key.SOURCE to source.name), + ) + + class Symbol(source: ManageTokensSource) : CustomTokenAnalyticsEvent( + event = "Custom Token Symbol", + params = mapOf(AnalyticsParam.Key.SOURCE to source.name), + ) + + class Decimals(source: ManageTokensSource) : CustomTokenAnalyticsEvent( + event = "Custom Token Decimals", + params = mapOf(AnalyticsParam.Key.SOURCE to source.name), + ) + + class ButtonCustomToken(source: ManageTokensSource) : CustomTokenAnalyticsEvent( + event = "Button - Custom Token", + params = mapOf(AnalyticsParam.Key.SOURCE to source.name), + ) +} \ No newline at end of file diff --git a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/analytics/ManageTokensAnalyticEvent.kt b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/analytics/ManageTokensAnalyticEvent.kt new file mode 100644 index 0000000000..bc70e1e736 --- /dev/null +++ b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/analytics/ManageTokensAnalyticEvent.kt @@ -0,0 +1,55 @@ +package com.tangem.features.managetokens.analytics + +import com.tangem.core.analytics.models.AnalyticsEvent +import com.tangem.core.analytics.models.AnalyticsParam +import com.tangem.features.managetokens.component.ManageTokensSource + +internal sealed class ManageTokensAnalyticEvent( + event: String, + params: Map = mapOf(), +) : AnalyticsEvent( + category = "ManageTokens", + event = event, + params = params, +) { + + class ScreenOpened(source: ManageTokensSource) : ManageTokensAnalyticEvent( + event = "Manage Tokens Screen Opened", + params = mapOf(AnalyticsParam.Key.SOURCE to source.name), + ) + + class TokensIsNotFound(query: String, source: ManageTokensSource) : ManageTokensAnalyticEvent( + event = "Token Is Not Found", + params = mapOf( + AnalyticsParam.Key.INPUT to query, + AnalyticsParam.Key.SOURCE to source.name, + ), + ) + + class TokenSwitcherChanged( + tokenSymbol: String, + isSelected: Boolean, + source: ManageTokensSource, + ) : ManageTokensAnalyticEvent( + event = "Token Switcher Changed", + params = mapOf( + AnalyticsParam.Key.TOKEN_PARAM to tokenSymbol, + AnalyticsParam.Key.STATE to AnalyticsParam.OnOffState.from(isSelected), + AnalyticsParam.Key.SOURCE to source.name, + ), + ) + + class TokenAdded( + tokensCount: Int, + source: ManageTokensSource, + ) : ManageTokensAnalyticEvent( + event = "Token Added", + params = mapOf( + AnalyticsParam.Key.COUNT to tokensCount.toString(), + AnalyticsParam.Key.SOURCE to source.name, + ), + ) + + // TODO: Will be used later + data object ButtonLater : ManageTokensAnalyticEvent(event = "Button - Later") +} \ No newline at end of file diff --git a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/component/CustomTokenFormComponent.kt b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/component/CustomTokenFormComponent.kt index b7c219e560..e2f093a62e 100644 --- a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/component/CustomTokenFormComponent.kt +++ b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/component/CustomTokenFormComponent.kt @@ -14,6 +14,7 @@ internal interface CustomTokenFormComponent : ComposableContentComponent { val network: SelectedNetwork, val derivationPath: SelectedDerivationPath?, val formValues: CustomTokenFormValues, + val source: ManageTokensSource, val onSelectNetworkClick: (CustomTokenFormValues) -> Unit, val onSelectDerivationPathClick: (CustomTokenFormValues) -> Unit, val onCurrencyAdded: () -> Unit, diff --git a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/component/impl/DefaultAddCustomTokenComponent.kt b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/component/impl/DefaultAddCustomTokenComponent.kt index bde22e12c6..e9444a6d3d 100644 --- a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/component/impl/DefaultAddCustomTokenComponent.kt +++ b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/component/impl/DefaultAddCustomTokenComponent.kt @@ -8,10 +8,12 @@ import com.arkivanov.decompose.extensions.compose.jetpack.stack.Children import com.arkivanov.decompose.extensions.compose.jetpack.stack.animation.stackAnimation import com.arkivanov.decompose.extensions.compose.jetpack.subscribeAsState import com.arkivanov.decompose.router.stack.* +import com.tangem.core.analytics.api.AnalyticsEventHandler import com.tangem.core.decompose.context.AppComponentContext import com.tangem.core.decompose.context.childByContext import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig import com.tangem.core.ui.decompose.ComposableContentComponent +import com.tangem.features.managetokens.analytics.CustomTokenAnalyticsEvent import com.tangem.features.managetokens.component.AddCustomTokenComponent import com.tangem.features.managetokens.component.CustomTokenFormComponent import com.tangem.features.managetokens.component.CustomTokenSelectorComponent @@ -29,6 +31,7 @@ internal class DefaultAddCustomTokenComponent @AssistedInject constructor( @Assisted private val params: AddCustomTokenComponent.Params, private val selectorComponentFactory: CustomTokenSelectorComponent.Factory, private val formComponentFactory: CustomTokenFormComponent.Factory, + private val analyticsEventHandler: AnalyticsEventHandler, ) : AddCustomTokenComponent, AppComponentContext by context { private val navigation = StackNavigation() @@ -45,6 +48,10 @@ internal class DefaultAddCustomTokenComponent @AssistedInject constructor( childFactory = ::contentChild, ) + init { + analyticsEventHandler.send(CustomTokenAnalyticsEvent.ScreenOpened(params.source)) + } + override fun dismiss() { params.onDismiss() } @@ -85,9 +92,7 @@ internal class DefaultAddCustomTokenComponent @AssistedInject constructor( params = CustomTokenSelectorComponent.Params.NetworkSelector( userWalletId = config.userWalletId, selectedNetwork = null, - onNetworkSelected = { network -> - showForm(network = network) - }, + onNetworkSelected = ::changeSelectedNetwork, ), ) } @@ -97,9 +102,7 @@ internal class DefaultAddCustomTokenComponent @AssistedInject constructor( params = CustomTokenSelectorComponent.Params.NetworkSelector( userWalletId = config.userWalletId, selectedNetwork = config.selectedNetwork, - onNetworkSelected = { network -> - showForm(network = network) - }, + onNetworkSelected = ::changeSelectedNetwork, ), ) } @@ -112,9 +115,7 @@ internal class DefaultAddCustomTokenComponent @AssistedInject constructor( "Network is not selected" }, selectedDerivationPath = config.selectedDerivationPath, - onDerivationPathSelected = { derivationPath -> - showForm(derivationPath = derivationPath) - }, + onDerivationPathSelected = ::changeDerivationPath, ), ) } @@ -128,6 +129,7 @@ internal class DefaultAddCustomTokenComponent @AssistedInject constructor( }, derivationPath = config.selectedDerivationPath, formValues = config.formValues, + source = params.source, onSelectNetworkClick = ::showNetworkSelector, onSelectDerivationPathClick = ::showDerivationPathSelector, onCurrencyAdded = ::dismissAndNotify, @@ -136,6 +138,26 @@ internal class DefaultAddCustomTokenComponent @AssistedInject constructor( } } + private fun changeSelectedNetwork(network: SelectedNetwork) { + val event = CustomTokenAnalyticsEvent.NetworkSelected( + networkName = network.name, + source = params.source, + ) + analyticsEventHandler.send(event) + + showForm(network = network) + } + + private fun changeDerivationPath(derivationPath: SelectedDerivationPath) { + val event = CustomTokenAnalyticsEvent.DerivationSelected( + derivationName = derivationPath.name, + source = params.source, + ) + analyticsEventHandler.send(event) + + showForm(derivationPath = derivationPath) + } + private fun showDerivationPathSelector(formValues: CustomTokenFormValues) { val currentConfig = contentStack.value.active.configuration diff --git a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/component/impl/DefaultCustomTokenDerivationInputComponent.kt b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/component/impl/DefaultCustomTokenDerivationInputComponent.kt index 0695290569..7066fa1307 100644 --- a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/component/impl/DefaultCustomTokenDerivationInputComponent.kt +++ b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/component/impl/DefaultCustomTokenDerivationInputComponent.kt @@ -7,7 +7,6 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle import arrow.core.getOrElse import com.tangem.core.decompose.context.AppComponentContext import com.tangem.core.ui.extensions.resourceReference -import com.tangem.core.ui.extensions.stringReference import com.tangem.domain.managetokens.ValidateDerivationPathUseCase import com.tangem.domain.managetokens.model.exceptoin.DerivationPathValidationException import com.tangem.domain.tokens.model.Network @@ -111,7 +110,8 @@ internal class DefaultCustomTokenDerivationInputComponent @AssistedInject constr val model = SelectedDerivationPath( id = null, value = Network.DerivationPath.Custom(value), - networkName = stringReference(value = value), + name = value, + isDefault = false, ) params.onConfirm(model) diff --git a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/component/impl/DefaultManageTokensComponent.kt b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/component/impl/DefaultManageTokensComponent.kt index 2b513d23cb..9a8b81b83a 100644 --- a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/component/impl/DefaultManageTokensComponent.kt +++ b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/component/impl/DefaultManageTokensComponent.kt @@ -24,7 +24,7 @@ import dagger.assisted.AssistedInject internal class DefaultManageTokensComponent @AssistedInject constructor( @Assisted context: AppComponentContext, - @Assisted params: ManageTokensComponent.Params, + @Assisted private val params: ManageTokensComponent.Params, private val addCustomTokenComponentFactory: AddCustomTokenComponent.Factory, ) : ManageTokensComponent, AppComponentContext by context { @@ -61,6 +61,7 @@ internal class DefaultManageTokensComponent @AssistedInject constructor( context = childByContext(componentContext), params = AddCustomTokenComponent.Params( userWalletId = config.userWalletId, + source = params.source, onDismiss = model.bottomSheetNavigation::dismiss, onCurrencyAdded = model::reloadList, ), diff --git a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/component/preview/PreviewCustomTokenFormComponent.kt b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/component/preview/PreviewCustomTokenFormComponent.kt index fa909e5257..5d995e9b1c 100644 --- a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/component/preview/PreviewCustomTokenFormComponent.kt +++ b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/component/preview/PreviewCustomTokenFormComponent.kt @@ -1,5 +1,6 @@ package com.tangem.features.managetokens.component.preview +import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import com.tangem.core.ui.components.notifications.NotificationConfig @@ -13,6 +14,7 @@ import com.tangem.features.managetokens.impl.R import com.tangem.features.managetokens.ui.CustomTokenFormContent import kotlinx.collections.immutable.PersistentList import kotlinx.collections.immutable.persistentListOf +import kotlinx.collections.immutable.toPersistentMap internal class PreviewCustomTokenFormComponent( networkName: ClickableFieldUM = PreviewCustomTokenFormComponent.networkName, @@ -48,30 +50,40 @@ internal class PreviewCustomTokenFormComponent( onClick = {}, ) val tokenForm: CustomTokenFormUM.TokenFormUM = CustomTokenFormUM.TokenFormUM( - contractAddress = TextInputFieldUM( - label = resourceReference(R.string.custom_token_contract_address_input_title), - placeholder = stringReference(value = "0x000000000000000000000000000"), - value = "", - onValueChange = {}, - ), - name = TextInputFieldUM( - label = resourceReference(R.string.custom_token_name_input_title), - placeholder = stringReference(value = "E.g. USD Coin"), - value = "", - onValueChange = {}, - ), - symbol = TextInputFieldUM( - label = resourceReference(R.string.custom_token_token_symbol_input_title), - placeholder = stringReference(value = "E.g. USDC"), - value = "", - onValueChange = {}, - ), - decimals = TextInputFieldUM( - label = resourceReference(R.string.custom_token_decimals_input_title), - placeholder = stringReference(value = "8"), - value = "", - onValueChange = {}, - ), + fields = mapOf( + CustomTokenFormUM.TokenFormUM.Field.CONTRACT_ADDRESS to TextInputFieldUM( + label = resourceReference(R.string.custom_token_contract_address_input_title), + placeholder = stringReference(value = "0x000000000000000000000000000"), + value = "", + keyboardOptions = KeyboardOptions(), + onValueChange = {}, + onFocusChange = {}, + ), + CustomTokenFormUM.TokenFormUM.Field.NAME to TextInputFieldUM( + label = resourceReference(R.string.custom_token_name_input_title), + placeholder = stringReference(value = "E.g. USD Coin"), + value = "", + keyboardOptions = KeyboardOptions(), + onValueChange = {}, + onFocusChange = {}, + ), + CustomTokenFormUM.TokenFormUM.Field.SYMBOL to TextInputFieldUM( + label = resourceReference(R.string.custom_token_token_symbol_input_title), + placeholder = stringReference(value = "E.g. USDC"), + value = "", + keyboardOptions = KeyboardOptions(), + onValueChange = {}, + onFocusChange = {}, + ), + CustomTokenFormUM.TokenFormUM.Field.DECIMALS to TextInputFieldUM( + label = resourceReference(R.string.custom_token_decimals_input_title), + placeholder = stringReference(value = "8"), + value = "", + keyboardOptions = KeyboardOptions(), + onValueChange = {}, + onFocusChange = {}, + ), + ).toPersistentMap(), ) val notifications: PersistentList = persistentListOf( CustomTokenFormUM.NotificationUM( diff --git a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/component/preview/PreviewCustomTokenSelectorComponent.kt b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/component/preview/PreviewCustomTokenSelectorComponent.kt index 24b389d850..baef39e2e5 100644 --- a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/component/preview/PreviewCustomTokenSelectorComponent.kt +++ b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/component/preview/PreviewCustomTokenSelectorComponent.kt @@ -31,13 +31,14 @@ internal class PreviewCustomTokenSelectorComponent( val d = SelectedDerivationPath( id = Network.ID(index.toString()), value = Network.DerivationPath.Card("m/44'/0'/0'/0/$index"), - networkName = stringReference(value = "Network $index"), + name = "Network $index", + isDefault = false, ) DerivationPathUM( id = d.id?.value ?: "", value = d.value.value.orEmpty(), - networkName = d.networkName, + networkName = stringReference(d.name), isSelected = d.value == params.selectedDerivationPath?.value, onSelectedStateChange = { params.onDerivationPathSelected(d) }, ) @@ -45,7 +46,7 @@ internal class PreviewCustomTokenSelectorComponent( is Params.NetworkSelector -> { val n = SelectedNetwork( id = Network.ID(index.toString()), - name = stringReference(value = "Network $index"), + name = "Network $index", derivationPath = Network.DerivationPath.Card("m/44'/0'/0'/0/$index"), canHandleTokens = false, ) diff --git a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/entity/customtoken/AddCustomTokenConfig.kt b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/entity/customtoken/AddCustomTokenConfig.kt index 1c5de5c702..96e65e61d5 100644 --- a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/entity/customtoken/AddCustomTokenConfig.kt +++ b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/entity/customtoken/AddCustomTokenConfig.kt @@ -1,7 +1,6 @@ package com.tangem.features.managetokens.entity.customtoken import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfigContent -import com.tangem.core.ui.extensions.TextReference import com.tangem.domain.tokens.model.Network import com.tangem.domain.wallets.models.UserWalletId import kotlinx.serialization.Serializable @@ -27,7 +26,7 @@ internal data class AddCustomTokenConfig( @Serializable internal data class SelectedNetwork( val id: Network.ID, - val name: TextReference, + val name: String, val derivationPath: Network.DerivationPath, val canHandleTokens: Boolean, ) @@ -36,5 +35,6 @@ internal data class SelectedNetwork( internal data class SelectedDerivationPath( val id: Network.ID?, val value: Network.DerivationPath, - val networkName: TextReference, + val name: String, + val isDefault: Boolean, ) \ No newline at end of file diff --git a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/entity/customtoken/CustomTokenFormUM.kt b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/entity/customtoken/CustomTokenFormUM.kt index fe72248f0d..b4fbaea7be 100644 --- a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/entity/customtoken/CustomTokenFormUM.kt +++ b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/entity/customtoken/CustomTokenFormUM.kt @@ -1,8 +1,10 @@ package com.tangem.features.managetokens.entity.customtoken +import androidx.compose.foundation.text.KeyboardOptions import com.tangem.core.ui.components.notifications.NotificationConfig import com.tangem.core.ui.extensions.TextReference import kotlinx.collections.immutable.PersistentList +import kotlinx.collections.immutable.PersistentMap import kotlinx.collections.immutable.persistentListOf internal data class CustomTokenFormUM( @@ -16,12 +18,17 @@ internal data class CustomTokenFormUM( ) { data class TokenFormUM( - val contractAddress: TextInputFieldUM, - val name: TextInputFieldUM, - val symbol: TextInputFieldUM, - val decimals: TextInputFieldUM, + val fields: PersistentMap, val wasFilled: Boolean = false, - ) + ) { + + enum class Field { + CONTRACT_ADDRESS, + NAME, + SYMBOL, + DECIMALS, + } + } data class NotificationUM( val id: String, @@ -32,10 +39,13 @@ internal data class CustomTokenFormUM( internal data class TextInputFieldUM( val label: TextReference, val placeholder: TextReference, + val keyboardOptions: KeyboardOptions, val value: String = "", + val isFocused: Boolean = false, val error: TextReference? = null, val isEnabled: Boolean = true, val onValueChange: (String) -> Unit, + val onFocusChange: (Boolean) -> Unit, ) internal data class ClickableFieldUM( diff --git a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/entity/customtoken/CustomTokenFormValues.kt b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/entity/customtoken/CustomTokenFormValues.kt index 983eb8e61a..c391cfdc76 100644 --- a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/entity/customtoken/CustomTokenFormValues.kt +++ b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/entity/customtoken/CustomTokenFormValues.kt @@ -1,45 +1,35 @@ package com.tangem.features.managetokens.entity.customtoken -import com.tangem.domain.managetokens.model.AddCustomTokenForm import com.tangem.features.managetokens.entity.customtoken.CustomTokenFormUM.TokenFormUM +import com.tangem.features.managetokens.entity.customtoken.CustomTokenFormUM.TokenFormUM.Field +import kotlinx.collections.immutable.toPersistentMap import kotlinx.serialization.Serializable -@JvmInline @Serializable -internal value class CustomTokenFormValues private constructor(private val values: List) { - - constructor() : this(values = emptyList()) +internal class CustomTokenFormValues( + private val contractAddress: String = "", + private val name: String = "", + private val symbol: String = "", + private val decimals: String = "", +) { constructor(form: TokenFormUM?) : this( - values = if (form == null) { - emptyList() - } else { - listOf( - form.contractAddress.value, - form.name.value, - form.symbol.value, - form.decimals.value, - ) - }, + contractAddress = form?.fields?.get(Field.CONTRACT_ADDRESS)?.value.orEmpty(), + name = form?.fields?.get(Field.NAME)?.value.orEmpty(), + symbol = form?.fields?.get(Field.SYMBOL)?.value.orEmpty(), + decimals = form?.fields?.get(Field.DECIMALS)?.value.orEmpty(), ) fun fillValues(to: TokenFormUM): TokenFormUM = to.copy( - contractAddress = to.contractAddress.copy(value = values.getOrElse(index = 0) { "" }), - name = to.name.copy(value = values.getOrElse(index = 1) { "" }), - symbol = to.symbol.copy(value = values.getOrElse(index = 2) { "" }), - decimals = to.decimals.copy(value = values.getOrElse(index = 3) { "" }), - ) - - fun toDomainModel(): AddCustomTokenForm.Raw? { - return if (values.isEmpty()) { - null - } else { - AddCustomTokenForm.Raw( - contractAddress = values.getOrElse(index = 0) { "" }, - name = values.getOrElse(index = 1) { "" }, - symbol = values.getOrElse(index = 2) { "" }, - decimals = values.getOrElse(index = 3) { "" }, + fields = to.fields.mapValues { (key, field) -> + field.copy( + value = when (key) { + Field.CONTRACT_ADDRESS -> contractAddress + Field.NAME -> name + Field.SYMBOL -> symbol + Field.DECIMALS -> decimals + }, ) - } - } + }.toPersistentMap(), + ) } \ No newline at end of file diff --git a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/model/CustomTokenFormModel.kt b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/model/CustomTokenFormModel.kt index 4e678f1bb4..c1c6f9922e 100644 --- a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/model/CustomTokenFormModel.kt +++ b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/model/CustomTokenFormModel.kt @@ -2,6 +2,7 @@ package com.tangem.features.managetokens.model import androidx.compose.ui.res.stringResource import arrow.core.getOrElse +import com.tangem.core.analytics.api.AnalyticsEventHandler import com.tangem.core.decompose.di.ComponentScoped import com.tangem.core.decompose.model.Model import com.tangem.core.decompose.model.ParamsContainer @@ -15,22 +16,27 @@ import com.tangem.domain.managetokens.model.exceptoin.CustomTokenFormValidationE import com.tangem.domain.tokens.AddCryptoCurrenciesUseCase import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.domain.tokens.model.Network +import com.tangem.features.managetokens.analytics.CustomTokenAnalyticsEvent import com.tangem.features.managetokens.component.CustomTokenFormComponent import com.tangem.features.managetokens.entity.customtoken.ClickableFieldUM import com.tangem.features.managetokens.entity.customtoken.CustomTokenFormUM +import com.tangem.features.managetokens.entity.customtoken.CustomTokenFormUM.TokenFormUM.Field import com.tangem.features.managetokens.entity.customtoken.CustomTokenFormValues import com.tangem.features.managetokens.entity.customtoken.TextInputFieldUM import com.tangem.features.managetokens.impl.R +import com.tangem.features.managetokens.utils.CustomCurrencyFormBuilder import com.tangem.features.managetokens.utils.CustomCurrencyValidator import com.tangem.features.managetokens.utils.mapper.mapToDomainModel import com.tangem.features.managetokens.utils.ui.* import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import kotlinx.collections.immutable.mutate import kotlinx.coroutines.FlowPreview import kotlinx.coroutines.flow.* import kotlinx.coroutines.launch import timber.log.Timber import javax.inject.Inject +@Suppress("LongParameterList") @ComponentScoped internal class CustomTokenFormModel @Inject constructor( override val dispatchers: CoroutineDispatcherProvider, @@ -38,6 +44,8 @@ internal class CustomTokenFormModel @Inject constructor( private val addCryptoCurrenciesUseCase: AddCryptoCurrenciesUseCase, private val derivePublicKeysUseCase: DerivePublicKeysUseCase, private val messageSender: UiMessageSender, + private val customTokenFormManager: CustomCurrencyFormBuilder, + private val analyticsEventHandler: AnalyticsEventHandler, paramsContainer: ParamsContainer, ) : Model() { @@ -68,20 +76,23 @@ internal class CustomTokenFormModel @Inject constructor( return CustomTokenFormUM( networkName = ClickableFieldUM( label = resourceReference(R.string.custom_token_network_input_title), - value = params.network.name, + value = stringReference(params.network.name), onClick = ::selectNetwork, ), tokenForm = if (params.network.canHandleTokens) { - getInitialTokenForm() + customTokenFormManager.buildForm( + updateFormFieldValue = ::updateFormFieldValue, + updateFormFieldFocus = ::updateFormFieldFocus, + ) } else { null }, derivationPath = ClickableFieldUM( label = resourceReference(R.string.custom_token_derivation_path), - value = if (params.derivationPath == null || params.derivationPath.id == params.network.id) { + value = if (params.derivationPath == null || params.derivationPath.isDefault) { resourceReference(R.string.custom_token_derivation_path_default) } else { - params.derivationPath.networkName + stringReference(params.derivationPath.name) }, onClick = ::selectDerivationPath, ), @@ -241,79 +252,52 @@ internal class CustomTokenFormModel @Inject constructor( } } - private fun getInitialTokenForm(): CustomTokenFormUM.TokenFormUM { - val formValues = params.formValues - - val form = CustomTokenFormUM.TokenFormUM( - contractAddress = TextInputFieldUM( - label = resourceReference(R.string.custom_token_contract_address_input_title), - placeholder = stringReference(CONTRACT_ADDRESS_PLACEHOLDER), - onValueChange = ::updateContractAddress, - ), - name = TextInputFieldUM( - label = resourceReference(R.string.custom_token_name_input_title), - placeholder = resourceReference(R.string.custom_token_name_input_placeholder), - onValueChange = ::updateTokenName, - ), - symbol = TextInputFieldUM( - label = resourceReference(R.string.custom_token_token_symbol_input_title), - placeholder = resourceReference(R.string.custom_token_token_symbol_input_placeholder), - onValueChange = ::updateTokenSymbol, - ), - decimals = TextInputFieldUM( - label = resourceReference(R.string.custom_token_decimals_input_title), - placeholder = stringReference(DECIMALS_PLACEHOLDER), - onValueChange = ::updateDecimals, - ), - ) - - return formValues.fillValues(form) - } - private fun getDerivationPath(): Network.DerivationPath { return params.derivationPath?.value ?: params.network.derivationPath } - private fun updateContractAddress(value: String) { + private fun updateFormFieldValue(field: Field, value: String) { state.update { state -> state.updateTokenForm { + val fieldValue = fields.getValue(field) + + if (!fieldValue.isEnabled) return@updateTokenForm this + + val updatedFieldValue = fieldValue.copy( + value = value, + ) + val updatedFields = fields.mutate { + it[field] = updatedFieldValue + } + copy( - contractAddress = contractAddress.updateValue(value), + fields = updatedFields, wasFilled = false, ) } } } - private fun updateTokenName(value: String) { + private fun updateFormFieldFocus(field: Field, isFocused: Boolean) { state.update { state -> state.updateTokenForm { - copy( - name = name.updateValue(value), - wasFilled = false, - ) - } - } - } + val fieldValue = fields.getValue(field) - private fun updateTokenSymbol(value: String) { - state.update { state -> - state.updateTokenForm { - copy( - symbol = symbol.updateValue(value), - wasFilled = false, - ) - } - } - } + if (!fieldValue.isEnabled) return@updateTokenForm this - private fun updateDecimals(value: String) { - state.update { state -> - state.updateTokenForm { - copy( - decimals = decimals.updateValue(value), - wasFilled = false, + val updatedFieldValue = fieldValue.copy( + isFocused = isFocused, ) + val updatedFields = fields.mutate { + it[field] = updatedFieldValue + } + + // Checking if a field is out of focus + if (fieldValue.isFocused && !isFocused && fieldValue.value.isNotEmpty()) { + sendFieldAnalyticsEvent(field, fieldValue) + } + + copy(fields = updatedFields) } } } @@ -337,6 +321,13 @@ internal class CustomTokenFormModel @Inject constructor( return@resource } + val event = CustomTokenAnalyticsEvent.CustomTokenWasAdded( + currencySymbol = currency.symbol, + derivationPath = currency.network.derivationPath.value.orEmpty(), + source = params.source, + ) + analyticsEventHandler.send(event) + derivePublicKeysUseCase(params.userWalletId, listOf(currency)).getOrElse { Timber.e(it, "Failed to derive public keys") showErrorDialog() @@ -360,8 +351,17 @@ internal class CustomTokenFormModel @Inject constructor( params.onSelectDerivationPathClick(CustomTokenFormValues(state.value.tokenForm)) } - private companion object { - const val CONTRACT_ADDRESS_PLACEHOLDER = "0x000000000000000000000000000..." - const val DECIMALS_PLACEHOLDER = "0" + private fun sendFieldAnalyticsEvent(field: Field, fieldValue: TextInputFieldUM) { + val event = when (field) { + Field.CONTRACT_ADDRESS -> CustomTokenAnalyticsEvent.Address( + isValid = fieldValue.error == null, + source = params.source, + ) + Field.NAME -> CustomTokenAnalyticsEvent.Name(params.source) + Field.SYMBOL -> CustomTokenAnalyticsEvent.Symbol(params.source) + Field.DECIMALS -> CustomTokenAnalyticsEvent.Decimals(params.source) + } + + analyticsEventHandler.send(event) } } \ No newline at end of file diff --git a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/model/CustomTokenSelectorModel.kt b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/model/CustomTokenSelectorModel.kt index 611c6a1fe9..998307191a 100644 --- a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/model/CustomTokenSelectorModel.kt +++ b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/model/CustomTokenSelectorModel.kt @@ -8,7 +8,6 @@ import com.tangem.core.decompose.model.Model import com.tangem.core.decompose.model.ParamsContainer import com.tangem.core.decompose.ui.UiMessageSender import com.tangem.core.ui.extensions.resourceReference -import com.tangem.core.ui.extensions.stringReference import com.tangem.core.ui.message.SnackbarMessage import com.tangem.domain.managetokens.GetSupportedNetworksUseCase import com.tangem.domain.tokens.model.Network @@ -90,7 +89,7 @@ internal class CustomTokenSelectorModel @Inject constructor( onSelectedStateChange = { val model = SelectedNetwork( id = network.id, - name = stringReference(network.name), + name = network.name, derivationPath = network.derivationPath, canHandleTokens = network.canHandleTokens, ) @@ -109,8 +108,9 @@ internal class CustomTokenSelectorModel @Inject constructor( onSelectedStateChange = { val model = SelectedDerivationPath( id = network.id, - networkName = resourceReference(R.string.custom_token_derivation_path_default), + name = network.name, value = network.derivationPath, + isDefault = true, ) selector.onDerivationPathSelected(model) @@ -133,8 +133,9 @@ internal class CustomTokenSelectorModel @Inject constructor( onSelectedStateChange = { val model = SelectedDerivationPath( id = network.id, - networkName = stringReference(network.name), + name = network.name, value = network.derivationPath, + isDefault = false, ) selector.onDerivationPathSelected(model) diff --git a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/model/ManageTokensModel.kt b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/model/ManageTokensModel.kt index 5c4d924942..b72993de69 100644 --- a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/model/ManageTokensModel.kt +++ b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/model/ManageTokensModel.kt @@ -1,7 +1,9 @@ package com.tangem.features.managetokens.model +import arrow.core.getOrElse import com.arkivanov.decompose.router.slot.SlotNavigation import com.arkivanov.decompose.router.slot.activate +import com.tangem.core.analytics.api.AnalyticsEventHandler import com.tangem.core.decompose.di.ComponentScoped import com.tangem.core.decompose.model.Model import com.tangem.core.decompose.model.ParamsContainer @@ -16,6 +18,8 @@ import com.tangem.core.ui.extensions.stringReference import com.tangem.core.ui.message.SnackbarMessage import com.tangem.domain.managetokens.SaveManagedTokensUseCase import com.tangem.domain.wallets.models.UserWalletId +import com.tangem.features.managetokens.analytics.CustomTokenAnalyticsEvent +import com.tangem.features.managetokens.analytics.ManageTokensAnalyticEvent import com.tangem.features.managetokens.component.ManageTokensComponent import com.tangem.features.managetokens.entity.item.CurrencyItemUM import com.tangem.features.managetokens.entity.managetokens.ManageTokensBottomSheetConfig @@ -35,6 +39,7 @@ import kotlinx.coroutines.launch import timber.log.Timber import javax.inject.Inject +@Suppress("LongParameterList") @ComponentScoped internal class ManageTokensModel @Inject constructor( override val dispatchers: CoroutineDispatcherProvider, @@ -42,6 +47,7 @@ internal class ManageTokensModel @Inject constructor( private val manageTokensListManager: ManageTokensListManager, private val messageSender: UiMessageSender, private val saveManagedTokensUseCase: SaveManagedTokensUseCase, + private val analyticsEventHandler: AnalyticsEventHandler, paramsContainer: ParamsContainer, ) : Model() { @@ -68,7 +74,7 @@ internal class ManageTokensModel @Inject constructor( observeSearchQueryChanges() modelScope.launch { - manageTokensListManager.launchPagination(params.userWalletId) + manageTokensListManager.launchPagination(params) } } @@ -79,6 +85,8 @@ internal class ManageTokensModel @Inject constructor( } private fun getInitialState(userWalletId: UserWalletId?): ManageTokensUM { + analyticsEventHandler.send(ManageTokensAnalyticEvent.ScreenOpened(params.source)) + return if (userWalletId == null) { createReadContentModel() } else { @@ -140,8 +148,7 @@ internal class ManageTokensModel @Inject constructor( state .distinctUntilChanged { old, new -> // It's also used to skip search activation to avoid searching an empty query - old.search.query == new.search.query && - (old.search.isActive == new.search.isActive || new.search.isActive) + old.search.query == new.search.query && new.search.isActive } .transform { state -> val query = state.search.query @@ -161,11 +168,19 @@ internal class ManageTokensModel @Inject constructor( } private fun updateItems(items: ImmutableList) { - state.update { state -> + val updatedState = state.updateAndGet { state -> state.copySealed( items = items, ) } + + if (updatedState.items.isEmpty() && updatedState.search.isActive) { + val event = ManageTokensAnalyticEvent.TokensIsNotFound( + query = updatedState.search.query, + source = params.source, + ) + analyticsEventHandler.send(event) + } } private fun updatePaginationStatus(status: PaginationStatus<*>) { @@ -234,7 +249,7 @@ internal class ManageTokensModel @Inject constructor( } private fun consumeScrollToTopEvent() { - this.state.update { state -> + state.update { state -> state.copySealed( scrollToTop = consumedEvent(), ) @@ -264,24 +279,33 @@ internal class ManageTokensModel @Inject constructor( } private fun navigateToAddCustomToken() { + analyticsEventHandler.send(CustomTokenAnalyticsEvent.ButtonCustomToken(params.source)) + params.userWalletId?.let { bottomSheetNavigation.activate(ManageTokensBottomSheetConfig.AddCustomToken(it)) } } - private fun saveChanges() { - modelScope.launch { - state.update { state -> state.copySealed(isSavingInProgress = true) } - saveManagedTokensUseCase.invoke( - userWalletId = requireNotNull(params.userWalletId), - currenciesToAdd = manageTokensListManager.currenciesToAdd.value, - currenciesToRemove = manageTokensListManager.currenciesToRemove.value, - ).fold( - ifLeft = { Timber.e(it, "Failed to save changes") }, - ifRight = { router.pop() }, - ) - state.update { state -> state.copySealed(isSavingInProgress = false) } + private fun saveChanges() = resource( + acquire = { state.update { state -> state.copySealed(isSavingInProgress = true) } }, + release = { state.update { state -> state.copySealed(isSavingInProgress = false) } }, + ) { + val event = ManageTokensAnalyticEvent.TokenAdded( + tokensCount = manageTokensListManager.currenciesToAdd.value.values.sumOf { it.size }, + source = params.source, + ) + analyticsEventHandler.send(event) + + saveManagedTokensUseCase( + userWalletId = requireNotNull(params.userWalletId), + currenciesToAdd = manageTokensListManager.currenciesToAdd.value, + currenciesToRemove = manageTokensListManager.currenciesToRemove.value, + ).getOrElse { + Timber.e(it, "Failed to save changes") + return@resource } + + router.pop() } private fun searchCurrencies(query: String) { diff --git a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/ui/AddCustomTokenBottomSheet.kt b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/ui/AddCustomTokenBottomSheet.kt index 69ea2418d6..c8fc9b7dd7 100644 --- a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/ui/AddCustomTokenBottomSheet.kt +++ b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/ui/AddCustomTokenBottomSheet.kt @@ -16,7 +16,6 @@ import com.tangem.core.ui.components.bottomsheets.TangemBottomSheet import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetTitle import com.tangem.core.ui.extensions.resourceReference -import com.tangem.core.ui.extensions.stringReference import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreview import com.tangem.domain.tokens.model.Network @@ -102,7 +101,7 @@ private class AddCustomTokenComponentPreviewProvider : PreviewParameterProvider< popBack = {}, selectedNetwork = SelectedNetwork( id = Network.ID(value = "1"), - name = stringReference("Ethereum"), + name = "Ethereum", derivationPath = Network.DerivationPath.None, canHandleTokens = false, ), @@ -115,7 +114,7 @@ private class AddCustomTokenComponentPreviewProvider : PreviewParameterProvider< popBack = {}, selectedNetwork = SelectedNetwork( id = Network.ID(value = "0"), - name = stringReference("Ethereum"), + name = "Ethereum", derivationPath = Network.DerivationPath.None, canHandleTokens = false, ), @@ -129,7 +128,8 @@ private class AddCustomTokenComponentPreviewProvider : PreviewParameterProvider< selectedDerivationPath = SelectedDerivationPath( id = Network.ID(value = "0"), value = Network.DerivationPath.None, - networkName = stringReference("Ethereum"), + name = "Ethereum", + isDefault = false, ), ), ), diff --git a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/ui/CustomTokenFormContent.kt b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/ui/CustomTokenFormContent.kt index 3565f4c326..04e750f95c 100644 --- a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/ui/CustomTokenFormContent.kt +++ b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/ui/CustomTokenFormContent.kt @@ -7,19 +7,16 @@ import androidx.compose.foundation.background import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.* import androidx.compose.foundation.rememberScrollState -import androidx.compose.foundation.text.KeyboardActions -import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.foundation.verticalScroll import androidx.compose.material3.Text -import androidx.compose.runtime.* +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.focus.onFocusChanged import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.res.stringResource -import androidx.compose.ui.text.input.ImeAction -import androidx.compose.ui.text.input.KeyboardType import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.tooling.preview.PreviewParameterProvider @@ -37,9 +34,11 @@ import com.tangem.core.ui.res.TangemThemePreview import com.tangem.features.managetokens.component.preview.PreviewCustomTokenFormComponent import com.tangem.features.managetokens.entity.customtoken.ClickableFieldUM import com.tangem.features.managetokens.entity.customtoken.CustomTokenFormUM +import com.tangem.features.managetokens.entity.customtoken.CustomTokenFormUM.TokenFormUM.Field import com.tangem.features.managetokens.entity.customtoken.TextInputFieldUM import com.tangem.features.managetokens.impl.R import com.tangem.features.managetokens.ui.component.AddCustomTokenDescription +import kotlinx.collections.immutable.mutate @Composable internal fun CustomTokenFormContent(model: CustomTokenFormUM, modifier: Modifier = Modifier) { @@ -125,43 +124,14 @@ private fun TokenForm(tokenForm: CustomTokenFormUM.TokenFormUM, modifier: Modifi shape = TangemTheme.shapes.roundedCornersXMedium, ), ) { - TextField( - model = tokenForm.contractAddress, - keyboardOptions = KeyboardOptions.Default.copy( - imeAction = ImeAction.Next, - ), - ) - TextField( - model = tokenForm.name, - keyboardOptions = KeyboardOptions.Default.copy( - imeAction = ImeAction.Next, - ), - ) - TextField( - model = tokenForm.symbol, - keyboardOptions = KeyboardOptions.Default.copy( - imeAction = ImeAction.Next, - ), - ) - TextField( - model = tokenForm.decimals, - keyboardOptions = KeyboardOptions.Default.copy( - keyboardType = KeyboardType.Decimal, - imeAction = ImeAction.Next, - ), - ) + tokenForm.fields.values.forEach { field -> + TextField(model = field) + } } } @Composable -private fun TextField( - model: TextInputFieldUM, - modifier: Modifier = Modifier, - keyboardOptions: KeyboardOptions = KeyboardOptions.Default, - keyboardActions: KeyboardActions = KeyboardActions.Default, -) { - var isFocused by remember { mutableStateOf(value = false) } - +private fun TextField(model: TextInputFieldUM, modifier: Modifier = Modifier) { InformationBlock( modifier = modifier, title = { @@ -173,7 +143,7 @@ private fun TextField( model.error != null -> { TangemTheme.colors.text.warning } - model.value.isNotBlank() || isFocused -> { + model.value.isNotBlank() || model.isFocused -> { TangemTheme.colors.text.tertiary } else -> { @@ -204,16 +174,15 @@ private fun TextField( .padding(bottom = TangemTheme.dimens.spacing12) .fillMaxWidth() .onFocusChanged { - isFocused = it.isFocused + model.onFocusChange(it.isFocused) }, value = model.value, color = color, onValueChange = model.onValueChange, placeholder = model.placeholder, - readOnly = !model.isEnabled && !isFocused, + readOnly = !model.isEnabled && !model.isFocused, singleLine = true, - keyboardOptions = keyboardOptions, - keyboardActions = keyboardActions, + keyboardOptions = model.keyboardOptions, ) }, ) @@ -262,25 +231,31 @@ private class PreviewCustomTokenFormComponentProvider : override val values: Sequence get() = sequenceOf( PreviewCustomTokenFormComponent( - tokenForm = PreviewCustomTokenFormComponent.tokenForm.copy( - contractAddress = TextInputFieldUM( - label = stringReference("Contract address"), - value = "0x1234567890", - placeholder = stringReference("0x1234567890"), - onValueChange = {}, - ), - ), + tokenForm = PreviewCustomTokenFormComponent.tokenForm.let { form -> + form.copy( + fields = form.fields.mutate { + it[Field.CONTRACT_ADDRESS] = it[Field.CONTRACT_ADDRESS]!!.copy( + label = stringReference("Contract address"), + value = "0x1234567890", + placeholder = stringReference("0x1234567890"), + ) + }, + ) + }, ), PreviewCustomTokenFormComponent( - tokenForm = PreviewCustomTokenFormComponent.tokenForm.copy( - contractAddress = TextInputFieldUM( - label = stringReference("Contract address"), - value = "0x1234567890", - error = stringReference("Contract address is invalid"), - placeholder = stringReference("0x1234567890"), - onValueChange = {}, - ), - ), + tokenForm = PreviewCustomTokenFormComponent.tokenForm.let { form -> + form.copy( + fields = form.fields.mutate { + it[Field.CONTRACT_ADDRESS] = it[Field.CONTRACT_ADDRESS]!!.copy( + label = stringReference("Contract address"), + value = "0x1234567890", + error = stringReference("Contract address is invalid"), + placeholder = stringReference("0x1234567890"), + ) + }, + ) + }, ), PreviewCustomTokenFormComponent( tokenForm = null, diff --git a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/ui/CustomTokenSelectorContent.kt b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/ui/CustomTokenSelectorContent.kt index 476433fb7d..4196a9c491 100644 --- a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/ui/CustomTokenSelectorContent.kt +++ b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/ui/CustomTokenSelectorContent.kt @@ -26,7 +26,6 @@ import com.tangem.core.ui.components.currency.icon.CurrencyIconState import com.tangem.core.ui.components.rows.ChainRow import com.tangem.core.ui.components.rows.model.ChainRowUM import com.tangem.core.ui.extensions.resolveReference -import com.tangem.core.ui.extensions.stringReference import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreview import com.tangem.domain.tokens.model.Network @@ -271,14 +270,15 @@ private class CustomTokenNetworkSelectorComponentPreviewProvider : userWalletId = UserWalletId(stringValue = "321"), selectedNetwork = SelectedNetwork( id = Network.ID(value = "0"), - name = stringReference("Ethereum"), + name = "Ethereum", derivationPath = Network.DerivationPath.Card("m/44'/0'/0'/0/0"), canHandleTokens = true, ), selectedDerivationPath = SelectedDerivationPath( id = Network.ID(value = "0"), value = Network.DerivationPath.Card("m/44'/0'/0'/0/0"), - networkName = stringReference(""), + name = "", + isDefault = false, ), onDerivationPathSelected = {}, ), diff --git a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/utils/CustomCurrencyFormBuilder.kt b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/utils/CustomCurrencyFormBuilder.kt new file mode 100644 index 0000000000..caf9507cc2 --- /dev/null +++ b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/utils/CustomCurrencyFormBuilder.kt @@ -0,0 +1,94 @@ +package com.tangem.features.managetokens.utils + +import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.ui.text.input.KeyboardCapitalization +import androidx.compose.ui.text.input.KeyboardType +import com.tangem.core.decompose.di.ComponentScoped +import com.tangem.core.decompose.model.ParamsContainer +import com.tangem.core.ui.extensions.resourceReference +import com.tangem.core.ui.extensions.stringReference +import com.tangem.features.managetokens.component.CustomTokenFormComponent +import com.tangem.features.managetokens.entity.customtoken.CustomTokenFormUM +import com.tangem.features.managetokens.entity.customtoken.CustomTokenFormUM.TokenFormUM.Field +import com.tangem.features.managetokens.entity.customtoken.TextInputFieldUM +import com.tangem.features.managetokens.impl.R +import kotlinx.collections.immutable.persistentMapOf +import javax.inject.Inject + +@ComponentScoped +internal class CustomCurrencyFormBuilder @Inject constructor( + paramsContainer: ParamsContainer, +) { + + private val params: CustomTokenFormComponent.Params = paramsContainer.require() + + fun buildForm( + updateFormFieldValue: (Field, String) -> Unit, + updateFormFieldFocus: (Field, Boolean) -> Unit, + ): CustomTokenFormUM.TokenFormUM { + val formValues = params.formValues + val fields = persistentMapOf( + Field.CONTRACT_ADDRESS to TextInputFieldUM( + label = resourceReference(R.string.custom_token_contract_address_input_title), + placeholder = stringReference(CONTRACT_ADDRESS_PLACEHOLDER), + keyboardOptions = KeyboardOptions( + capitalization = KeyboardCapitalization.None, + keyboardType = KeyboardType.Text, + ), + onValueChange = { value -> + updateFormFieldValue(Field.CONTRACT_ADDRESS, value) + }, + onFocusChange = { isFocused -> + updateFormFieldFocus(Field.CONTRACT_ADDRESS, isFocused) + }, + ), + Field.NAME to TextInputFieldUM( + label = resourceReference(R.string.custom_token_name_input_title), + placeholder = resourceReference(R.string.custom_token_name_input_placeholder), + keyboardOptions = KeyboardOptions( + capitalization = KeyboardCapitalization.Words, + keyboardType = KeyboardType.Text, + ), + onValueChange = { value -> + updateFormFieldValue(Field.NAME, value) + }, + onFocusChange = { isFocused -> + updateFormFieldFocus(Field.NAME, isFocused) + }, + ), + Field.SYMBOL to TextInputFieldUM( + label = resourceReference(R.string.custom_token_token_symbol_input_title), + placeholder = resourceReference(R.string.custom_token_token_symbol_input_placeholder), + keyboardOptions = KeyboardOptions( + capitalization = KeyboardCapitalization.Characters, + keyboardType = KeyboardType.Text, + ), + onValueChange = { value -> + updateFormFieldValue(Field.SYMBOL, value) + }, + onFocusChange = { isFocused -> + updateFormFieldFocus(Field.SYMBOL, isFocused) + }, + ), + Field.DECIMALS to TextInputFieldUM( + label = resourceReference(R.string.custom_token_decimals_input_title), + placeholder = stringReference(DECIMALS_PLACEHOLDER), + keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Decimal), + onValueChange = { value -> + updateFormFieldValue(Field.DECIMALS, value) + }, + onFocusChange = { isFocused -> + updateFormFieldFocus(Field.DECIMALS, isFocused) + }, + ), + ) + val form = CustomTokenFormUM.TokenFormUM(fields) + + return formValues.fillValues(form) + } + + private companion object { + const val CONTRACT_ADDRESS_PLACEHOLDER = "0x000000000000000000000000000..." + const val DECIMALS_PLACEHOLDER = "0" + } +} \ No newline at end of file diff --git a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/utils/list/ManageTokensListManager.kt b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/utils/list/ManageTokensListManager.kt index da24501ab1..2eea0dc190 100644 --- a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/utils/list/ManageTokensListManager.kt +++ b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/utils/list/ManageTokensListManager.kt @@ -1,6 +1,7 @@ package com.tangem.features.managetokens.utils.list import arrow.core.getOrElse +import com.tangem.core.analytics.api.AnalyticsEventHandler import com.tangem.core.decompose.di.ComponentScoped import com.tangem.core.decompose.ui.UiMessageSender import com.tangem.core.ui.clipboard.ClipboardManager @@ -8,12 +9,15 @@ import com.tangem.core.ui.extensions.resourceReference import com.tangem.core.ui.extensions.stringReference import com.tangem.core.ui.message.SnackbarMessage import com.tangem.domain.managetokens.CheckCurrencyUnsupportedUseCase +import com.tangem.domain.managetokens.CheckHasLinkedTokensUseCase import com.tangem.domain.managetokens.GetManagedTokensUseCase import com.tangem.domain.managetokens.RemoveCustomManagedCryptoCurrencyUseCase -import com.tangem.domain.managetokens.CheckHasLinkedTokensUseCase import com.tangem.domain.managetokens.model.* import com.tangem.domain.tokens.model.Network import com.tangem.domain.wallets.models.UserWalletId +import com.tangem.features.managetokens.analytics.ManageTokensAnalyticEvent +import com.tangem.features.managetokens.component.ManageTokensComponent +import com.tangem.features.managetokens.component.ManageTokensSource import com.tangem.features.managetokens.entity.item.CurrencyItemUM import com.tangem.features.managetokens.impl.R import com.tangem.pagination.BatchAction @@ -42,10 +46,12 @@ internal class ManageTokensListManager @Inject constructor( private val checkCurrencyUnsupportedUseCase: CheckCurrencyUnsupportedUseCase, private val messageSender: UiMessageSender, private val dispatchers: CoroutineDispatcherProvider, + private val analyticsEventHandler: AnalyticsEventHandler, clipboardManager: ClipboardManager, ) : ManageTokensUiActions { private lateinit var scope: CoroutineScope + private lateinit var source: ManageTokensSource private val jobHolder = JobHolder() private val actionsFlow: MutableSharedFlow = MutableSharedFlow( @@ -74,8 +80,9 @@ internal class ManageTokensListManager @Inject constructor( .distinctUntilChanged() val uiItems: Flow> = uiManager.items - suspend fun launchPagination(userWalletId: UserWalletId?) = coroutineScope { + suspend fun launchPagination(params: ManageTokensComponent.Params) = coroutineScope { scope = this + source = params.source val batchFlow = getManagedTokensUseCase( context = ManageTokensListBatchingContext( @@ -85,13 +92,13 @@ internal class ManageTokensListManager @Inject constructor( ) batchFlow.state - .onEach { state -> updateState(state, userWalletId) } + .onEach { state -> updateState(state, params.userWalletId) } .flowOn(dispatchers.default) .launchIn(scope = this) .saveIn(jobHolder) // Initial load - reload(userWalletId) + reload(params.userWalletId) } suspend fun reload(userWalletId: UserWalletId?) { @@ -159,12 +166,16 @@ internal class ManageTokensListManager @Inject constructor( changedCurrenciesManager.addCurrency(currency, network) sendSelectCurrencyAction(batchKey, currency.id, network, isSelected = true) + + sendSelectCurrencyAnalyticsEvent(currency, isSelected = true) } override fun removeCurrency(batchKey: Int, currency: ManagedCryptoCurrency.Token, network: Network) { changedCurrenciesManager.removeCurrency(currency, network) sendSelectCurrencyAction(batchKey, currency.id, network, isSelected = false) + + sendSelectCurrencyAnalyticsEvent(currency, isSelected = false) } override fun removeCustomCurrency(userWalletId: UserWalletId, currency: ManagedCryptoCurrency.Custom) { @@ -180,6 +191,15 @@ internal class ManageTokensListManager @Inject constructor( network: Network, ): Boolean = !changedCurrenciesManager.containsCurrency(currency, network) + private fun sendSelectCurrencyAnalyticsEvent(currency: ManagedCryptoCurrency.Token, isSelected: Boolean) { + val event = ManageTokensAnalyticEvent.TokenSwitcherChanged( + tokenSymbol = currency.symbol, + isSelected = isSelected, + source = source, + ) + analyticsEventHandler.send(event) + } + private fun sendSelectCurrencyAction( batchKey: Int, currencyId: ManagedCryptoCurrency.ID, diff --git a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/utils/mapper/TokenFormMapper.kt b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/utils/mapper/TokenFormMapper.kt index fd9a7bd140..6e55316dfb 100644 --- a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/utils/mapper/TokenFormMapper.kt +++ b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/utils/mapper/TokenFormMapper.kt @@ -5,9 +5,9 @@ import com.tangem.features.managetokens.entity.customtoken.CustomTokenFormUM internal fun CustomTokenFormUM.TokenFormUM.mapToDomainModel(): AddCustomTokenForm.Raw { return AddCustomTokenForm.Raw( - contractAddress = contractAddress.value, - symbol = symbol.value, - name = name.value, - decimals = decimals.value, + contractAddress = fields.getValue(CustomTokenFormUM.TokenFormUM.Field.CONTRACT_ADDRESS).value, + symbol = fields.getValue(CustomTokenFormUM.TokenFormUM.Field.SYMBOL).value, + name = fields.getValue(CustomTokenFormUM.TokenFormUM.Field.NAME).value, + decimals = fields.getValue(CustomTokenFormUM.TokenFormUM.Field.DECIMALS).value, ) } \ No newline at end of file diff --git a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/utils/ui/CustomCurrencyFormOperations.kt b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/utils/ui/CustomCurrencyFormOperations.kt index ae05953156..d99bb9473b 100644 --- a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/utils/ui/CustomCurrencyFormOperations.kt +++ b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/utils/ui/CustomCurrencyFormOperations.kt @@ -1,17 +1,17 @@ package com.tangem.features.managetokens.utils.ui import com.tangem.core.ui.components.notifications.NotificationConfig -import com.tangem.core.ui.extensions.TextReference import com.tangem.core.ui.extensions.resourceReference import com.tangem.core.ui.extensions.wrappedList import com.tangem.domain.managetokens.ValidateTokenFormUseCase import com.tangem.domain.managetokens.model.exceptoin.CustomTokenFormValidationException import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.features.managetokens.entity.customtoken.CustomTokenFormUM -import com.tangem.features.managetokens.entity.customtoken.TextInputFieldUM +import com.tangem.features.managetokens.entity.customtoken.CustomTokenFormUM.TokenFormUM.Field import com.tangem.features.managetokens.impl.R import kotlinx.collections.immutable.mutate import kotlinx.collections.immutable.persistentListOf +import kotlinx.collections.immutable.toPersistentMap internal fun CustomTokenFormUM.updateTokenForm( block: CustomTokenFormUM.TokenFormUM.() -> CustomTokenFormUM.TokenFormUM, @@ -23,19 +23,6 @@ internal fun CustomTokenFormUM.updateTokenForm( return copy(tokenForm = updatedForm) } -internal fun TextInputFieldUM.updateValue( - value: String = this.value, - error: TextReference? = this.error, - isEnabled: Boolean = this.isEnabled, - clearError: Boolean = false, -): TextInputFieldUM { - return copy( - value = value, - error = if (clearError) null else error, - isEnabled = isEnabled, - ) -} - internal fun CustomTokenFormUM.updateWithProgress( showProgress: Boolean, isWasFilled: Boolean = this.tokenForm?.wasFilled ?: false, @@ -49,22 +36,21 @@ internal fun CustomTokenFormUM.updateWithProgress( canAddToken = canAddToken, notifications = if (clearNotifications) persistentListOf() else notifications, ).updateTokenForm { + val updatedFields = fields.mapValues { (key, field) -> + field.copy( + isEnabled = when (key) { + Field.CONTRACT_ADDRESS -> field.isEnabled + Field.NAME, + Field.SYMBOL, + Field.DECIMALS, + -> !(showProgress || disableSecondaryFields) + }, + error = if (clearFieldErrors) null else field.error, + ) + } + copy( - contractAddress = contractAddress.updateValue( - clearError = clearFieldErrors, - ), - name = name.updateValue( - isEnabled = !showProgress && !disableSecondaryFields, - clearError = clearFieldErrors, - ), - symbol = symbol.updateValue( - isEnabled = !showProgress && !disableSecondaryFields, - clearError = clearFieldErrors, - ), - decimals = decimals.updateValue( - isEnabled = !showProgress && !disableSecondaryFields, - clearError = clearFieldErrors, - ), + fields = updatedFields.toPersistentMap(), wasFilled = isWasFilled, ) } @@ -72,11 +58,31 @@ internal fun CustomTokenFormUM.updateWithProgress( internal fun CustomTokenFormUM.updateWithCurrency(currency: CryptoCurrency): CustomTokenFormUM { return updateTokenForm { + val updatedFields = fields.mapValues { (key, field) -> + when (key) { + Field.CONTRACT_ADDRESS -> field.copy( + error = null, + ) + Field.NAME -> field.copy( + value = currency.name, + error = null, + isEnabled = false, + ) + Field.SYMBOL -> field.copy( + value = currency.symbol, + error = null, + isEnabled = false, + ) + Field.DECIMALS -> field.copy( + value = currency.decimals.toString(), + error = null, + isEnabled = false, + ) + } + } + copy( - contractAddress = contractAddress.updateValue(error = null), - name = name.updateValue(currency.name), - symbol = symbol.updateValue(currency.symbol), - decimals = decimals.updateValue(currency.decimals.toString()), + fields = updatedFields.toPersistentMap(), ) } } @@ -85,18 +91,20 @@ internal fun CustomTokenFormUM.updateWithContractAddressException( exception: CustomTokenFormValidationException.ContractAddress, ): CustomTokenFormUM { return updateTokenForm { - copy( - contractAddress = contractAddress.updateValue( + val updatedFields = fields.mutate { + it[Field.CONTRACT_ADDRESS] = it.getValue(Field.CONTRACT_ADDRESS).copy( error = when (exception) { CustomTokenFormValidationException.ContractAddress.Empty -> { - null + null // Should not display this error } CustomTokenFormValidationException.ContractAddress.Invalid -> { resourceReference(R.string.custom_token_creation_error_invalid_contract_address) } }, - ), - ) + ) + } + + copy(fields = updatedFields) } } @@ -104,11 +112,11 @@ internal fun CustomTokenFormUM.updateWithDecimalsException( exception: CustomTokenFormValidationException.Decimals, ): CustomTokenFormUM { return updateTokenForm { - copy( - decimals = decimals.updateValue( + val updatedFields = fields.mutate { + it[Field.DECIMALS] = it.getValue(Field.DECIMALS).copy( error = when (exception) { is CustomTokenFormValidationException.Decimals.Empty -> { - null + null // Should not display this error } is CustomTokenFormValidationException.Decimals.Invalid -> { resourceReference( @@ -117,8 +125,10 @@ internal fun CustomTokenFormUM.updateWithDecimalsException( ) } }, - ), - ) + ) + } + + copy(fields = updatedFields) } } diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/DefaultMarketsTokenDetailsComponent.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/DefaultMarketsTokenDetailsComponent.kt index 681d36de1b..2628335d48 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/DefaultMarketsTokenDetailsComponent.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/DefaultMarketsTokenDetailsComponent.kt @@ -110,11 +110,8 @@ internal class DefaultMarketsTokenDetailsComponent @AssistedInject constructor( backgroundColor = LocalMainBottomSheetColor.current.value, addTopBarStatusBarPadding = false, state = state, - onBackClick = { - if (bsState == BottomSheetState.EXPANDED) { - navigateBack() - } - }, + onBackClick = ::navigateBack, + backButtonEnabled = bsState == BottomSheetState.EXPANDED, onHeaderSizeChange = onHeaderSizeChange, portfolioBlock = portfolioComponent?.let { component -> { blockModifier -> @@ -145,6 +142,7 @@ internal class DefaultMarketsTokenDetailsComponent @AssistedInject constructor( addTopBarStatusBarPadding = true, state = state, onBackClick = ::navigateBack, + backButtonEnabled = true, onHeaderSizeChange = {}, portfolioBlock = portfolioComponent?.let { component -> { blockModifier -> diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/MarketsTokenDetailsContent.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/MarketsTokenDetailsContent.kt index bce07d98c0..26639bd472 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/MarketsTokenDetailsContent.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/MarketsTokenDetailsContent.kt @@ -52,6 +52,7 @@ internal fun MarketsTokenDetailsContent( addTopBarStatusBarPadding: Boolean, onBackClick: () -> Unit, onHeaderSizeChange: (Dp) -> Unit, + backButtonEnabled: Boolean, portfolioBlock: @Composable ((Modifier) -> Unit)?, modifier: Modifier = Modifier, ) { @@ -61,6 +62,7 @@ internal fun MarketsTokenDetailsContent( state = state, onBackClick = onBackClick, onHeaderSizeChange = onHeaderSizeChange, + backButtonEnabled = backButtonEnabled, portfolioBlock = portfolioBlock, addTopBarStatusBarInsets = addTopBarStatusBarPadding, ) @@ -76,6 +78,7 @@ private fun Content( addTopBarStatusBarInsets: Boolean, onBackClick: () -> Unit, onHeaderSizeChange: (Dp) -> Unit, + backButtonEnabled: Boolean, portfolioBlock: @Composable ((Modifier) -> Unit)?, modifier: Modifier = Modifier, ) { @@ -98,7 +101,10 @@ private fun Content( } }, title = state.tokenName, - startButton = TopAppBarButtonUM.Back(onBackClick), + startButton = TopAppBarButtonUM.Back( + onBackClicked = onBackClick, + enabled = backButtonEnabled, + ), ) SpacerH4() @@ -305,6 +311,7 @@ private fun Preview() { onBackClick = {}, backgroundColor = TangemTheme.colors.background.tertiary, portfolioBlock = {}, + backButtonEnabled = true, addTopBarStatusBarPadding = false, ) } diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/MyPortfolioUMFactory.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/MyPortfolioUMFactory.kt index 3fbf58975e..1bc2ecf543 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/MyPortfolioUMFactory.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/MyPortfolioUMFactory.kt @@ -52,7 +52,7 @@ internal class MyPortfolioUMFactory( onAddClick = onAddClick, ) } else { - MyPortfolioUM.Unavailable + MyPortfolioUM.UnavailableForWallet } } diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/MyPortfolio.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/MyPortfolio.kt index 3637c9ed61..daaa385754 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/MyPortfolio.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/MyPortfolio.kt @@ -1,6 +1,7 @@ package com.tangem.features.markets.portfolio.impl.ui import android.content.res.Configuration +import androidx.annotation.StringRes import androidx.compose.foundation.background import androidx.compose.foundation.layout.* import androidx.compose.foundation.shape.RoundedCornerShape @@ -50,7 +51,8 @@ internal fun MyPortfolio(state: MyPortfolioUM, modifier: Modifier = Modifier) { is MyPortfolioUM.Tokens -> TokenList(state = state) is MyPortfolioUM.AddFirstToken -> AddFirstTokenContent(state = state, modifier = contentModifier) MyPortfolioUM.Loading -> LoadingPlaceholder(modifier = contentModifier) - MyPortfolioUM.Unavailable -> UnavailableContent(modifier = contentModifier) + MyPortfolioUM.Unavailable -> UnavailableAsset(modifier = contentModifier) + MyPortfolioUM.UnavailableForWallet -> UnavailableAssetForWallet(modifier = contentModifier) } } @@ -111,10 +113,26 @@ private fun TokenList(state: MyPortfolioUM.Tokens, modifier: Modifier = Modifier } @Composable -private fun UnavailableContent(modifier: Modifier = Modifier) { +fun UnavailableAsset(modifier: Modifier = Modifier) { + UnavailableContent( + textId = R.string.markets_add_to_my_portfolio_unavailable_description, + modifier = modifier, + ) +} + +@Composable +fun UnavailableAssetForWallet(modifier: Modifier = Modifier) { + UnavailableContent( + textId = R.string.markets_add_to_my_portfolio_unavailable_for_wallet_description, + modifier = modifier, + ) +} + +@Composable +private fun UnavailableContent(@StringRes textId: Int, modifier: Modifier = Modifier) { Text( modifier = modifier, - text = stringResource(R.string.markets_add_to_my_portfolio_unavailable_description), + text = stringResource(textId), style = TangemTheme.typography.body2, color = TangemTheme.colors.text.tertiary, ) diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/preview/PreviewMyPortfolioUMProvider.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/preview/PreviewMyPortfolioUMProvider.kt index bdff4f722c..fab6e21c48 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/preview/PreviewMyPortfolioUMProvider.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/preview/PreviewMyPortfolioUMProvider.kt @@ -42,6 +42,7 @@ internal class PreviewMyPortfolioUMProvider : PreviewParameterProvider R.drawable.ic_doc_24 is TransactionType.TronStakingTransactionType.Stake, is TransactionType.TronStakingTransactionType.Vote, - -> R.drawable.ic_transaction_history_staking - is TransactionType.TronStakingTransactionType.Withdraw, + -> R.drawable.ic_transaction_history_staking_24 + is TransactionType.TronStakingTransactionType.ClaimRewards, + -> R.drawable.ic_transaction_history_claim_rewards_24 is TransactionType.TronStakingTransactionType.Unstake, - -> R.drawable.ic_transaction_history_unstaking + is TransactionType.TronStakingTransactionType.Withdraw, + -> R.drawable.ic_transaction_history_unstaking_24 is TransactionType.Operation, is TransactionType.Swap, is TransactionType.Transfer, @@ -72,6 +74,7 @@ internal class TokenDetailsTxHistoryTransactionStateConverter( is TransactionType.TronStakingTransactionType.Stake -> resourceReference(R.string.common_stake) is TransactionType.TronStakingTransactionType.Unstake -> resourceReference(R.string.common_unstake) is TransactionType.TronStakingTransactionType.Vote -> resourceReference(R.string.staking_vote) + is TransactionType.TronStakingTransactionType.ClaimRewards -> resourceReference(R.string.common_claim_rewards) is TransactionType.TronStakingTransactionType.Withdraw -> resourceReference(R.string.staking_withdraw) } @@ -97,9 +100,13 @@ internal class TokenDetailsTxHistoryTransactionStateConverter( }, formatArgs = wrappedList(interactionAddress.address.toBriefAddressFormat()), ) - is InteractionAddressType.Staking -> resourceReference( - id = R.string.common_staking, + is InteractionAddressType.Validator -> resourceReference( + id = R.string.transaction_history_transaction_validator, + formatArgs = wrappedList(interactionAddress.address.toBriefAddressFormat()), ) + null -> { + TextReference.EMPTY + } } private fun TxHistoryItem.extractDirection() = if (isOutgoing) Direction.OUTGOING else Direction.INCOMING @@ -111,7 +118,8 @@ internal class TokenDetailsTxHistoryTransactionStateConverter( } private fun TxHistoryItem.getAmount(): String { - if (type == TransactionType.TronStakingTransactionType.Vote || + if (type is TransactionType.TronStakingTransactionType.Vote || + type == TransactionType.TronStakingTransactionType.ClaimRewards || type == TransactionType.TronStakingTransactionType.Withdraw ) { return "" diff --git a/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/analytics/Settings.kt b/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/analytics/Settings.kt index 9a655d973a..99b401cfda 100644 --- a/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/analytics/Settings.kt +++ b/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/analytics/Settings.kt @@ -9,5 +9,7 @@ internal sealed class Settings( error: Throwable? = null, ) : AnalyticsEvent(category, event, params, error) { - class ButtonCreateBackup : Settings(event = "Button - Create Backup") + data object ButtonCreateBackup : Settings(event = "Button - Create Backup") + + data object ButtonManageTokens : Settings(event = "Button - Manage Tokens") } \ No newline at end of file diff --git a/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/component/preview/PreviewWalletSettingsComponent.kt b/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/component/preview/PreviewWalletSettingsComponent.kt index c0f134ef22..5844c0eab0 100644 --- a/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/component/preview/PreviewWalletSettingsComponent.kt +++ b/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/component/preview/PreviewWalletSettingsComponent.kt @@ -2,6 +2,7 @@ package com.tangem.feature.walletsettings.component.preview import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier +import com.tangem.core.analytics.DummyAnalyticsEventHandler import com.tangem.core.decompose.navigation.DummyRouter import com.tangem.domain.wallets.models.UserWalletId import com.tangem.feature.walletsettings.component.WalletSettingsComponent @@ -13,7 +14,10 @@ internal class PreviewWalletSettingsComponent : WalletSettingsComponent { private val previewState = WalletSettingsUM( popBack = {}, - items = ItemsBuilder(router = DummyRouter()).buildItems( + items = ItemsBuilder( + router = DummyRouter(), + analyticsEventHandler = DummyAnalyticsEventHandler(), + ).buildItems( userWalletId = UserWalletId("011"), userWalletName = "My Wallet", isReferralAvailable = true, diff --git a/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/model/WalletSettingsModel.kt b/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/model/WalletSettingsModel.kt index ba19734477..1ad797573f 100644 --- a/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/model/WalletSettingsModel.kt +++ b/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/model/WalletSettingsModel.kt @@ -145,7 +145,7 @@ internal class WalletSettingsModel @Inject constructor( } private fun onLinkMoreCardsClick(scanResponse: ScanResponse) { - analyticsEventHandler.send(Settings.ButtonCreateBackup()) + analyticsEventHandler.send(Settings.ButtonCreateBackup) analyticsContextProxy.addContext(scanResponse) diff --git a/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/utils/ItemsBuilder.kt b/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/utils/ItemsBuilder.kt index 896d1bbf0c..74ebf2067c 100644 --- a/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/utils/ItemsBuilder.kt +++ b/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/utils/ItemsBuilder.kt @@ -1,12 +1,15 @@ package com.tangem.feature.walletsettings.utils import com.tangem.common.routing.AppRoute +import com.tangem.common.routing.AppRoute.ManageTokens.Source +import com.tangem.core.analytics.api.AnalyticsEventHandler import com.tangem.core.decompose.di.ComponentScoped import com.tangem.core.decompose.navigation.Router import com.tangem.core.ui.components.block.model.BlockUM import com.tangem.core.ui.extensions.resourceReference import com.tangem.core.ui.extensions.stringReference import com.tangem.domain.wallets.models.UserWalletId +import com.tangem.feature.walletsettings.analytics.Settings import com.tangem.feature.walletsettings.entity.WalletSettingsItemUM import com.tangem.feature.walletsettings.impl.R import kotlinx.collections.immutable.PersistentList @@ -17,6 +20,7 @@ import javax.inject.Inject @ComponentScoped internal class ItemsBuilder @Inject constructor( private val router: Router, + private val analyticsEventHandler: AnalyticsEventHandler, ) { @Suppress("LongParameterList") @@ -62,7 +66,10 @@ internal class ItemsBuilder @Inject constructor( BlockUM( text = resourceReference(R.string.add_tokens_title), iconRes = R.drawable.ic_tether_24, - onClick = { router.push(AppRoute.ManageTokens(userWalletId)) }, + onClick = { + analyticsEventHandler.send(Settings.ButtonManageTokens) + router.push(AppRoute.ManageTokens(Source.SETTINGS, userWalletId)) + }, ).let(::add) } diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/router/DefaultWalletRouter.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/router/DefaultWalletRouter.kt index 421f33f996..d08f24beca 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/router/DefaultWalletRouter.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/router/DefaultWalletRouter.kt @@ -14,6 +14,7 @@ import androidx.navigation.compose.composable import androidx.navigation.compose.rememberNavController import androidx.navigation.navArgument import com.tangem.common.routing.AppRoute +import com.tangem.common.routing.AppRoute.ManageTokens.Source import com.tangem.common.routing.AppRouter import com.tangem.core.navigation.url.UrlOpener import com.tangem.domain.redux.ReduxStateHolder @@ -140,7 +141,7 @@ internal class DefaultWalletRouter( } override fun openManageTokensScreen(userWalletId: UserWalletId) { - router.push(AppRoute.ManageTokens(userWalletId = userWalletId)) + router.push(AppRoute.ManageTokens(Source.SETTINGS, userWalletId)) } override fun openScanFailedDialog(onTryAgain: () -> Unit) { diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/TxHistoryItemStateConverter.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/TxHistoryItemStateConverter.kt index f37407d469..2d162a17c0 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/TxHistoryItemStateConverter.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/TxHistoryItemStateConverter.kt @@ -50,10 +50,12 @@ internal class TxHistoryItemStateConverter( is TransactionType.Approve -> R.drawable.ic_doc_24 is TransactionType.TronStakingTransactionType.Stake, is TransactionType.TronStakingTransactionType.Vote, - -> R.drawable.ic_transaction_history_staking - is TransactionType.TronStakingTransactionType.Withdraw, + -> R.drawable.ic_transaction_history_staking_24 + is TransactionType.TronStakingTransactionType.ClaimRewards, + -> R.drawable.ic_transaction_history_claim_rewards_24 is TransactionType.TronStakingTransactionType.Unstake, - -> R.drawable.ic_transaction_history_unstaking + is TransactionType.TronStakingTransactionType.Withdraw, + -> R.drawable.ic_transaction_history_unstaking_24 is TransactionType.Operation, is TransactionType.Swap, is TransactionType.Transfer, @@ -70,7 +72,8 @@ internal class TxHistoryItemStateConverter( is TransactionType.TronStakingTransactionType.Stake -> resourceReference(R.string.common_stake) is TransactionType.TronStakingTransactionType.Unstake -> resourceReference(R.string.common_unstake) is TransactionType.TronStakingTransactionType.Vote -> resourceReference(R.string.staking_vote) - is TransactionType.TronStakingTransactionType.Withdraw -> resourceReference(R.string.staking_withdraw) + is TransactionType.TronStakingTransactionType.ClaimRewards -> resourceReference(R.string.common_claim_rewards) + is TransactionType.TronStakingTransactionType.Withdraw -> { resourceReference(R.string.staking_withdraw) } is TransactionType.UnknownOperation -> resourceReference(R.string.transaction_history_operation) } @@ -96,9 +99,13 @@ internal class TxHistoryItemStateConverter( }, formatArgs = wrappedList(interactionAddress.address.toBriefAddressFormat()), ) - is InteractionAddressType.Staking -> resourceReference( - id = R.string.common_staking, + is InteractionAddressType.Validator -> resourceReference( + id = R.string.transaction_history_transaction_validator, + formatArgs = wrappedList(interactionAddress.address.toBriefAddressFormat()), ) + null -> { + TextReference.EMPTY + } } private fun TxHistoryItem.extractDirection() = @@ -111,7 +118,8 @@ internal class TxHistoryItemStateConverter( } private fun TxHistoryItem.getAmount(): String { - if (type == TransactionType.TronStakingTransactionType.Vote || + if (type is TransactionType.TronStakingTransactionType.Vote || + type == TransactionType.TronStakingTransactionType.ClaimRewards || type == TransactionType.TronStakingTransactionType.Withdraw ) { return "" diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/WalletScreen.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/WalletScreen.kt index aff4cf252f..d131e8e482 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/WalletScreen.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/WalletScreen.kt @@ -8,6 +8,7 @@ import androidx.compose.animation.fadeIn import androidx.compose.animation.fadeOut import androidx.compose.animation.slideIn import androidx.compose.foundation.Canvas +import androidx.compose.foundation.clickable import androidx.compose.foundation.gestures.detectTapGestures import androidx.compose.foundation.layout.* import androidx.compose.foundation.lazy.LazyColumn @@ -425,8 +426,17 @@ private inline fun BaseScaffoldWithMarkets( modifier = Modifier.sizeIn(maxHeight = maxHeight - statusBarHeight), ) { Hand(Modifier.drawBehind { drawRect(backgroundColor.value) }) + Box( modifier = Modifier + // expand bottom sheet when clicked on the header + .clickable( + enabled = bottomSheetState.currentValue == TangemSheetValue.PartiallyExpanded, + indication = null, + interactionSource = null, + ) { + coroutineScope.launch { bottomSheetState.expand() } + } .onFocusChanged { isSearchFieldFocused = it.isFocused }, diff --git a/gradle/dependencies.toml b/gradle/dependencies.toml index 49b3c92007..aaf2b34885 100644 --- a/gradle/dependencies.toml +++ b/gradle/dependencies.toml @@ -89,9 +89,9 @@ markdownComposeView = "0.5.4" # endregion Other libraries # region Tangem -tangemBlockchainSdk = "release-app_5.15-781" +tangemBlockchainSdk = "release-app_5.15-796" #tangemBlockchainSdk = "0.0.1" # Keep it! - used for local builds -tangemCardSdk = "release-app_5.15-382" +tangemCardSdk = "release-app_5.15-384" #tangemCardSdk = "0.0.1" # Keep it! - used for local builds ^ tangemVico = "2.0.0-alpha.25-tangem16" #tangemVico = "0.0.1" # Keep it! - used for local builds ^