Updated on 2026-08-14
This commit is contained in:
commit
e855ddbc21
10 changed files with 173 additions and 60 deletions
|
|
@ -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<TangemButtonUM>, 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,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -200,25 +200,24 @@ internal class DefaultWcPairUseCase @AssistedInject constructor(
|
|||
verifyContext: Wallet.Model.VerifyContext,
|
||||
): Either<WcPairError, WcPairState.Proposal> = 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 {
|
||||
|
|
|
|||
|
|
@ -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()) }
|
||||
}
|
||||
}
|
||||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -392,7 +392,6 @@ internal class TokenDetailsModel @Inject constructor(
|
|||
redesignStateController.update(
|
||||
UpdateZeroBalanceActionsTransformer(
|
||||
actions = state.states,
|
||||
networkSource = networkSource,
|
||||
clickIntents = this@TokenDetailsModel,
|
||||
),
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue