Updated on 2026-08-14

This commit is contained in:
Tangem 2026-07-15 17:02:09 +04:00
commit e855ddbc21
10 changed files with 173 additions and 60 deletions

View file

@ -17,6 +17,17 @@ sealed class CommonSendAnalyticEvents(
params: Map<String, String> = emptyMap(),
) : AnalyticsEvent(category = category, event = event, params = params) {
data class SendScreenOpened(
val categoryName: String,
val source: CommonSendSource,
) : CommonSendAnalyticEvents(
category = categoryName,
event = "Send Screen Opened",
params = mapOf(
SOURCE to source.analyticsName,
),
)
/** Recipient address screen opened */
data class AddressScreenOpened(
val categoryName: String,

View file

@ -82,6 +82,12 @@ internal class DefaultSendComponent @AssistedInject constructor(
)
init {
analyticsEventHandler.send(
CommonSendAnalyticEvents.SendScreenOpened(
categoryName = model.analyticCategoryName,
source = model.analyticsSendSource,
),
)
childStack.subscribe(
lifecycle = lifecycle,
mode = ObserveLifecycleMode.CREATE_DESTROY,

View file

@ -392,7 +392,6 @@ internal class TokenDetailsModel @Inject constructor(
redesignStateController.update(
UpdateZeroBalanceActionsTransformer(
actions = state.states,
networkSource = networkSource,
clickIntents = this@TokenDetailsModel,
),
)

View file

@ -215,7 +215,7 @@ internal class UpdateStakingNotificationTransformer(
)
}
@Suppress("LongMethod")
@Suppress("LongMethod", "CyclomaticComplexMethod")
private fun getRewardSubtitle(
status: CryptoCurrencyStatus,
stakingRewardAmount: BigDecimal?,
@ -245,7 +245,7 @@ internal class UpdateStakingNotificationTransformer(
RewardBlockType.CardanoNoRewards -> resourceReference(R.string.staking_cardano_details_rewards_info_text)
RewardBlockType.RewardUnavailable.DefaultRewardUnavailable,
RewardBlockType.RewardUnavailable.SolanaRewardUnavailable,
-> return null
-> rewardRateReference() ?: return null
RewardBlockType.EthereumEarnedRewards -> {
val cryptoRewardAmount = (stakingBalance as? StakingBalance.Data.P2PEthPool)?.totalRewards
return EarnBlockUM.SubtitleUM.AccentedText(
@ -286,7 +286,8 @@ internal class UpdateStakingNotificationTransformer(
}
val isAccent = rewardBlockType == RewardBlockType.Rewards ||
rewardBlockType == RewardBlockType.RewardsRequirementsError
rewardBlockType == RewardBlockType.RewardsRequirementsError ||
rewardBlockType is RewardBlockType.RewardUnavailable
return EarnBlockUM.SubtitleUM.Text(
text = text,

View file

@ -1,6 +1,5 @@
package com.tangem.feature.tokendetails.presentation.tokendetails.state.transformer
import com.tangem.domain.models.StatusSource
import com.tangem.domain.tokens.model.ScenarioUnavailabilityReason
import com.tangem.domain.tokens.model.TokenActionsState
import com.tangem.domain.tokens.model.isLoading
@ -11,7 +10,6 @@ import com.tangem.utils.transformer.Transformer
internal class UpdateZeroBalanceActionsTransformer(
private val actions: List<TokenActionsState.ActionState>,
private val networkSource: StatusSource,
private val clickIntents: TokenDetailsClickIntents,
) : Transformer<TokenDetailsUM> {
@ -29,7 +27,6 @@ internal class UpdateZeroBalanceActionsTransformer(
receive = receiveAction?.toRow(
onClick = clickIntents::onReceiveClick,
onLongClick = { clickIntents.onCopyAddress() },
forceLoading = networkSource == StatusSource.CACHE,
),
),
)
@ -38,11 +35,10 @@ internal class UpdateZeroBalanceActionsTransformer(
private fun TokenActionsState.ActionState.toRow(
onClick: (ScenarioUnavailabilityReason) -> Unit,
onLongClick: (() -> Unit)? = null,
forceLoading: Boolean = false,
): ZeroBalanceActionsUM.Row {
val reason = unavailabilityReason
return ZeroBalanceActionsUM.Row(
isLoading = reason.isLoading || forceLoading,
isLoading = reason.isLoading,
isEnabled = reason == ScenarioUnavailabilityReason.None,
onClick = { onClick(reason) },
onLongClick = onLongClick,

View file

@ -215,6 +215,34 @@ class UpdateStakingNotificationTransformerTest {
assertThat((combined.refs.data.last() as TextReference.Str).value).isNotEqualTo(THREE_STARS)
}
@Test
fun `GIVEN auto-compound rewards AND rate known WHEN transform THEN subtitle shows rate only`() {
// Arrange
val status = buildStatus(
networkRawId = "solana",
symbol = "SOL",
isCoin = true,
stakingBalance = stakeKitBalance(staked = BigDecimal("100"), rewards = BigDecimal("5")),
)
val transformer = createTransformer(
availability = availableOption(BigDecimal("4.2")),
entryInfo = StakingEntryInfo(tokenSymbol = "SOL"),
status = status,
isBalanceHidden = false,
)
// Act
val result = transformer.transform(initialState())
// Assert
val content = result.earnBlockState as EarnBlockUM.Content
val subtitle = content.subtitleUM as EarnBlockUM.SubtitleUM.Text
val rateLabel = subtitle.text as TextReference.Combined
val apyLabel = rateLabel.refs.data.filterIsInstance<TextReference.Res>().first()
assertThat(apyLabel.id).isEqualTo(CoreResR.string.staking_details_apy)
assertThat(subtitle.tone).isEqualTo(EarnBlockUM.SubtitleUM.Tone.Accent)
}
private fun rewardFormatArg(text: TextReference): Any? = when (text) {
is TextReference.Res -> text.formatArgs.data.firstOrNull()
is TextReference.Combined -> (text.refs.data.last() as? TextReference.Str)?.value

View file

@ -4,17 +4,11 @@ import com.google.common.truth.Truth.assertThat
import com.tangem.core.ui.components.containers.pullToRefresh.PullToRefreshConfig
import com.tangem.core.ui.components.marketprice.MarketPriceBlockState
import com.tangem.core.ui.extensions.stringReference
import com.tangem.domain.models.StatusSource
import com.tangem.domain.tokens.model.ScenarioUnavailabilityReason
import com.tangem.domain.tokens.model.TokenActionsState
import com.tangem.feature.tokendetails.presentation.tokendetails.model.TokenDetailsClickIntents
import com.tangem.feature.tokendetails.presentation.tokendetails.state.AddFundsUM
import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenDetailsBalanceBlockUM
import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenDetailsTopAppBarUM
import com.tangem.feature.tokendetails.presentation.tokendetails.state.*
import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenDetailsTopAppBarUM.TitleState
import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenDetailsUM
import com.tangem.feature.tokendetails.presentation.tokendetails.state.TransferUM
import com.tangem.feature.tokendetails.presentation.tokendetails.state.ZeroBalanceActionsUM
import io.mockk.mockk
import io.mockk.verify
import kotlinx.collections.immutable.persistentListOf
@ -193,37 +187,15 @@ class UpdateZeroBalanceActionsTransformerTest {
}
@Test
fun `GIVEN Receive with None reason AND networkSource is CACHE WHEN transform THEN only Receive is marked isLoading`() {
// GIVEN — networkSource=CACHE is the "still loading" signal for Receive (which has no
// Loading reason of its own). Buy/Swap rely on their own reasons and must not be flipped.
fun `GIVEN Receive action present WHEN transform THEN Receive is enabled and never loading`() {
// GIVEN — Receive only needs the local address (already available whenever the action is present),
// so it must be immediately usable and never show a spinner, regardless of network freshness.
val transformer = createTransformer(
actions = listOf(
TokenActionsState.ActionState.Buy(ScenarioUnavailabilityReason.None),
TokenActionsState.ActionState.Swap(ScenarioUnavailabilityReason.None, false),
TokenActionsState.ActionState.Swap(ScenarioUnavailabilityReason.DataLoading, false),
TokenActionsState.ActionState.Receive(ScenarioUnavailabilityReason.None),
),
networkSource = StatusSource.CACHE,
)
// WHEN
val result = transformer.transform(initialState())
// THEN
val content = result.zeroBalanceActionsUM as ZeroBalanceActionsUM.Content
assertThat(content.receive?.isLoading).isTrue()
assertThat(content.receive?.isEnabled).isTrue()
assertThat(content.buy?.isLoading).isFalse()
assertThat(content.swap?.isLoading).isFalse()
}
@Test
fun `GIVEN Receive with None reason AND networkSource is ONLY_CACHE WHEN transform THEN Receive is not loading`() {
// GIVEN — ONLY_CACHE is the terminal "refresh failed" state; Receive drops the spinner.
val transformer = createTransformer(
actions = listOf(
TokenActionsState.ActionState.Receive(ScenarioUnavailabilityReason.None),
),
networkSource = StatusSource.ONLY_CACHE,
)
// WHEN
@ -251,10 +223,8 @@ class UpdateZeroBalanceActionsTransformerTest {
private fun createTransformer(
actions: List<TokenActionsState.ActionState>,
networkSource: StatusSource = StatusSource.ACTUAL,
) = UpdateZeroBalanceActionsTransformer(
actions = actions,
networkSource = networkSource,
clickIntents = clickIntents,
)