diff --git a/common/ui/src/main/java/com/tangem/common/ui/tokens/TokenItemStateConverter.kt b/common/ui/src/main/java/com/tangem/common/ui/tokens/TokenItemStateConverter.kt index 38a1c8c270..c92f348a54 100644 --- a/common/ui/src/main/java/com/tangem/common/ui/tokens/TokenItemStateConverter.kt +++ b/common/ui/src/main/java/com/tangem/common/ui/tokens/TokenItemStateConverter.kt @@ -51,6 +51,8 @@ class TokenItemStateConverter( }, private val onApyLabelClick: ((CryptoCurrencyStatus, ApySource, String) -> Unit)? = null, private val onYieldPromoCloseClick: (() -> Unit)? = null, + private val onYieldPromoShown: ((cryptoCurrency: CryptoCurrency) -> Unit)? = null, + private val onYieldPromoClicked: ((cryptoCurrency: CryptoCurrency) -> Unit)? = null, private val titleStateProvider: (CryptoCurrencyStatus) -> TokenItemState.TitleState = { currencyStatus -> createTitleState( currencyStatus = currencyStatus, @@ -75,6 +77,8 @@ class TokenItemStateConverter( yieldSupplyPromoBannerKey = yieldSupplyPromoBannerKey, onApyLabelClick = onApyLabelClick, onYieldPromoCloseClick = onYieldPromoCloseClick, + onYieldPromoShown = onYieldPromoShown, + onYieldPromoClicked = onYieldPromoClicked, ) }, private val onItemClick: ((TokenItemState, CryptoCurrencyStatus) -> Unit)? = null, @@ -389,12 +393,15 @@ class TokenItemStateConverter( } } + @Suppress("LongParameterList") private fun createPromoBannerState( status: CryptoCurrencyStatus, yieldModuleApyMap: Map, yieldSupplyPromoBannerKey: String?, onApyLabelClick: ((CryptoCurrencyStatus, ApySource, String) -> Unit)?, onYieldPromoCloseClick: (() -> Unit)?, + onYieldPromoShown: ((cryptoCurrency: CryptoCurrency) -> Unit)?, + onYieldPromoClicked: ((cryptoCurrency: CryptoCurrency) -> Unit)?, ): TokenItemState.PromoBannerState { val token = status.currency as? CryptoCurrency.Token ?: return TokenItemState.PromoBannerState.Empty if (status.value !is CryptoCurrencyStatus.Loaded) { @@ -414,11 +421,15 @@ class TokenItemStateConverter( wrappedList(yieldSupplyApy), ), onPromoBannerClick = { + onYieldPromoClicked?.invoke(status.currency) onApyLabelClick?.invoke(status, ApySource.YIELD_SUPPLY, yieldSupplyApy.toString()) }, onCloseClick = { onYieldPromoCloseClick?.invoke() }, + onPromoShown = { + onYieldPromoShown?.invoke(status.currency) + }, ) } diff --git a/core/analytics/models/src/main/java/com/tangem/core/analytics/models/event/MainScreenAnalyticsEvent.kt b/core/analytics/models/src/main/java/com/tangem/core/analytics/models/event/MainScreenAnalyticsEvent.kt index c8864d3617..c7958a7e35 100644 --- a/core/analytics/models/src/main/java/com/tangem/core/analytics/models/event/MainScreenAnalyticsEvent.kt +++ b/core/analytics/models/src/main/java/com/tangem/core/analytics/models/event/MainScreenAnalyticsEvent.kt @@ -126,6 +126,28 @@ sealed class MainScreenAnalyticsEvent( STATE to state, ), ) + + data class YieldPromo( + val token: String, + val blockchain: String, + ) : MainScreenAnalyticsEvent( + event = "Yield Promo", + params = mapOf( + TOKEN_PARAM to token, + BLOCKCHAIN to blockchain, + ), + ) + + data class YieldPromoClicked( + val token: String, + val blockchain: String, + ) : MainScreenAnalyticsEvent( + event = "Yield Promo Clicked", + params = mapOf( + TOKEN_PARAM to token, + BLOCKCHAIN to blockchain, + ), + ) // endregion companion object { diff --git a/core/datasource/src/main/java/com/tangem/datasource/utils/NetworkLogsSaveInterceptor.kt b/core/datasource/src/main/java/com/tangem/datasource/utils/NetworkLogsSaveInterceptor.kt index 9e3d88dcb4..55d88cd09e 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/utils/NetworkLogsSaveInterceptor.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/utils/NetworkLogsSaveInterceptor.kt @@ -32,8 +32,16 @@ class NetworkLogsSaveInterceptor( @Throws(IOException::class) override fun intercept(chain: Interceptor.Chain): Response { val request = chain.request() + val host = request.url.host + val path = request.url.encodedPath + val isRestrictedUrl = restrictedForLogURLs.contains(host + path) + val isRestrictedHost = restrictedForLogHosts.any { host.contains(it) } - logRequestMessage(chain, request) + if (isRestrictedUrl || isRestrictedHost) { + logEmptyRequestMessage(chain, request) + } else { + logRequestMessage(chain, request) + } val startNs = System.nanoTime() val response: Response @@ -44,11 +52,6 @@ class NetworkLogsSaveInterceptor( throw e } - val host = request.url.host - val path = request.url.encodedPath - val isRestrictedUrl = restrictedForLogURLs.contains(host + path) - val isRestrictedHost = restrictedForLogHosts.any { host.contains(it) } - if (isRestrictedUrl || isRestrictedHost) { logResponseWithEmptyMessage(response, startNs) } else { @@ -58,6 +61,13 @@ class NetworkLogsSaveInterceptor( return response } + private fun logEmptyRequestMessage(chain: Interceptor.Chain, request: Request) { + val connection = chain.connection() + val connectionProtocol = if (connection != null) " ${connection.protocol()}" else "" + + saveLogMessage("--> ${request.method} ${request.url}$connectionProtocol\n") + } + private fun logRequestMessage(chain: Interceptor.Chain, request: Request) { val connection = chain.connection() val connectionProtocol = if (connection != null) " ${connection.protocol()}" else "" diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/token/internal/YieldSupplyPromoBanner.kt b/core/ui/src/main/java/com/tangem/core/ui/components/token/internal/YieldSupplyPromoBanner.kt index b949061451..71ccba070a 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/token/internal/YieldSupplyPromoBanner.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/token/internal/YieldSupplyPromoBanner.kt @@ -18,6 +18,7 @@ import android.content.res.Configuration import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.layout.Box import androidx.compose.material3.ripple +import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.remember import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.vectorResource @@ -39,6 +40,9 @@ internal fun YieldSupplyPromoBanner(state: PromoBannerState, modifier: Modifier @Composable internal fun YieldSupplyPromoBanner(state: PromoBannerState.Content, modifier: Modifier = Modifier) { + LaunchedEffect(state) { + state.onPromoShown() + } val bgColor = TangemTheme.colors.control.unchecked Column(modifier = modifier) { Row( diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/token/state/TokenItemState.kt b/core/ui/src/main/java/com/tangem/core/ui/components/token/state/TokenItemState.kt index a2180bb614..7193eed339 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/token/state/TokenItemState.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/token/state/TokenItemState.kt @@ -269,6 +269,7 @@ sealed class TokenItemState { val title: TextReference, val onPromoBannerClick: () -> Unit, val onCloseClick: () -> Unit, + val onPromoShown: () -> Unit = {}, ) : PromoBannerState() data object Empty : PromoBannerState() diff --git a/domain/yield-supply/src/main/java/com/tangem/domain/yield/supply/usecase/YieldSupplyGetRewardsBalanceUseCase.kt b/domain/yield-supply/src/main/java/com/tangem/domain/yield/supply/usecase/YieldSupplyGetRewardsBalanceUseCase.kt index f0b5c39b43..9dcaed0bd4 100644 --- a/domain/yield-supply/src/main/java/com/tangem/domain/yield/supply/usecase/YieldSupplyGetRewardsBalanceUseCase.kt +++ b/domain/yield-supply/src/main/java/com/tangem/domain/yield/supply/usecase/YieldSupplyGetRewardsBalanceUseCase.kt @@ -102,14 +102,16 @@ class YieldSupplyGetRewardsBalanceUseCase( }.flowOn(dispatcherProvider.default) private fun calculateMinVisibleDecimals(perTickDeltaAbs: BigDecimal, maxDecimals: Int): Int { - if (perTickDeltaAbs <= BigDecimal.ZERO) return MIN_DECIMALS + val effectiveMin = MIN_DECIMALS.coerceAtMost(maxDecimals) + + if (perTickDeltaAbs <= BigDecimal.ZERO) return effectiveMin val perTickAsDouble = perTickDeltaAbs.toDouble() - if (perTickAsDouble.isNaN() || perTickAsDouble.isInfinite()) return MIN_DECIMALS + if (perTickAsDouble.isNaN() || perTickAsDouble.isInfinite()) return effectiveMin val safe = if (perTickAsDouble <= 0.0) EPSILON else perTickAsDouble val raw = ceil(-ln(safe) / LN_10) - return raw.toInt().coerceIn(MIN_DECIMALS, maxDecimals) + return raw.toInt().coerceIn(effectiveMin, maxDecimals) } private fun perTickDelta(amount: BigDecimal, apyFraction: BigDecimal): BigDecimal { diff --git a/domain/yield-supply/src/test/java/com/tangem/domain/yield/supply/usecase/YieldSupplyGetRewardsBalanceUseCaseTest.kt b/domain/yield-supply/src/test/java/com/tangem/domain/yield/supply/usecase/YieldSupplyGetRewardsBalanceUseCaseTest.kt index d8b29bfd94..8f80623ff1 100644 --- a/domain/yield-supply/src/test/java/com/tangem/domain/yield/supply/usecase/YieldSupplyGetRewardsBalanceUseCaseTest.kt +++ b/domain/yield-supply/src/test/java/com/tangem/domain/yield/supply/usecase/YieldSupplyGetRewardsBalanceUseCaseTest.kt @@ -459,4 +459,72 @@ class YieldSupplyGetRewardsBalanceUseCaseTest { YieldSupplyGetRewardsBalanceUseCase.FIAT_MAX_DECIMALS, ) } + + @Test + fun `GIVEN token with decimals less than MIN_DECIMALS WHEN invoke THEN does not crash`() = runTest { + val network = createNetwork() + val tokenId = CryptoCurrency.ID( + prefix = CryptoCurrency.ID.Prefix.TOKEN_PREFIX, + body = CryptoCurrency.ID.Body.NetworkId(network.rawId), + suffix = CryptoCurrency.ID.Suffix.RawID("low-decimals-token", "0xLowDecimals"), + ) + val tokenWithLowDecimals = CryptoCurrency.Token( + id = tokenId, + network = network, + name = "Low Decimals Token", + symbol = "LDT", + decimals = 0, + iconUrl = null, + isCustom = false, + contractAddress = "0xLowDecimals", + ) + + val amount = BigDecimal("100.00") + val apy = BigDecimal("10.0") + + val status = CryptoCurrencyStatus( + currency = tokenWithLowDecimals, + value = CryptoCurrencyStatus.Custom( + amount = amount, + fiatAmount = null, + fiatRate = BigDecimal.ONE, + priceChange = null, + yieldBalance = null, + yieldSupplyStatus = null, + hasCurrentNetworkTransactions = false, + pendingTransactions = emptySet(), + networkAddress = NetworkAddress.Single( + NetworkAddress.Address(value = "0xabc", type = NetworkAddress.Address.Type.Primary), + ), + sources = CryptoCurrencyStatus.Sources(), + ), + ) + + coEvery { repository.getCachedMarkets() } returns listOf( + YieldMarketToken( + tokenAddress = tokenWithLowDecimals.contractAddress, + chainId = 1, + apy = apy, + isActive = true, + maxFeeNative = BigDecimal.ZERO, + maxFeeUSD = BigDecimal.ZERO, + backendId = "id", + ), + ) + + val dispatcherProvider = testDispatcherProvider(this) + val useCase = YieldSupplyGetRewardsBalanceUseCase(repository, dispatcherProvider) + val appCurrency = AppCurrency.Default + + val deferred = async { useCase(status, appCurrency).take(2).toList() } + + testScheduler.advanceUntilIdle() + advanceTimeBy(TICK_MILLIS) + testScheduler.advanceUntilIdle() + + val emissions = deferred.await() + assertThat(emissions).hasSize(2) + assertThat(emissions[0].cryptoBalance).isNotNull() + assertThat(emissions[1].cryptoBalance).isNotNull() + } } \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/intents/WalletContentClickIntents.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/intents/WalletContentClickIntents.kt index 4c0038bfe2..46fbfc16d4 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/intents/WalletContentClickIntents.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/intents/WalletContentClickIntents.kt @@ -7,6 +7,7 @@ import com.tangem.core.analytics.api.AnalyticsEventHandler import com.tangem.core.analytics.models.event.MainScreenAnalyticsEvent import com.tangem.core.decompose.di.ModelScoped import com.tangem.domain.models.account.Account +import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.models.currency.CryptoCurrencyStatus import com.tangem.domain.models.staking.YieldBalance import com.tangem.domain.models.wallet.UserWallet @@ -23,6 +24,7 @@ import com.tangem.domain.wallets.usecase.GetUserWalletUseCase import com.tangem.domain.yield.supply.usecase.YieldSupplyEnterStatusUseCase import com.tangem.domain.yield.supply.usecase.YieldSupplySetShouldShowMainPromoUseCase import com.tangem.feature.wallet.presentation.account.AccountDependencies +import com.tangem.feature.wallet.presentation.wallet.analytics.utils.TokenListAnalyticsSender import com.tangem.feature.wallet.presentation.wallet.domain.OnrampStatusFactory import com.tangem.feature.wallet.presentation.wallet.domain.unwrap import com.tangem.feature.wallet.presentation.wallet.state.WalletStateController @@ -60,6 +62,10 @@ internal interface WalletContentClickIntents { fun onYieldPromoCloseClick() + fun onYieldPromoShown(cryptoCurrency: CryptoCurrency) + + fun onYieldPromoClicked(cryptoCurrency: CryptoCurrency) + fun onAccountExpandClick(account: Account) fun onAccountCollapseClick(account: Account) @@ -79,7 +85,7 @@ internal interface WalletContentClickIntents { fun onNFTClick(userWallet: UserWallet) } -@Suppress("LongParameterList") +@Suppress("LongParameterList", "LargeClass") @ModelScoped internal class WalletContentClickIntentsImplementor @Inject constructor( private val stateHolder: WalletStateController, @@ -98,6 +104,7 @@ internal class WalletContentClickIntentsImplementor @Inject constructor( private val accountDependencies: AccountDependencies, private val yieldSupplyEnterStatusUseCase: YieldSupplyEnterStatusUseCase, private val yieldSupplySetShouldShowMainPromoUseCase: YieldSupplySetShouldShowMainPromoUseCase, + private val tokenListAnalyticsSender: TokenListAnalyticsSender, ) : BaseWalletClickIntents(), WalletContentClickIntents { override fun onDetailsClick() { @@ -203,6 +210,27 @@ internal class WalletContentClickIntentsImplementor @Inject constructor( } } + override fun onYieldPromoShown(cryptoCurrency: CryptoCurrency) { + modelScope.launch(dispatchers.io) { + tokenListAnalyticsSender.sendYieldPromoShown( + userWalletId = stateHolder.getSelectedWalletId(), + token = cryptoCurrency.symbol, + blockchain = cryptoCurrency.network.name, + ) + } + } + + override fun onYieldPromoClicked(cryptoCurrency: CryptoCurrency) { + modelScope.launch(dispatchers.io) { + analyticsEventHandler.send( + MainScreenAnalyticsEvent.YieldPromoClicked( + token = cryptoCurrency.symbol, + blockchain = cryptoCurrency.network.name, + ), + ) + } + } + override fun onAccountExpandClick(account: Account) { val userWalletId = stateHolder.getSelectedWalletId() accountDependencies.expandedAccountsHolder.expandAccount(userWalletId, account.accountId) diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/analytics/utils/TokenListAnalyticsSender.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/analytics/utils/TokenListAnalyticsSender.kt index fe583f7183..eb12beb3f3 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/analytics/utils/TokenListAnalyticsSender.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/analytics/utils/TokenListAnalyticsSender.kt @@ -8,6 +8,7 @@ import com.tangem.blockchainsdk.utils.fromNetworkId import com.tangem.common.extensions.isZero import com.tangem.core.analytics.api.AnalyticsEventHandler import com.tangem.core.analytics.models.AnalyticsParam +import com.tangem.core.analytics.models.event.MainScreenAnalyticsEvent import com.tangem.core.decompose.di.ModelScoped import com.tangem.domain.analytics.CheckIsWalletToppedUpUseCase import com.tangem.domain.analytics.model.WalletBalanceState @@ -34,6 +35,7 @@ internal class TokenListAnalyticsSender @Inject constructor( ) { private val balanceWasSentMap = mutableMapOf() + private val yieldPromoShownMap = mutableMapOf() private val mutex = Mutex() private val loadingTraces = mutableMapOf() @@ -233,6 +235,19 @@ internal class TokenListAnalyticsSender @Inject constructor( } } + fun sendYieldPromoShown(userWalletId: UserWalletId, token: String, blockchain: String) { + val key = "${userWalletId.stringValue}_${blockchain}_$token" + if (yieldPromoShownMap[key] == true) return + + analyticsEventHandler.send( + MainScreenAnalyticsEvent.YieldPromo( + token = token, + blockchain = blockchain, + ), + ) + yieldPromoShownMap[key] = true + } + companion object { const val BALANCE_LOADED_TRACE_NAME = "Total_balance_loaded" const val HAS_ERROR = "has_error" diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/TokenListStateConverter.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/TokenListStateConverter.kt index 2520491085..2d42286d42 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/TokenListStateConverter.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/TokenListStateConverter.kt @@ -78,6 +78,8 @@ internal class TokenListStateConverter( onItemLongClick = { _, status -> onTokenLongClick(accountId, status) }, onApyLabelClick = { status, apySource, apy -> onApyLabelClick(status, apySource, apy) }, onYieldPromoCloseClick = clickIntents::onYieldPromoCloseClick, + onYieldPromoShown = clickIntents::onYieldPromoShown, + onYieldPromoClicked = clickIntents::onYieldPromoClicked, ) override fun convert(value: WalletTokensListState): WalletTokensListState { diff --git a/gradle/tangem_dependencies.toml b/gradle/tangem_dependencies.toml index e126698726..b867761e5e 100644 --- a/gradle/tangem_dependencies.toml +++ b/gradle/tangem_dependencies.toml @@ -5,7 +5,7 @@ # https://github.com/tangem/tangem-sdk-android/ # https://github.com/tangem/vico -tangemBlockchainSdk = "releases-5.31.1-1326" +tangemBlockchainSdk = "releases-5.31.1-1334" #tangemBlockchainSdk = "0.0.1" # Keep it! - used for local builds tangemCardSdk = "releases-5.31-569" #tangemCardSdk = "0.0.1" # Keep it! - used for local builds ^