Updated on 2026-08-14

This commit is contained in:
Tangem 2026-06-15 12:16:18 +05:00
parent f0278c85b9
commit 85cbd2669c
20 changed files with 634 additions and 1 deletions

View file

@ -9,4 +9,12 @@ android {
}
dependencies {
/** Core */
implementation(projects.core.decompose)
implementation(projects.core.ui)
/** Compose */
implementation(deps.compose.runtime)
implementation(deps.compose.foundation)
implementation(deps.compose.ui)
}

View file

@ -0,0 +1,19 @@
package com.tangem.features.virtualaccount.main.component
import androidx.compose.foundation.lazy.LazyListScope
import androidx.compose.runtime.Stable
import androidx.compose.ui.Modifier
import com.tangem.core.decompose.factory.ComponentFactory
import com.tangem.features.virtualaccount.main.entity.VirtualAccountMainUM
@Stable
interface VirtualAccountMainBlockComponent {
fun LazyListScope.virtualAccountMainContent(
state: VirtualAccountMainUM,
isBalanceHidden: Boolean,
modifier: Modifier = Modifier,
)
interface Factory : ComponentFactory<Unit, VirtualAccountMainBlockComponent>
}

View file

@ -0,0 +1,31 @@
package com.tangem.features.virtualaccount.main.entity
import androidx.compose.runtime.Immutable
import com.tangem.core.ui.extensions.TextReference
/**
* UI model for the Virtual Account main-screen block.
*
* Mirrors `TangemPayMainUM` but carries virtual-account-specific states (no card-related variants).
*/
@Immutable
sealed class VirtualAccountMainUM {
data object Empty : VirtualAccountMainUM()
data object Loading : VirtualAccountMainUM()
data class UnderReview(val subtitle: TextReference, val onClick: () -> Unit) : VirtualAccountMainUM()
data class Provisioning(val onClick: () -> Unit) : VirtualAccountMainUM()
data class CountryNotSupported(val onClick: () -> Unit) : VirtualAccountMainUM()
data class Content(
val subtitle: TextReference,
val isBalanceFlickering: Boolean,
val balance: TextReference,
val balanceSubtitle: TextReference,
val onClick: () -> Unit,
val shouldShowOnlyCacheWarning: Boolean,
) : VirtualAccountMainUM()
data object TemporaryUnavailable : VirtualAccountMainUM()
data object SyncNeeded : VirtualAccountMainUM()
data object ExposedDevice : VirtualAccountMainUM()
}

View file

@ -1,6 +1,8 @@
plugins {
alias(deps.plugins.android.library)
alias(deps.plugins.kotlin.android)
alias(deps.plugins.kotlin.kapt)
alias(deps.plugins.hilt.android)
id("configuration")
}
@ -9,4 +11,24 @@ android {
}
dependencies {
/** Core */
implementation(projects.core.decompose)
implementation(projects.core.res)
implementation(projects.core.ui)
/** Common */
implementation(projects.common.ui)
/** Features api */
implementation(projects.features.virtualAccounts.main.api)
/** Compose */
implementation(deps.compose.foundation)
implementation(deps.compose.material3)
implementation(deps.compose.ui)
implementation(deps.compose.ui.tooling)
/** DI */
implementation(deps.hilt.android)
kapt(deps.hilt.kapt)
}

View file

@ -0,0 +1,42 @@
package com.tangem.features.virtualaccount.main.component
import androidx.compose.animation.animateContentSize
import androidx.compose.foundation.lazy.LazyListScope
import androidx.compose.ui.Modifier
import com.tangem.core.decompose.context.AppComponentContext
import com.tangem.features.virtualaccount.main.entity.VirtualAccountMainUM
import com.tangem.features.virtualaccount.main.ui.VirtualAccountMainBlockContent
import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
private const val VIRTUAL_ACCOUNT_CONTENT_TYPE = "VirtualAccount"
@Suppress("UnusedPrivateProperty")
internal class DefaultVirtualAccountMainBlockComponent @AssistedInject constructor(
@Assisted context: AppComponentContext,
@Assisted params: Unit,
) : VirtualAccountMainBlockComponent, AppComponentContext by context {
override fun LazyListScope.virtualAccountMainContent(
state: VirtualAccountMainUM,
isBalanceHidden: Boolean,
modifier: Modifier,
) {
item(
key = VIRTUAL_ACCOUNT_CONTENT_TYPE,
contentType = VIRTUAL_ACCOUNT_CONTENT_TYPE,
) {
VirtualAccountMainBlockContent(
state = state,
isBalanceHidden = isBalanceHidden,
modifier = modifier.animateContentSize(),
)
}
}
@AssistedFactory
interface Factory : VirtualAccountMainBlockComponent.Factory {
override fun create(context: AppComponentContext, params: Unit): DefaultVirtualAccountMainBlockComponent
}
}

View file

@ -0,0 +1,17 @@
package com.tangem.features.virtualaccount.main.di
import com.tangem.features.virtualaccount.main.component.DefaultVirtualAccountMainBlockComponent
import com.tangem.features.virtualaccount.main.component.VirtualAccountMainBlockComponent
import dagger.Binds
import dagger.Module
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
@Module
@InstallIn(SingletonComponent::class)
internal interface VirtualAccountMainModule {
@Binds
fun bindVirtualAccountMainBlockComponent(
factory: DefaultVirtualAccountMainBlockComponent.Factory,
): VirtualAccountMainBlockComponent.Factory
}

View file

@ -0,0 +1,284 @@
package com.tangem.features.virtualaccount.main.ui
import android.content.res.Configuration
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.Icon
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.layout.layoutId
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.AnnotatedString
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider
import androidx.compose.ui.unit.dp
import com.tangem.common.ui.account.AccountIcon
import com.tangem.common.ui.account.AccountIconUM
import com.tangem.core.ui.components.CircleShimmer
import com.tangem.core.ui.components.TextShimmer
import com.tangem.core.ui.components.account.AccountIconSize
import com.tangem.core.ui.components.text.applyBladeBrush
import com.tangem.core.ui.ds.row.TangemRowContainer
import com.tangem.core.ui.ds.row.TangemRowLayoutId
import com.tangem.core.ui.extensions.*
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreviewRedesign
import com.tangem.features.virtualaccount.main.entity.VirtualAccountMainUM
import com.tangem.utils.StringsSigns.DASH_SIGN
import com.tangem.core.ui.R as CoreUiR
@Composable
internal fun VirtualAccountMainBlockContent(
state: VirtualAccountMainUM,
isBalanceHidden: Boolean,
modifier: Modifier = Modifier,
) {
when (state) {
is VirtualAccountMainUM.Empty -> Unit
is VirtualAccountMainUM.Loading -> VirtualAccountMainLoading(modifier)
is VirtualAccountMainUM.UnderReview -> VirtualAccountStateRow(
subtitle = state.subtitle,
modifier = modifier,
onClick = state.onClick,
)
is VirtualAccountMainUM.Provisioning -> VirtualAccountStateRow(
subtitle = stringReference("Setting up your account"),
modifier = modifier,
onClick = state.onClick,
)
is VirtualAccountMainUM.CountryNotSupported -> VirtualAccountStateRow(
subtitle = stringReference("Not available in your region"),
modifier = modifier,
onClick = state.onClick,
)
is VirtualAccountMainUM.Content -> VirtualAccountMainContent(state, isBalanceHidden, modifier)
is VirtualAccountMainUM.TemporaryUnavailable -> VirtualAccountStateRow(
subtitle = stringReference(DASH_SIGN),
modifier = modifier,
isEnabled = false,
)
is VirtualAccountMainUM.SyncNeeded -> VirtualAccountStateRow(
subtitle = stringReference("Virtual account session expired"),
modifier = modifier,
isEnabled = false,
)
is VirtualAccountMainUM.ExposedDevice -> VirtualAccountStateRow(
subtitle = stringReference("Unable to use on rooted devices"),
modifier = modifier,
)
}
}
@Composable
private fun VirtualAccountMainContent(
state: VirtualAccountMainUM.Content,
isBalanceHidden: Boolean,
modifier: Modifier = Modifier,
) {
TangemRowContainer(
modifier = modifier
.clip(RoundedCornerShape(size = 18.dp))
.background(TangemTheme.colors2.surface.level3)
.clickableSingle(onClick = state.onClick),
) {
AccountIcon(
name = TextReference.EMPTY,
icon = AccountIconUM.Virtual,
size = AccountIconSize.RedesignedDefault,
modifier = Modifier
.layoutId(TangemRowLayoutId.HEAD)
.padding(end = TangemTheme.dimens2.x2),
)
Text(
text = "Virtual account",
color = TangemTheme.colors2.text.neutral.primary,
style = TangemTheme.typography2.bodySemibold16,
modifier = Modifier.layoutId(TangemRowLayoutId.START_TOP),
)
Text(
text = state.subtitle.resolveReference(),
color = TangemTheme.colors2.text.neutral.secondary,
style = TangemTheme.typography2.captionSemibold12,
modifier = Modifier.layoutId(TangemRowLayoutId.START_BOTTOM),
)
VirtualAccountFiatAmount(
text = state.balance.orMaskWithStars(isBalanceHidden).resolveAnnotatedReference(),
isBalanceFlickering = state.isBalanceFlickering,
isBalanceFromCache = state.shouldShowOnlyCacheWarning,
modifier = Modifier.layoutId(TangemRowLayoutId.END_TOP),
)
Text(
text = state.balanceSubtitle.resolveReference(),
color = TangemTheme.colors2.text.neutral.secondary,
style = TangemTheme.typography2.captionSemibold12,
modifier = Modifier.layoutId(TangemRowLayoutId.END_BOTTOM),
)
}
}
@Composable
private fun VirtualAccountStateRow(
subtitle: TextReference,
modifier: Modifier = Modifier,
onClick: (() -> Unit)? = null,
isEnabled: Boolean = true,
) {
TangemRowContainer(
modifier = modifier
.clip(RoundedCornerShape(size = 18.dp))
.background(TangemTheme.colors2.surface.level3)
.conditional(onClick != null && isEnabled) { clickableSingle(onClick = requireNotNull(onClick)) },
) {
AccountIcon(
name = TextReference.EMPTY,
icon = AccountIconUM.Virtual,
size = AccountIconSize.RedesignedDefault,
modifier = Modifier
.layoutId(TangemRowLayoutId.HEAD)
.padding(end = TangemTheme.dimens2.x2),
)
Text(
text = "Virtual account",
color = if (isEnabled) {
TangemTheme.colors2.text.neutral.primary
} else {
TangemTheme.colors2.text.status.disabled
},
style = TangemTheme.typography2.bodySemibold16,
modifier = Modifier.layoutId(TangemRowLayoutId.START_TOP),
)
Text(
text = subtitle.resolveReference(),
color = if (isEnabled) {
TangemTheme.colors2.text.neutral.secondary
} else {
TangemTheme.colors2.text.status.disabled
},
style = TangemTheme.typography2.captionSemibold12,
modifier = Modifier.layoutId(TangemRowLayoutId.START_BOTTOM),
)
}
}
@Composable
private fun VirtualAccountFiatAmount(
text: AnnotatedString,
isBalanceFlickering: Boolean,
isBalanceFromCache: Boolean,
modifier: Modifier = Modifier,
) {
Row(
modifier = modifier,
verticalAlignment = Alignment.CenterVertically,
) {
AnimatedVisibility(isBalanceFromCache) {
Row(
modifier = Modifier.padding(horizontal = 4.dp),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(4.dp),
) {
Icon(
modifier = Modifier.size(12.dp),
painter = painterResource(CoreUiR.drawable.ic_error_sync_24),
tint = TangemTheme.colors2.graphic.neutral.secondary,
contentDescription = null,
)
}
}
Text(
text = text,
style = TangemTheme.typography2.bodySemibold16.applyBladeBrush(
isEnabled = isBalanceFlickering,
textColor = TangemTheme.colors2.text.neutral.primary,
),
textAlign = TextAlign.End,
)
}
}
@Composable
private fun VirtualAccountMainLoading(modifier: Modifier = Modifier) {
TangemRowContainer(
modifier = modifier
.clip(RoundedCornerShape(size = 18.dp))
.background(TangemTheme.colors2.surface.level3),
) {
CircleShimmer(
modifier = Modifier
.layoutId(TangemRowLayoutId.HEAD)
.padding(end = TangemTheme.dimens2.x2)
.size(TangemTheme.dimens2.x10),
)
TextShimmer(
style = TangemTheme.typography2.bodySemibold16,
radius = TangemTheme.dimens2.x25,
modifier = Modifier
.layoutId(TangemRowLayoutId.START_TOP)
.width(TangemTheme.dimens2.x25),
)
TextShimmer(
style = TangemTheme.typography2.captionSemibold12,
radius = TangemTheme.dimens2.x25,
modifier = Modifier
.layoutId(TangemRowLayoutId.START_BOTTOM)
.width(TangemTheme.dimens2.x11),
)
TextShimmer(
style = TangemTheme.typography2.bodyRegular16,
radius = TangemTheme.dimens2.x25,
modifier = Modifier
.layoutId(TangemRowLayoutId.END_TOP)
.width(TangemTheme.dimens2.x20),
)
TextShimmer(
style = TangemTheme.typography2.bodyRegular16,
radius = TangemTheme.dimens2.x25,
modifier = Modifier
.layoutId(TangemRowLayoutId.END_BOTTOM)
.width(TangemTheme.dimens2.x11),
)
}
}
// region Preview
@Preview(showBackground = true, widthDp = 360)
@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES)
@Composable
private fun VirtualAccountMainBlockContent_Preview(
@PreviewParameter(VirtualAccountMainBlockContentPreviewParameterProvider::class)
state: VirtualAccountMainUM,
) {
TangemThemePreviewRedesign {
VirtualAccountMainBlockContent(state = state, isBalanceHidden = false)
}
}
private class VirtualAccountMainBlockContentPreviewParameterProvider :
CollectionPreviewParameterProvider<VirtualAccountMainUM>(
collection = listOf(
VirtualAccountMainUM.Loading,
VirtualAccountMainUM.SyncNeeded,
VirtualAccountMainUM.TemporaryUnavailable,
VirtualAccountMainUM.ExposedDevice,
VirtualAccountMainUM.Provisioning(onClick = {}),
VirtualAccountMainUM.CountryNotSupported(onClick = {}),
VirtualAccountMainUM.UnderReview(subtitle = TextReference.Str("KYC in progress"), onClick = {}),
VirtualAccountMainUM.Content(
subtitle = TextReference.Str("USDC"),
isBalanceFlickering = true,
balance = TextReference.Str("$ 101.56"),
balanceSubtitle = TextReference.Str("USDC"),
onClick = {},
shouldShowOnlyCacheWarning = true,
),
),
)
// endregion

View file

@ -153,6 +153,7 @@ dependencies {
implementation(projects.features.promoBanners.api)
implementation(projects.features.tangempay.main.api)
implementation(projects.features.tangempay.details.api)
implementation(projects.features.virtualAccounts.main.api)
/** Common modules */
implementation(projects.common)

View file

@ -26,7 +26,6 @@ import com.tangem.domain.tokens.model.details.TokenAction
import com.tangem.feature.wallet.child.managetokens.AddAndManageBottomSheetComponent
import com.tangem.feature.wallet.child.organizetokens.OrganizeTokensComponent
import com.tangem.features.commonfeatures.api.addfunds.AddFundsComponent
import com.tangem.features.commonfeatures.api.portfolioselector.PortfolioSelectorComponent
import com.tangem.feature.wallet.child.tokenActions.DefaultTokenActionsComponent
import com.tangem.feature.wallet.child.tokenActions.TokenActionsComponent
import com.tangem.feature.wallet.child.wallet.model.WalletModel
@ -37,6 +36,7 @@ import com.tangem.feature.wallet.presentation.wallet.ui.WalletScreen2
import com.tangem.feature.wallet.presentation.wallet.ui.components.visa.KycRejectedComponent
import com.tangem.feature.walletsettings.component.RenameWalletComponent
import com.tangem.features.biometry.AskBiometryComponent
import com.tangem.features.commonfeatures.api.portfolioselector.PortfolioSelectorComponent
import com.tangem.features.feed.entry.components.FeedEntryComponent
import com.tangem.features.promobanners.api.PromoBannersBlockComponent
import com.tangem.features.pushnotifications.api.PushNotificationsBottomSheetComponent
@ -45,6 +45,7 @@ import com.tangem.features.send.api.NetworkSelectionComponent
import com.tangem.features.tangempay.component.TangemPayMainBlockComponent
import com.tangem.features.tangempay.components.TangemPayTransactionBottomSheetComponent
import com.tangem.features.tokenreceive.TokenReceiveComponent
import com.tangem.features.virtualaccount.main.component.VirtualAccountMainBlockComponent
import com.tangem.features.yield.supply.api.YieldSupplyDepositedWarningComponent
import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
@ -58,6 +59,7 @@ internal class WalletComponent @AssistedInject constructor(
@Assisted navigate: (WalletRoute) -> Unit,
feedEntryComponentFactory: FeedEntryComponent.Factory,
tangemPayMainBlockComponentFactory: TangemPayMainBlockComponent.Factory,
virtualAccountMainBlockComponentFactory: VirtualAccountMainBlockComponent.Factory,
private val tangemPayTransactionBottomSheetComponentFactory: TangemPayTransactionBottomSheetComponent.Factory,
private val renameWalletComponentFactory: RenameWalletComponent.Factory,
private val askBiometryComponentFactory: AskBiometryComponent.Factory,
@ -86,6 +88,12 @@ internal class WalletComponent @AssistedInject constructor(
params = Unit,
)
}
private val virtualAccountMainBlockComponent by lazy {
virtualAccountMainBlockComponentFactory.create(
context = child("virtualAccountMainBlockComponent"),
params = Unit,
)
}
private val promoBannersBlockComponent: PromoBannersBlockComponent by lazy {
promoBannersBlockComponentFactory.create(
@ -282,6 +290,7 @@ internal class WalletComponent @AssistedInject constructor(
WalletScreen2(
state = uiState,
tangemPayComponent = tangemPayMainBlockComponent,
virtualAccountComponent = virtualAccountMainBlockComponent,
bottomSheetContent = { onExpandSheet ->
BottomSheetContent(
bottomSheetState = bottomSheetState,
@ -298,6 +307,7 @@ internal class WalletComponent @AssistedInject constructor(
state = uiState,
promoBannersBlockComponent = promoBannersBlockComponent,
tangemPayComponent = tangemPayMainBlockComponent,
virtualAccountComponent = virtualAccountMainBlockComponent,
bottomSheetContent = { onExpandSheet ->
BottomSheetContent(
bottomSheetState = bottomSheetState,

View file

@ -19,6 +19,7 @@ import com.tangem.feature.wallet.presentation.preview.WalletBalancePreview
import com.tangem.feature.wallet.presentation.preview.WalletPreviewData
import com.tangem.feature.wallet.presentation.wallet.state.model.*
import com.tangem.features.tangempay.entity.TangemPayMainUM
import com.tangem.features.virtualaccount.main.entity.VirtualAccountMainUM
import kotlinx.collections.immutable.persistentListOf
internal object WalletScreenPreviewData {
@ -179,6 +180,7 @@ internal object WalletScreenPreviewData {
onItemClick = {},
),
tangemPayMainUM = TangemPayMainUM.Loading,
virtualAccountMainUM = VirtualAccountMainUM.Loading,
)
private val walletEmpty = WalletUM.Content(
@ -194,6 +196,7 @@ internal object WalletScreenPreviewData {
tokensListUM = WalletTokensListUM.Empty(onEmptyClick = {}),
nftState = WalletNFTItemUM.Hidden,
tangemPayMainUM = TangemPayMainUM.Empty,
virtualAccountMainUM = VirtualAccountMainUM.Empty,
)
private val walletAccountDefault = walletDefault.copy(

View file

@ -11,6 +11,7 @@ import com.tangem.feature.wallet.presentation.wallet.state.model.holder.LockedWa
import com.tangem.feature.wallet.presentation.wallet.state.model.holder.TxHistoryStateHolder
import com.tangem.feature.wallet.presentation.wallet.state.model.holder.WalletStateHolder
import com.tangem.features.tangempay.entity.TangemPayMainUM
import com.tangem.features.virtualaccount.main.entity.VirtualAccountMainUM
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.PersistentList
@ -25,6 +26,7 @@ internal sealed interface WalletState : WalletStateHolder {
abstract val nftState: WalletNFTItemUM
abstract val type: WalletType
abstract val tangemPayMainUM: TangemPayMainUM
abstract val virtualAccountMainUM: VirtualAccountMainUM
abstract val assetsDiscoveryProgressUM: AssetsDiscoveryProgressUM
data class Content(
@ -37,6 +39,7 @@ internal sealed interface WalletState : WalletStateHolder {
override val nftState: WalletNFTItemUM,
override val type: WalletType,
override val tangemPayMainUM: TangemPayMainUM,
override val virtualAccountMainUM: VirtualAccountMainUM = VirtualAccountMainUM.Empty,
override val assetsDiscoveryProgressUM: AssetsDiscoveryProgressUM = AssetsDiscoveryProgressUM.Idle,
) : MultiCurrency()
@ -57,6 +60,7 @@ internal sealed interface WalletState : WalletStateHolder {
override val tokensListState = WalletTokensListState.ContentState.Locked
override val nftState: WalletNFTItemUM = WalletNFTItemUM.Hidden
override val tangemPayMainUM: TangemPayMainUM = TangemPayMainUM.Empty
override val virtualAccountMainUM: VirtualAccountMainUM = VirtualAccountMainUM.Empty
override val assetsDiscoveryProgressUM: AssetsDiscoveryProgressUM = AssetsDiscoveryProgressUM.Idle
}
}

View file

@ -4,6 +4,7 @@ import androidx.compose.runtime.Immutable
import com.tangem.core.ui.components.containers.pullToRefresh.PullToRefreshConfig
import com.tangem.core.ui.ds.button.TangemButtonUM
import com.tangem.features.tangempay.entity.TangemPayMainUM
import com.tangem.features.virtualaccount.main.entity.VirtualAccountMainUM
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.PersistentList
import kotlinx.collections.immutable.persistentListOf
@ -26,6 +27,8 @@ internal sealed interface WalletUM {
val tangemPayMainUM: TangemPayMainUM
val virtualAccountMainUM: VirtualAccountMainUM
data class Content(
override val pullToRefreshConfig: PullToRefreshConfig,
override val walletsBalanceUM: WalletBalanceUM,
@ -36,6 +39,7 @@ internal sealed interface WalletUM {
override val nftState: WalletNFTItemUM,
override val type: WalletType,
override val tangemPayMainUM: TangemPayMainUM,
override val virtualAccountMainUM: VirtualAccountMainUM,
) : WalletUM
data class Locked(
@ -49,5 +53,6 @@ internal sealed interface WalletUM {
override val tokensListUM: WalletTokensListUM = WalletTokensListUM.Locked
override val nftState: WalletNFTItemUM = WalletNFTItemUM.Hidden
override val tangemPayMainUM: TangemPayMainUM = TangemPayMainUM.Empty
override val virtualAccountMainUM: VirtualAccountMainUM = VirtualAccountMainUM.Empty
}
}

View file

@ -12,6 +12,7 @@ import com.tangem.feature.wallet.presentation.wallet.state.transformers.converte
import com.tangem.feature.wallet.presentation.wallet.state.utils.disableButtons
import com.tangem.feature.wallet.presentation.wallet.state.utils.enableButtons
import com.tangem.features.tangempay.entity.TangemPayMainUM
import com.tangem.features.virtualaccount.main.entity.VirtualAccountMainUM
import com.tangem.utils.logging.TangemLogger
import java.math.BigDecimal
@ -36,6 +37,12 @@ internal class SetTokenListTransformer(
)
}
private val virtualAccountConverter by lazy {
VirtualAccountMainBlockConverter(
isRedesignEnabled = isRedesignEnabled,
)
}
override fun transform(prevState: WalletState): WalletState {
return when (prevState) {
is WalletState.MultiCurrency.Content -> {
@ -43,6 +50,7 @@ internal class SetTokenListTransformer(
walletCardState = prevState.walletCardState.toLoadedState(),
tokensListState = prevState.tokensListState.toLoadedState(),
tangemPayMainUM = prevState.tangemPayMainUM.toLoadedState(),
virtualAccountMainUM = prevState.virtualAccountMainUM.toLoadedVirtualState(),
buttons = prevState.enableButtons(),
)
}
@ -65,6 +73,7 @@ internal class SetTokenListTransformer(
walletUM.copy(
walletsBalanceUM = walletUM.walletsBalanceUM.toLoadedState2(),
tangemPayMainUM = walletUM.tangemPayMainUM.toLoadedState(),
virtualAccountMainUM = walletUM.virtualAccountMainUM.toLoadedVirtualState(),
tokensListUM = tokensListUM,
buttons = if (tokensListUM is WalletTokensListUM.Empty) {
walletUM.disableButtons()
@ -127,6 +136,17 @@ internal class SetTokenListTransformer(
return tangemPayConverter.convert(paymentAccountStatus)
}
private fun VirtualAccountMainUM.toLoadedVirtualState(): VirtualAccountMainUM {
val virtualAccountStatus = when (params) {
is TokenConverterParams.Account -> params.accountList.accountStatuses
.filterIsInstance<AccountStatus.Virtual>()
.firstOrNull()
is TokenConverterParams.Wallet -> return VirtualAccountMainUM.Empty
} ?: return this
return virtualAccountConverter.convert(virtualAccountStatus)
}
private fun toLoadedState(): WalletTokensListUM {
if (params !is TokenConverterParams.Account) {
return WalletTokensListUM.Empty(

View file

@ -0,0 +1,86 @@
package com.tangem.feature.wallet.presentation.wallet.state.transformers.converter
import androidx.compose.ui.text.SpanStyle
import com.tangem.common.ui.R
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.stringReference
import com.tangem.core.ui.format.bigdecimal.fiat
import com.tangem.core.ui.format.bigdecimal.format
import com.tangem.core.ui.format.bigdecimal.formatStyled
import com.tangem.core.ui.format.bigdecimal.getJavaCurrencyByCode
import com.tangem.core.ui.res.TangemTheme
import com.tangem.domain.models.StatusSource
import com.tangem.domain.models.account.AccountStatus
import com.tangem.domain.models.account.VirtualAccountStatusValue
import com.tangem.domain.models.kyc.KycStatus
import com.tangem.features.virtualaccount.main.entity.VirtualAccountMainUM
import com.tangem.utils.converter.Converter
import java.math.BigDecimal
internal class VirtualAccountMainBlockConverter(
private val isRedesignEnabled: Boolean,
) : Converter<AccountStatus.Virtual, VirtualAccountMainUM> {
override fun convert(value: AccountStatus.Virtual): VirtualAccountMainUM {
return when (val statusValue = value.value) {
is VirtualAccountStatusValue.Empty -> VirtualAccountMainUM.Empty
is VirtualAccountStatusValue.NotCreated -> VirtualAccountMainUM.Empty
is VirtualAccountStatusValue.Loading -> VirtualAccountMainUM.Loading
is VirtualAccountStatusValue.UnderReview -> VirtualAccountMainUM.UnderReview(
subtitle = when (statusValue.kycStatus) {
KycStatus.REJECTED -> TextReference.Res(R.string.tangempay_kyc_has_failed)
else -> TextReference.Res(R.string.tangempay_kyc_in_progress)
},
onClick = {
// TODO([REDACTED_TASK_KEY]): navigate to VA screen
},
)
is VirtualAccountStatusValue.Provisioning -> VirtualAccountMainUM.Provisioning(
onClick = {
// TODO([REDACTED_TASK_KEY]): navigate to VA screen
},
)
is VirtualAccountStatusValue.CountryNotSupported -> VirtualAccountMainUM.CountryNotSupported(
onClick = {
// TODO([REDACTED_TASK_KEY]): navigate to VA screen
},
)
is VirtualAccountStatusValue.Active -> VirtualAccountMainUM.Content(
subtitle = stringReference(statusValue.cryptoCurrency.symbol),
isBalanceFlickering = statusValue.source == StatusSource.CACHE,
balance = getBalanceText(
currencyCode = statusValue.fiatBalance.currency,
balance = statusValue.fiatBalance.availableBalance,
),
balanceSubtitle = stringReference(statusValue.cryptoCurrency.symbol),
shouldShowOnlyCacheWarning = statusValue.source == StatusSource.ONLY_CACHE,
onClick = {
// TODO([REDACTED_TASK_KEY]): navigate to VA screen
},
)
is VirtualAccountStatusValue.Error.Unavailable -> VirtualAccountMainUM.TemporaryUnavailable
is VirtualAccountStatusValue.Error.NotSynced -> VirtualAccountMainUM.SyncNeeded
is VirtualAccountStatusValue.Error.ExposedDevice -> VirtualAccountMainUM.ExposedDevice
}
}
private fun getBalanceText(currencyCode: String, balance: BigDecimal): TextReference {
val currency = getJavaCurrencyByCode(currencyCode)
val formattedBalance = if (isRedesignEnabled) {
balance.formatStyled {
fiat(
fiatCurrencyCode = currency.currencyCode,
fiatCurrencySymbol = currency.symbol,
spanStyleReference = { SpanStyle(color = TangemTheme.colors2.text.neutral.secondary) },
)
}
} else {
stringReference(
balance.format {
fiat(fiatCurrencyCode = currency.currencyCode, fiatCurrencySymbol = currency.symbol)
},
)
}
return formattedBalance
}
}

View file

@ -18,6 +18,7 @@ import com.tangem.feature.wallet.presentation.wallet.domain.WalletAdditionalInfo
import com.tangem.feature.wallet.presentation.wallet.domain.WalletImageResolver
import com.tangem.feature.wallet.presentation.wallet.state.model.*
import com.tangem.features.tangempay.entity.TangemPayMainUM
import com.tangem.features.virtualaccount.main.entity.VirtualAccountMainUM
import com.tangem.utils.extensions.addIf
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.PersistentList
@ -70,6 +71,7 @@ internal class WalletLoadingStateFactory(
is UserWallet.Hot -> WalletType.Hot
},
tangemPayMainUM = TangemPayMainUM.Empty,
virtualAccountMainUM = VirtualAccountMainUM.Empty,
)
}

View file

@ -85,15 +85,19 @@ import com.tangem.feature.wallet.presentation.wallet.ui.components.singlecurrenc
import com.tangem.feature.wallet.presentation.wallet.ui.utils.changeWalletAnimator
import com.tangem.features.tangempay.component.TangemPayMainBlockComponent
import com.tangem.features.tangempay.entity.TangemPayMainUM
import com.tangem.features.virtualaccount.main.component.VirtualAccountMainBlockComponent
import com.tangem.features.virtualaccount.main.entity.VirtualAccountMainUM
import kotlinx.collections.immutable.toImmutableList
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import kotlin.math.roundToInt
@Suppress("LongParameterList")
@Composable
internal fun WalletScreen(
state: WalletScreenState,
tangemPayComponent: TangemPayMainBlockComponent,
virtualAccountComponent: VirtualAccountMainBlockComponent,
promoBannersBlockComponent: ComposableContentComponent? = null,
bottomSheetContent: @Composable (onExpandSheet: () -> Unit) -> Unit,
bottomSheetHeaderHeightProvider: () -> Dp,
@ -109,6 +113,7 @@ internal fun WalletScreen(
WalletContent(
state = state,
tangemPayComponent = tangemPayComponent,
virtualAccountComponent = virtualAccountComponent,
walletsListState = walletsListState,
snackbarHostState = snackbarHostState,
isAutoScroll = isAutoScroll,
@ -132,6 +137,7 @@ internal fun WalletScreen(
private fun WalletContent(
state: WalletScreenState,
tangemPayComponent: TangemPayMainBlockComponent,
virtualAccountComponent: VirtualAccountMainBlockComponent,
walletsListState: LazyListState,
snackbarHostState: SnackbarHostState,
isAutoScroll: State<Boolean>,
@ -231,6 +237,13 @@ private fun WalletContent(
tangemPayComponent = tangemPayComponent,
)
virtualAccountItem(
modifier = itemModifier,
state = selectedWallet,
isHidingMode = state.isHidingMode,
virtualAccountComponent = virtualAccountComponent,
)
(selectedWallet as? WalletState.SingleCurrency)?.let { walletState ->
walletState.marketPriceBlockState?.let { marketPriceBlockState ->
marketPriceBlock(state = marketPriceBlockState, modifier = itemModifier)
@ -758,6 +771,23 @@ internal fun LazyListScope.tangemPayItem(
}
}
internal fun LazyListScope.virtualAccountItem(
state: WalletState,
isHidingMode: Boolean,
virtualAccountComponent: VirtualAccountMainBlockComponent,
modifier: Modifier = Modifier,
) {
if (state !is WalletState.MultiCurrency) return
with(virtualAccountComponent) {
virtualAccountMainContent(
modifier = modifier,
state = state.virtualAccountMainUM,
isBalanceHidden = isHidingMode,
)
}
}
@Composable
private fun ShowBottomSheet(bottomSheetConfig: TangemBottomSheetConfig?) {
if (bottomSheetConfig != null) {
@ -784,6 +814,14 @@ private fun WalletScreen_Preview(@PreviewParameter(WalletScreenPreviewProvider::
) {
}
},
virtualAccountComponent = object : VirtualAccountMainBlockComponent {
override fun LazyListScope.virtualAccountMainContent(
state: VirtualAccountMainUM,
isBalanceHidden: Boolean,
modifier: Modifier,
) {
}
},
bottomSheetContent = {
Text("Markets Content")
},

View file

@ -75,6 +75,8 @@ import com.tangem.feature.wallet.presentation.wallet.ui.components.common.Wallet
import com.tangem.feature.wallet.presentation.wallet.ui.utils.lazyListStateMapSaver
import com.tangem.features.tangempay.component.TangemPayMainBlockComponent
import com.tangem.features.tangempay.entity.TangemPayMainUM
import com.tangem.features.virtualaccount.main.component.VirtualAccountMainBlockComponent
import com.tangem.features.virtualaccount.main.entity.VirtualAccountMainUM
import dev.chrisbanes.haze.HazeProgressive
import dev.chrisbanes.haze.HazeTint
import kotlinx.coroutines.launch
@ -83,10 +85,12 @@ import kotlin.math.abs
private const val MARKET_HINT_THRESHOLD = 0.5f
@OptIn(ExperimentalDecomposeApi::class)
@Suppress("LongParameterList")
@Composable
internal fun WalletScreen2(
state: WalletScreenState,
tangemPayComponent: TangemPayMainBlockComponent,
virtualAccountComponent: VirtualAccountMainBlockComponent,
modifier: Modifier = Modifier,
bottomSheetContent: @Composable (onExpandSheet: () -> Unit) -> Unit,
bottomSheetHeaderHeightProvider: () -> Dp,
@ -132,6 +136,7 @@ internal fun WalletScreen2(
state = state,
walletsPagerState = walletsPagerState,
tangemPayComponent = tangemPayComponent,
virtualAccountComponent = virtualAccountComponent,
behavior = behavior,
bottomSheetContent = bottomSheetContent,
bottomSheetHeaderHeightProvider = bottomSheetHeaderHeightProvider,
@ -159,6 +164,7 @@ private fun WalletContent2(
state: WalletScreenState,
walletsPagerState: PagerState,
tangemPayComponent: TangemPayMainBlockComponent,
virtualAccountComponent: VirtualAccountMainBlockComponent,
behavior: TangemCollapsingAppBarBehavior,
listStates: Map<Int, LazyListState>,
modifier: Modifier = Modifier,
@ -330,6 +336,7 @@ private fun WalletContent2(
isBalanceHidden = state.isHidingMode,
contentPadding = contentPadding,
tangemPayComponent = tangemPayComponent,
virtualAccountComponent = virtualAccountComponent,
modifier = Modifier
.fillMaxSize()
.nestedScroll(behavior.nestedScrollConnection),
@ -655,6 +662,14 @@ private fun WalletScreen2_Preview(@PreviewParameter(WalletScreen2PreviewProvider
) {
}
},
virtualAccountComponent = object : VirtualAccountMainBlockComponent {
override fun LazyListScope.virtualAccountMainContent(
state: VirtualAccountMainUM,
isBalanceHidden: Boolean,
modifier: Modifier,
) {
}
},
bottomSheetContent = {
Text("Markets Content")
},

View file

@ -10,6 +10,8 @@ import com.tangem.feature.wallet.impl.R
import com.tangem.feature.wallet.presentation.wallet.state.model.WalletUM
import com.tangem.features.tangempay.component.TangemPayMainBlockComponent
import com.tangem.features.tangempay.entity.TangemPayMainUM
import com.tangem.features.virtualaccount.main.component.VirtualAccountMainBlockComponent
import com.tangem.features.virtualaccount.main.entity.VirtualAccountMainUM
internal fun LazyListScope.nftCollections2(state: WalletUM, itemModifier: Modifier) {
(state as? WalletUM.Content)?.let { content ->
@ -51,4 +53,15 @@ internal fun LazyListScope.tangemPay(
with(tangemPayComponent) {
tangemPayMainContent(modifier = modifier, state = tangemPayUM, isBalanceHidden = isBalanceHidden)
}
}
internal fun LazyListScope.virtualAccount(
virtualAccountComponent: VirtualAccountMainBlockComponent,
virtualAccountUM: VirtualAccountMainUM,
isBalanceHidden: Boolean,
modifier: Modifier = Modifier,
) {
with(virtualAccountComponent) {
virtualAccountMainContent(modifier = modifier, state = virtualAccountUM, isBalanceHidden = isBalanceHidden)
}
}

View file

@ -24,15 +24,19 @@ import com.tangem.feature.wallet.presentation.wallet.ui.components.multicurrency
import com.tangem.feature.wallet.presentation.wallet.ui.components.nftCollections2
import com.tangem.feature.wallet.presentation.wallet.ui.components.organizeTokens2
import com.tangem.feature.wallet.presentation.wallet.ui.components.tangemPay
import com.tangem.feature.wallet.presentation.wallet.ui.components.virtualAccount
import com.tangem.features.tangempay.component.TangemPayMainBlockComponent
import com.tangem.features.virtualaccount.main.component.VirtualAccountMainBlockComponent
import kotlinx.collections.immutable.toPersistentList
@Suppress("LongParameterList")
@Composable
internal fun WalletListContent(
currentWallet: WalletUM,
isBalanceHidden: Boolean,
listState: LazyListState,
tangemPayComponent: TangemPayMainBlockComponent,
virtualAccountComponent: VirtualAccountMainBlockComponent,
contentPadding: PaddingValues,
modifier: Modifier = Modifier,
) {
@ -66,6 +70,13 @@ internal fun WalletListContent(
modifier = itemModifier,
)
virtualAccount(
virtualAccountComponent = virtualAccountComponent,
virtualAccountUM = currentWallet.virtualAccountMainUM,
isBalanceHidden = isBalanceHidden,
modifier = itemModifier,
)
tokensListItems2(
walletTokensListUM = currentWallet.tokensListUM,
modifier = movableItemModifier,

View file

@ -26,6 +26,7 @@ import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.feature.wallet.child.wallet.model.intents.WalletClickIntents
import com.tangem.feature.wallet.presentation.wallet.state.model.*
import com.tangem.features.tangempay.entity.TangemPayMainUM
import com.tangem.features.virtualaccount.main.entity.VirtualAccountMainUM
import io.mockk.every
import io.mockk.mockk
import kotlinx.collections.immutable.persistentListOf
@ -133,6 +134,7 @@ class SetTokenListTransformerTest {
nftState = WalletNFTItemUM.Hidden,
type = WalletType.Hot,
tangemPayMainUM = TangemPayMainUM.Empty,
virtualAccountMainUM = VirtualAccountMainUM.Empty,
)
private fun createToken(): CryptoCurrency.Token {