Updated on 2026-08-14
This commit is contained in:
commit
8f7b41afab
11 changed files with 174 additions and 11 deletions
|
|
@ -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<String, BigDecimal>,
|
||||
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)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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 ""
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -269,6 +269,7 @@ sealed class TokenItemState {
|
|||
val title: TextReference,
|
||||
val onPromoBannerClick: () -> Unit,
|
||||
val onCloseClick: () -> Unit,
|
||||
val onPromoShown: () -> Unit = {},
|
||||
) : PromoBannerState()
|
||||
|
||||
data object Empty : PromoBannerState()
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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<String, Boolean>()
|
||||
private val yieldPromoShownMap = mutableMapOf<String, Boolean>()
|
||||
private val mutex = Mutex()
|
||||
private val loadingTraces = mutableMapOf<UserWalletId, Trace>()
|
||||
|
||||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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 ^
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue