diff --git a/core/ui/src/main/java/com/tangem/core/ui/ds/button/action/ActionButtons.kt b/core/ui/src/main/java/com/tangem/core/ui/ds/button/action/ActionButtons.kt index 22fcfa94f7..b13daec9cf 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/ds/button/action/ActionButtons.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/ds/button/action/ActionButtons.kt @@ -15,10 +15,12 @@ import androidx.compose.ui.platform.testTag import androidx.compose.ui.semantics.disabled import androidx.compose.ui.semantics.semantics import androidx.compose.ui.text.style.TextAlign +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.PreviewParameterProvider import androidx.compose.ui.unit.Constraints +import androidx.compose.ui.unit.constrainHeight import androidx.compose.ui.unit.dp import com.tangem.core.ui.R import com.tangem.core.ui.ds.button.SecondaryTangemButton @@ -62,23 +64,30 @@ fun ActionButtons(buttons: ImmutableList, modifier: Modifier = M if (measurables.isEmpty()) { return@Layout layout(constraints.minWidth, constraints.minHeight) {} } - val cellWidth = measurables.maxOf { it.maxIntrinsicWidth(constraints.maxHeight) } + val count = measurables.size + val desiredCellWidth = measurables.maxOf { it.maxIntrinsicWidth(constraints.maxHeight) } + val cellWidth = if (constraints.hasBoundedWidth) { + val maxCellWidth = ((constraints.maxWidth - spacingPx * (count - 1)) / count).coerceAtLeast(0) + desiredCellWidth.coerceAtMost(maxCellWidth) + } else { + desiredCellWidth + } val cellConstraints = Constraints( minWidth = cellWidth, maxWidth = cellWidth, minHeight = 0, - maxHeight = constraints.maxHeight, + maxHeight = Constraints.Infinity, ) val placeables = measurables.map { it.measure(cellConstraints) } val contentWidth = cellWidth * placeables.size + spacingPx * (placeables.size - 1) val width = if (constraints.hasBoundedWidth) maxOf(constraints.maxWidth, contentWidth) else contentWidth - val height = placeables.maxOf { it.height } + val height = constraints.constrainHeight(placeables.maxOf { it.height }) layout(width, height) { var x = (width - contentWidth) / 2 placeables.forEach { placeable -> - placeable.place(x = x, y = (height - placeable.height) / 2) + placeable.place(x = x, y = 0) x += cellWidth + spacingPx } } @@ -112,7 +121,8 @@ private fun ActionButton(button: TangemButtonUM, modifier: Modifier = Modifier) style = TangemTheme.typography2.subheadlineMedium14, color = textColor, textAlign = TextAlign.Center, - maxLines = 1, + maxLines = 2, + overflow = TextOverflow.Ellipsis, ) } } diff --git a/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/pair/DefaultWcPairUseCase.kt b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/pair/DefaultWcPairUseCase.kt index 16871f2f13..95f8b0eb83 100644 --- a/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/pair/DefaultWcPairUseCase.kt +++ b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/pair/DefaultWcPairUseCase.kt @@ -200,25 +200,24 @@ internal class DefaultWcPairUseCase @AssistedInject constructor( verifyContext: Wallet.Model.VerifyContext, ): Either = runCatching { val proposalAccountNetwork = associateNetworksDelegate.associateAccounts(sessionProposal) - // Display URL: shown to the user and logged to analytics. Reown's verified origin when - // present, otherwise its `verify.walletconnect.org` fallback. NOT trustworthy for - // security checks: when validation is INVALID, getDappOriginUrl returns the dApp-claimed - // origin (so the UI can show what was claimed), which a scam dApp can spoof. + // Display URL: shown to the user and logged to analytics. getDappOriginUrl() returns the + // Verify-attested origin (verifyContext.origin), or the verify.walletconnect.org fallback + // when origin is empty. Display only — the security verdict is decided below (see + // isDomainConfirmed), where sessionProposal.url is used solely for a host-equality check. val displayUrl = verifyContext.getDappOriginUrl() val verificationInfo = when { - verifyContext.validation == Wallet.Model.Validation.INVALID -> CheckDAppResult.UNSAFE verifyContext.isScam == true -> CheckDAppResult.UNSAFE - // BlockAid is scanned only against the Reown-verified origin (validation == VALID - // guarantees Reown confirmed origin matches the dApp's registered domain). - // For UNKNOWN we have no trustworthy URL: passing a dApp-claimed URL would let an - // impersonator (e.g. a scam claiming metadata.url=dydx.trade) inherit its target's - // BlockAid verdict. - verifyContext.validation == Wallet.Model.Validation.VALID -> { + // BlockAid scans only the Verify-attested origin (verifyContext.origin), reached for + // VALID or for a false-positive INVALID whose metadata host matches that origin (see + // isDomainConfirmed). For UNKNOWN there is no trustworthy origin, so BlockAid is + // skipped to avoid letting an impersonator inherit its target's verdict. + isDomainConfirmed(verifyContext, sessionProposal.url) -> { blockAidVerifier.verifyDApp(DAppData(verifyContext.origin)).getOrElse { error -> TangemLogger.withTag(WC_TAG).e("Failed to verify DApp ${sessionProposal.name}", error) CheckDAppResult.FAILED_TO_VERIFY } } + verifyContext.validation == Wallet.Model.Validation.INVALID -> CheckDAppResult.UNSAFE else -> CheckDAppResult.FAILED_TO_VERIFY } val requestedNetworks = proposalAccountNetwork @@ -254,8 +253,31 @@ internal class DefaultWcPairUseCase @AssistedInject constructor( }, ) + private fun isDomainConfirmed(verifyContext: Wallet.Model.VerifyContext, metadataUrl: String): Boolean { + return when (verifyContext.validation) { + Wallet.Model.Validation.VALID -> true + Wallet.Model.Validation.INVALID -> hostsMatchWithScheme(metadataUrl, verifyContext.origin) + else -> false + } + } + + private fun hostsMatchWithScheme(metadataUrl: String, origin: String): Boolean = runCatching { + val metadataHost = URI(metadataUrl.ensureScheme()).host?.lowercase() + val originHost = URI(origin.ensureScheme()).host?.lowercase() + !metadataHost.isNullOrEmpty() && metadataHost == originHost + }.getOrDefault(false) + + private fun String.ensureScheme(): String = + if (startsWith(HTTP_SCHEME, ignoreCase = true) || startsWith(HTTPS_SCHEME, ignoreCase = true)) { + this + } else { + HTTPS_SCHEME + this + } + private companion object { const val PENDING_SESSION_EXPIRED_DURATION_MIN = 15L + const val HTTP_SCHEME = "http://" + const val HTTPS_SCHEME = "https://" } private sealed interface TerminalAction { diff --git a/data/wallet-connect/src/test/kotlin/com/tangem/domain/walletconnect/DefaultWcPairUseCaseTest.kt b/data/wallet-connect/src/test/kotlin/com/tangem/domain/walletconnect/DefaultWcPairUseCaseTest.kt index 34caed274f..ed502a82cd 100644 --- a/data/wallet-connect/src/test/kotlin/com/tangem/domain/walletconnect/DefaultWcPairUseCaseTest.kt +++ b/data/wallet-connect/src/test/kotlin/com/tangem/domain/walletconnect/DefaultWcPairUseCaseTest.kt @@ -23,9 +23,11 @@ import com.tangem.domain.walletconnect.model.WcSession import com.tangem.domain.walletconnect.model.WcSessionApprove import com.tangem.domain.walletconnect.usecase.pair.WcPairState import io.mockk.coEvery +import io.mockk.coVerify import io.mockk.coVerifyOrder import io.mockk.mockk import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Assertions.assertInstanceOf import kotlinx.coroutines.test.runTest import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.Test @@ -287,4 +289,72 @@ internal class DefaultWcPairUseCaseTest { awaitComplete() } } + + @Test + fun `pair treats invalid validation with scheme-less metadata url matching origin as verified`() = runTest { + val schemelessProposal = sdkProposal.copy(url = "app.eigenlayer.xyz") + val invalidVerifyContext = sdkVerifyContext.copy( + origin = "https://app.eigenlayer.xyz", + validation = Wallet.Model.Validation.INVALID, + ) + coEvery { sdkDelegate.pair(url) } returns (schemelessProposal to invalidVerifyContext).right() + coEvery { associateNetworksDelegate.associateAccounts(schemelessProposal) } returns mapOf() + coEvery { blockAidVerifier.verifyDApp(any()) } returns Either.catch { CheckDAppResult.SAFE } + + val useCase = useCaseFactory() + useCase.invoke().test { + assertEquals(loading, awaitItem()) + coVerifyOrder { + sdkDelegate.pair(url) + blockAidVerifier.verifyDApp(DAppData(invalidVerifyContext.origin)) + } + val proposal = assertInstanceOf(WcPairState.Proposal::class.java, awaitItem()) + assertEquals(CheckDAppResult.SAFE, proposal.dAppSession.securityStatus) + expectNoEvents() + } + } + + @Test + fun `pair treats invalid validation with case-differing scheme-less metadata url as verified`() = runTest { + val schemelessProposal = sdkProposal.copy(url = "APP.EigenLayer.xyz") + val invalidVerifyContext = sdkVerifyContext.copy( + origin = "HTTPS://app.eigenlayer.xyz", + validation = Wallet.Model.Validation.INVALID, + ) + coEvery { sdkDelegate.pair(url) } returns (schemelessProposal to invalidVerifyContext).right() + coEvery { associateNetworksDelegate.associateAccounts(schemelessProposal) } returns mapOf() + coEvery { blockAidVerifier.verifyDApp(any()) } returns Either.catch { CheckDAppResult.SAFE } + + val useCase = useCaseFactory() + useCase.invoke().test { + assertEquals(loading, awaitItem()) + coVerifyOrder { + sdkDelegate.pair(url) + blockAidVerifier.verifyDApp(DAppData(invalidVerifyContext.origin)) + } + val proposal = assertInstanceOf(WcPairState.Proposal::class.java, awaitItem()) + assertEquals(CheckDAppResult.SAFE, proposal.dAppSession.securityStatus) + expectNoEvents() + } + } + + @Test + fun `pair keeps genuine invalid domain unsafe and skips blockaid`() = runTest { + val proposal = sdkProposal.copy(url = "https://legit-dapp.example/") + val invalidVerifyContext = sdkVerifyContext.copy( + origin = "https://phishing.example/", + validation = Wallet.Model.Validation.INVALID, + ) + coEvery { sdkDelegate.pair(url) } returns (proposal to invalidVerifyContext).right() + coEvery { associateNetworksDelegate.associateAccounts(proposal) } returns mapOf() + + val useCase = useCaseFactory() + useCase.invoke().test { + assertEquals(loading, awaitItem()) + val state = assertInstanceOf(WcPairState.Proposal::class.java, awaitItem()) + assertEquals(CheckDAppResult.UNSAFE, state.dAppSession.securityStatus) + expectNoEvents() + } + coVerify(exactly = 0) { blockAidVerifier.verifyDApp(any()) } + } } \ No newline at end of file diff --git a/features/send/api/src/main/java/com/tangem/features/send/api/analytics/CommonSendAnalyticEvents.kt b/features/send/api/src/main/java/com/tangem/features/send/api/analytics/CommonSendAnalyticEvents.kt index db765e3e92..f50e4a60fc 100644 --- a/features/send/api/src/main/java/com/tangem/features/send/api/analytics/CommonSendAnalyticEvents.kt +++ b/features/send/api/src/main/java/com/tangem/features/send/api/analytics/CommonSendAnalyticEvents.kt @@ -17,6 +17,17 @@ sealed class CommonSendAnalyticEvents( params: Map = 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, diff --git a/features/send/impl/src/main/java/com/tangem/features/send/send/DefaultSendComponent.kt b/features/send/impl/src/main/java/com/tangem/features/send/send/DefaultSendComponent.kt index ba7b0fe261..8f0b318449 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/send/DefaultSendComponent.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/send/DefaultSendComponent.kt @@ -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, diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/model/TokenDetailsModel.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/model/TokenDetailsModel.kt index e44ae5445f..89f796e859 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/model/TokenDetailsModel.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/model/TokenDetailsModel.kt @@ -392,7 +392,6 @@ internal class TokenDetailsModel @Inject constructor( redesignStateController.update( UpdateZeroBalanceActionsTransformer( actions = state.states, - networkSource = networkSource, clickIntents = this@TokenDetailsModel, ), ) diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/transformer/UpdateStakingNotificationTransformer.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/transformer/UpdateStakingNotificationTransformer.kt index 21a31432a6..0dd4747ebd 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/transformer/UpdateStakingNotificationTransformer.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/transformer/UpdateStakingNotificationTransformer.kt @@ -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, diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/transformer/UpdateZeroBalanceActionsTransformer.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/transformer/UpdateZeroBalanceActionsTransformer.kt index 097379958e..5ec1dd92d2 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/transformer/UpdateZeroBalanceActionsTransformer.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/transformer/UpdateZeroBalanceActionsTransformer.kt @@ -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, - private val networkSource: StatusSource, private val clickIntents: TokenDetailsClickIntents, ) : Transformer { @@ -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, diff --git a/features/tokendetails/impl/src/test/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/transformer/UpdateStakingNotificationTransformerTest.kt b/features/tokendetails/impl/src/test/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/transformer/UpdateStakingNotificationTransformerTest.kt index 7e312e66ee..acc2c83a3a 100644 --- a/features/tokendetails/impl/src/test/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/transformer/UpdateStakingNotificationTransformerTest.kt +++ b/features/tokendetails/impl/src/test/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/transformer/UpdateStakingNotificationTransformerTest.kt @@ -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().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 diff --git a/features/tokendetails/impl/src/test/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/transformer/UpdateZeroBalanceActionsTransformerTest.kt b/features/tokendetails/impl/src/test/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/transformer/UpdateZeroBalanceActionsTransformerTest.kt index d0649f4dac..c9ffb6737e 100644 --- a/features/tokendetails/impl/src/test/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/transformer/UpdateZeroBalanceActionsTransformerTest.kt +++ b/features/tokendetails/impl/src/test/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/transformer/UpdateZeroBalanceActionsTransformerTest.kt @@ -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, - networkSource: StatusSource = StatusSource.ACTUAL, ) = UpdateZeroBalanceActionsTransformer( actions = actions, - networkSource = networkSource, clickIntents = clickIntents, )