From 285bed34a40209b1653e69570febf97e437b465e Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 24 Jun 2026 01:21:15 -0700 Subject: [PATCH] Updated on 2026-08-14 --- .../details/api/build.gradle.kts | 9 + .../component/VirtualAccountMainComponent.kt | 14 ++ .../details/impl/build.gradle.kts | 20 +- .../common/ui/TangemBalanceHeader.kt | 115 +++++++++ .../common/ui/TangemBalanceHeaderState.kt | 18 ++ .../common/ui/TangemCircleActionButton.kt | 70 ++++++ .../common/ui/TangemEmptyState.kt | 73 ++++++ ...sModule.kt => VirtualAccountMainModule.kt} | 2 +- .../DefaultVirtualAccountMainComponent.kt | 34 +++ .../main/VirtualAccountMainModel.kt | 46 ++++ .../main/VirtualAccountMainScreen.kt | 229 ++++++++++++++++++ .../main/VirtualAccountMainUM.kt | 29 +++ .../di/VirtualAccountMainComponentModule.kt | 18 ++ .../main/di/VirtualAccountMainModelModule.kt | 20 ++ .../extension/BaseExtensionConfigurations.kt | 1 + 15 files changed, 694 insertions(+), 4 deletions(-) create mode 100644 features/virtual-accounts/details/api/src/main/kotlin/com/tangem/features/virtualaccount/details/component/VirtualAccountMainComponent.kt create mode 100644 features/virtual-accounts/details/impl/src/main/kotlin/com/tangem/features/virtualaccount/common/ui/TangemBalanceHeader.kt create mode 100644 features/virtual-accounts/details/impl/src/main/kotlin/com/tangem/features/virtualaccount/common/ui/TangemBalanceHeaderState.kt create mode 100644 features/virtual-accounts/details/impl/src/main/kotlin/com/tangem/features/virtualaccount/common/ui/TangemCircleActionButton.kt create mode 100644 features/virtual-accounts/details/impl/src/main/kotlin/com/tangem/features/virtualaccount/common/ui/TangemEmptyState.kt rename features/virtual-accounts/details/impl/src/main/kotlin/com/tangem/features/virtualaccount/di/{VirtualAccountDetailsModule.kt => VirtualAccountMainModule.kt} (94%) create mode 100644 features/virtual-accounts/details/impl/src/main/kotlin/com/tangem/features/virtualaccount/main/DefaultVirtualAccountMainComponent.kt create mode 100644 features/virtual-accounts/details/impl/src/main/kotlin/com/tangem/features/virtualaccount/main/VirtualAccountMainModel.kt create mode 100644 features/virtual-accounts/details/impl/src/main/kotlin/com/tangem/features/virtualaccount/main/VirtualAccountMainScreen.kt create mode 100644 features/virtual-accounts/details/impl/src/main/kotlin/com/tangem/features/virtualaccount/main/VirtualAccountMainUM.kt create mode 100644 features/virtual-accounts/details/impl/src/main/kotlin/com/tangem/features/virtualaccount/main/di/VirtualAccountMainComponentModule.kt create mode 100644 features/virtual-accounts/details/impl/src/main/kotlin/com/tangem/features/virtualaccount/main/di/VirtualAccountMainModelModule.kt diff --git a/features/virtual-accounts/details/api/build.gradle.kts b/features/virtual-accounts/details/api/build.gradle.kts index ccb34f0307..1f5657de2a 100644 --- a/features/virtual-accounts/details/api/build.gradle.kts +++ b/features/virtual-accounts/details/api/build.gradle.kts @@ -9,4 +9,13 @@ android { } dependencies { + /** Core */ + api(projects.core.decompose) + api(projects.core.ui) + + /** Domain */ + api(projects.domain.models) + + /** Compose */ + implementation(deps.compose.runtime) } \ No newline at end of file diff --git a/features/virtual-accounts/details/api/src/main/kotlin/com/tangem/features/virtualaccount/details/component/VirtualAccountMainComponent.kt b/features/virtual-accounts/details/api/src/main/kotlin/com/tangem/features/virtualaccount/details/component/VirtualAccountMainComponent.kt new file mode 100644 index 0000000000..cd452567a0 --- /dev/null +++ b/features/virtual-accounts/details/api/src/main/kotlin/com/tangem/features/virtualaccount/details/component/VirtualAccountMainComponent.kt @@ -0,0 +1,14 @@ +package com.tangem.features.virtualaccount.details.component + +import com.tangem.core.decompose.factory.ComponentFactory +import com.tangem.core.ui.decompose.ComposableContentComponent +import com.tangem.domain.models.wallet.UserWalletId + +interface VirtualAccountMainComponent : ComposableContentComponent { + + data class Params( + val userWalletId: UserWalletId, + ) + + interface Factory : ComponentFactory +} \ No newline at end of file diff --git a/features/virtual-accounts/details/impl/build.gradle.kts b/features/virtual-accounts/details/impl/build.gradle.kts index 3fec5d85d5..1d9448064a 100644 --- a/features/virtual-accounts/details/impl/build.gradle.kts +++ b/features/virtual-accounts/details/impl/build.gradle.kts @@ -11,11 +11,25 @@ android { } dependencies { + /** Core */ + implementation(projects.core.configToggles) + implementation(projects.core.decompose) + implementation(projects.core.res) + implementation(projects.core.ui) + implementation(projects.core.utils) + + /** Domain */ + implementation(projects.domain.models) + + /** Features */ implementation(projects.features.virtualAccounts.details.api) - implementation(projects.core.configToggles) - - implementation(deps.compose.runtime) + /** Compose */ + implementation(deps.compose.foundation) + implementation(deps.compose.material3) + implementation(deps.compose.ui) + implementation(deps.compose.ui.tooling) + implementation(deps.decompose.ext.compose) /** DI */ implementation(deps.hilt.android) diff --git a/features/virtual-accounts/details/impl/src/main/kotlin/com/tangem/features/virtualaccount/common/ui/TangemBalanceHeader.kt b/features/virtual-accounts/details/impl/src/main/kotlin/com/tangem/features/virtualaccount/common/ui/TangemBalanceHeader.kt new file mode 100644 index 0000000000..6ec531b3d2 --- /dev/null +++ b/features/virtual-accounts/details/impl/src/main/kotlin/com/tangem/features/virtualaccount/common/ui/TangemBalanceHeader.kt @@ -0,0 +1,115 @@ +package com.tangem.features.virtualaccount.common.ui + +import android.content.res.Configuration +import androidx.compose.animation.AnimatedContent +import androidx.compose.animation.core.tween +import androidx.compose.animation.fadeIn +import androidx.compose.animation.fadeOut +import androidx.compose.animation.togetherWith +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.text.TextAutoSize +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import com.tangem.core.ui.components.text.applyBladeBrush +import com.tangem.core.ui.ds2.shimmers.TextShimmer +import com.tangem.core.ui.ds2.shimmers.TextShimmerStyle +import com.tangem.core.ui.extensions.* +import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.res.TangemThemePreviewRedesign +import com.tangem.utils.StringsSigns.DASH_SIGN + +@Composable +fun TangemBalanceHeader( + state: TangemBalanceHeaderState, + label: TextReference, + modifier: Modifier = Modifier, + balanceModifier: Modifier = Modifier, +) { + Column( + modifier = modifier, + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens2.x2), + ) { + AnimatedContent( + targetState = state, + label = "Updating the balance", + transitionSpec = { + fadeIn(animationSpec = tween(durationMillis = 220, delayMillis = 90)) togetherWith + fadeOut(animationSpec = tween(durationMillis = 90)) + }, + ) { animatedState -> + when (animatedState) { + is TangemBalanceHeaderState.Loading -> TextShimmer( + modifier = Modifier.size(width = 160.dp, height = 56.dp), + text = "1234.00", + style = TextShimmerStyle.HEADING_MEDIUM, + radius = TangemTheme.dimens2.x25, + ) + is TangemBalanceHeaderState.Content -> Text( + modifier = balanceModifier, + text = animatedState.balance + .orMaskWithStars(animatedState.isBalanceHidden) + .resolveAnnotatedReference(), + style = TangemTheme.typography3.display.medium.applyBladeBrush( + isEnabled = animatedState.isFlickering, + textColor = TangemTheme.colors3.text.primary, + ), + color = TangemTheme.colors3.text.primary, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + autoSize = TextAutoSize.StepBased( + minFontSize = TangemTheme.typography3.heading.medium.fontSize, + maxFontSize = TangemTheme.typography3.display.medium.fontSize, + ), + ) + is TangemBalanceHeaderState.Error -> Text( + modifier = balanceModifier, + text = DASH_SIGN, + style = TangemTheme.typography3.display.medium, + color = TangemTheme.colors3.text.primary, + ) + } + } + Text( + modifier = Modifier.padding(vertical = TangemTheme.dimens2.x1), + text = label.resolveReference(), + color = TangemTheme.colors3.text.secondary, + style = TangemTheme.typography3.caption.medium, + ) + } +} + +@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES) +@Composable +private fun TangemBalanceHeaderPreview() { + TangemThemePreviewRedesign { + Column( + modifier = Modifier.fillMaxWidth(), + verticalArrangement = Arrangement.spacedBy(16.dp), + ) { + TangemBalanceHeader( + modifier = Modifier.fillMaxWidth(), + state = TangemBalanceHeaderState.Content( + balance = stringReference("$0.00"), + isBalanceHidden = false, + ), + label = stringReference("Total balance"), + ) + TangemBalanceHeader( + modifier = Modifier.fillMaxWidth(), + state = TangemBalanceHeaderState.Loading, + label = stringReference("Total balance"), + ) + TangemBalanceHeader( + modifier = Modifier.fillMaxWidth(), + state = TangemBalanceHeaderState.Error, + label = stringReference("Total balance"), + ) + } + } +} \ No newline at end of file diff --git a/features/virtual-accounts/details/impl/src/main/kotlin/com/tangem/features/virtualaccount/common/ui/TangemBalanceHeaderState.kt b/features/virtual-accounts/details/impl/src/main/kotlin/com/tangem/features/virtualaccount/common/ui/TangemBalanceHeaderState.kt new file mode 100644 index 0000000000..2bbe065b20 --- /dev/null +++ b/features/virtual-accounts/details/impl/src/main/kotlin/com/tangem/features/virtualaccount/common/ui/TangemBalanceHeaderState.kt @@ -0,0 +1,18 @@ +package com.tangem.features.virtualaccount.common.ui + +import androidx.compose.runtime.Immutable +import com.tangem.core.ui.extensions.TextReference + +@Immutable +sealed interface TangemBalanceHeaderState { + + data object Loading : TangemBalanceHeaderState + + data class Content( + val balance: TextReference, + val isBalanceHidden: Boolean, + val isFlickering: Boolean = false, + ) : TangemBalanceHeaderState + + data object Error : TangemBalanceHeaderState +} \ No newline at end of file diff --git a/features/virtual-accounts/details/impl/src/main/kotlin/com/tangem/features/virtualaccount/common/ui/TangemCircleActionButton.kt b/features/virtual-accounts/details/impl/src/main/kotlin/com/tangem/features/virtualaccount/common/ui/TangemCircleActionButton.kt new file mode 100644 index 0000000000..62ad094981 --- /dev/null +++ b/features/virtual-accounts/details/impl/src/main/kotlin/com/tangem/features/virtualaccount/common/ui/TangemCircleActionButton.kt @@ -0,0 +1,70 @@ +package com.tangem.features.virtualaccount.common.ui + +import android.content.res.Configuration +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.text.TextAutoSize +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.tooling.preview.Preview +import com.tangem.core.ui.ds.image.TangemIconUM +import com.tangem.core.ui.ds2.button.TangemButton +import com.tangem.core.ui.extensions.TextReference +import com.tangem.core.ui.extensions.resolveAnnotatedReference +import com.tangem.core.ui.extensions.stringReference +import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.res.TangemThemePreviewRedesign +import com.tangem.core.ui.res.generated.icons.Icons +import com.tangem.core.ui.res.generated.icons.ic_arrow_down_24 + +@Composable +fun TangemCircleActionButton( + title: TextReference, + icon: TangemIconUM, + onClick: () -> Unit, + modifier: Modifier = Modifier, + isEnabled: Boolean = true, + isLoading: Boolean = false, +) { + Column( + modifier = modifier, + verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens2.x2), + horizontalAlignment = Alignment.CenterHorizontally, + ) { + TangemButton( + variant = TangemButton.Variant.Material, + size = TangemButton.Size.X14, + onClick = onClick, + iconStart = icon, + isLoading = isLoading, + isEnabled = isEnabled, + ) + Text( + text = title.resolveAnnotatedReference(), + style = TangemTheme.typography3.subheading.medium, + color = TangemTheme.colors3.text.primary, + autoSize = TextAutoSize.StepBased( + minFontSize = TangemTheme.typography3.caption.medium.fontSize, + maxFontSize = TangemTheme.typography3.subheading.medium.fontSize, + ), + maxLines = 1, + ) + } +} + +@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES) +@Composable +private fun TangemCircleActionButtonPreview() { + TangemThemePreviewRedesign { + TangemCircleActionButton( + title = stringReference("Action"), + icon = TangemIconUM.Icon( + imageVector = Icons.ic_arrow_down_24, + tintReference = { TangemTheme.colors3.icon.primary }, + ), + onClick = {}, + ) + } +} \ No newline at end of file diff --git a/features/virtual-accounts/details/impl/src/main/kotlin/com/tangem/features/virtualaccount/common/ui/TangemEmptyState.kt b/features/virtual-accounts/details/impl/src/main/kotlin/com/tangem/features/virtualaccount/common/ui/TangemEmptyState.kt new file mode 100644 index 0000000000..672ee4004f --- /dev/null +++ b/features/virtual-accounts/details/impl/src/main/kotlin/com/tangem/features/virtualaccount/common/ui/TangemEmptyState.kt @@ -0,0 +1,73 @@ +package com.tangem.features.virtualaccount.common.ui + +import android.content.res.Configuration +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.shape.CircleShape +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.graphics.vector.ImageVector +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import com.tangem.core.ui.extensions.TextReference +import com.tangem.core.ui.extensions.resolveReference +import com.tangem.core.ui.extensions.stringReference +import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.res.TangemThemePreviewRedesign +import com.tangem.core.ui.res.generated.icons.Icons +import com.tangem.core.ui.res.generated.icons.ic_binoculars_20 + +@Composable +fun TangemEmptyState( + icon: ImageVector, + text: TextReference, + modifier: Modifier = Modifier, + iconModifier: Modifier = Modifier, + textModifier: Modifier = Modifier, +) { + Column( + modifier = modifier, + verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens2.x3, Alignment.CenterVertically), + horizontalAlignment = Alignment.CenterHorizontally, + ) { + Icon( + modifier = Modifier + .size(TangemTheme.dimens2.x10) + .background( + color = TangemTheme.colors3.bg.opaque.primary, + shape = CircleShape, + ) + .padding(10.dp) + .then(iconModifier), + imageVector = icon, + tint = TangemTheme.colors3.icon.secondary, + contentDescription = null, + ) + + Text( + modifier = textModifier, + textAlign = TextAlign.Center, + text = text.resolveReference(), + style = TangemTheme.typography3.caption.medium, + color = TangemTheme.colors3.text.secondary, + ) + } +} + +@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES) +@Composable +private fun TangemEmptyStatePreview() { + TangemThemePreviewRedesign { + TangemEmptyState( + icon = Icons.ic_binoculars_20, + text = stringReference("No transactions yet\nStart spending and see history here"), + ) + } +} \ No newline at end of file diff --git a/features/virtual-accounts/details/impl/src/main/kotlin/com/tangem/features/virtualaccount/di/VirtualAccountDetailsModule.kt b/features/virtual-accounts/details/impl/src/main/kotlin/com/tangem/features/virtualaccount/di/VirtualAccountMainModule.kt similarity index 94% rename from features/virtual-accounts/details/impl/src/main/kotlin/com/tangem/features/virtualaccount/di/VirtualAccountDetailsModule.kt rename to features/virtual-accounts/details/impl/src/main/kotlin/com/tangem/features/virtualaccount/di/VirtualAccountMainModule.kt index 8e35f7eb24..d3a53a6e88 100644 --- a/features/virtual-accounts/details/impl/src/main/kotlin/com/tangem/features/virtualaccount/di/VirtualAccountDetailsModule.kt +++ b/features/virtual-accounts/details/impl/src/main/kotlin/com/tangem/features/virtualaccount/di/VirtualAccountMainModule.kt @@ -11,7 +11,7 @@ import javax.inject.Singleton @Module @InstallIn(SingletonComponent::class) -internal object VirtualAccountDetailsModule { +internal object VirtualAccountMainModule { @Provides @Singleton diff --git a/features/virtual-accounts/details/impl/src/main/kotlin/com/tangem/features/virtualaccount/main/DefaultVirtualAccountMainComponent.kt b/features/virtual-accounts/details/impl/src/main/kotlin/com/tangem/features/virtualaccount/main/DefaultVirtualAccountMainComponent.kt new file mode 100644 index 0000000000..7ad7ef7c21 --- /dev/null +++ b/features/virtual-accounts/details/impl/src/main/kotlin/com/tangem/features/virtualaccount/main/DefaultVirtualAccountMainComponent.kt @@ -0,0 +1,34 @@ +package com.tangem.features.virtualaccount.main + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.ui.Modifier +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import com.tangem.core.decompose.context.AppComponentContext +import com.tangem.core.decompose.model.getOrCreateModel +import com.tangem.features.virtualaccount.details.component.VirtualAccountMainComponent +import dagger.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject + +internal class DefaultVirtualAccountMainComponent @AssistedInject constructor( + @Assisted appComponentContext: AppComponentContext, + @Assisted params: VirtualAccountMainComponent.Params, +) : VirtualAccountMainComponent, AppComponentContext by appComponentContext { + + private val model: VirtualAccountMainModel = getOrCreateModel(params = params) + + @Composable + override fun Content(modifier: Modifier) { + val state by model.uiState.collectAsStateWithLifecycle() + VirtualAccountMainScreen(state = state, modifier = modifier) + } + + @AssistedFactory + interface Factory : VirtualAccountMainComponent.Factory { + override fun create( + context: AppComponentContext, + params: VirtualAccountMainComponent.Params, + ): DefaultVirtualAccountMainComponent + } +} \ No newline at end of file diff --git a/features/virtual-accounts/details/impl/src/main/kotlin/com/tangem/features/virtualaccount/main/VirtualAccountMainModel.kt b/features/virtual-accounts/details/impl/src/main/kotlin/com/tangem/features/virtualaccount/main/VirtualAccountMainModel.kt new file mode 100644 index 0000000000..65a9fd1e60 --- /dev/null +++ b/features/virtual-accounts/details/impl/src/main/kotlin/com/tangem/features/virtualaccount/main/VirtualAccountMainModel.kt @@ -0,0 +1,46 @@ +package com.tangem.features.virtualaccount.main + +import androidx.compose.runtime.Stable +import com.tangem.core.decompose.di.ModelScoped +import com.tangem.core.decompose.model.Model +import com.tangem.core.decompose.model.ParamsContainer +import com.tangem.core.decompose.navigation.Router +import com.tangem.core.ui.extensions.resourceReference +import com.tangem.core.ui.extensions.stringReference +import com.tangem.features.virtualaccount.details.component.VirtualAccountMainComponent +import com.tangem.features.virtualaccount.details.impl.R +import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import javax.inject.Inject + +@Stable +@ModelScoped +internal class VirtualAccountMainModel @Inject constructor( + paramsContainer: ParamsContainer, + override val dispatchers: CoroutineDispatcherProvider, + private val router: Router, +) : Model() { + + @Suppress("UnusedPrivateProperty") + private val params = paramsContainer.require() + + val uiState: StateFlow + field = MutableStateFlow( + createInitialState(), + ) + + private fun createInitialState(): VirtualAccountMainUM = VirtualAccountMainUM( + title = resourceReference(R.string.virtual_account_title), + subtitle = resourceReference(R.string.tangempay_usdc_on_polygon_network), + balance = VirtualAccountBalanceBlockState.Content( + fiatBalance = stringReference("$0.00"), + isBalanceFlickering = false, + ), + isBalanceHidden = false, + onBackClick = { router.pop() }, + onMenuClick = {}, + onAddFundsClick = {}, + onSendClick = {}, + ) +} \ No newline at end of file diff --git a/features/virtual-accounts/details/impl/src/main/kotlin/com/tangem/features/virtualaccount/main/VirtualAccountMainScreen.kt b/features/virtual-accounts/details/impl/src/main/kotlin/com/tangem/features/virtualaccount/main/VirtualAccountMainScreen.kt new file mode 100644 index 0000000000..98d4289358 --- /dev/null +++ b/features/virtual-accounts/details/impl/src/main/kotlin/com/tangem/features/virtualaccount/main/VirtualAccountMainScreen.kt @@ -0,0 +1,229 @@ +package com.tangem.features.virtualaccount.main + +import android.content.res.Configuration +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.lazy.* +import androidx.compose.runtime.* +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.layout.onSizeChanged +import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.tooling.preview.Devices +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp +import com.tangem.core.ui.components.SpacerH24 +import com.tangem.core.ui.components.topFade +import com.tangem.core.ui.ds.image.TangemIconUM +import com.tangem.core.ui.ds.topbar.TangemTopBar +import com.tangem.core.ui.ds2.button.TangemButton +import com.tangem.core.ui.extensions.resourceReference +import com.tangem.core.ui.extensions.stringReference +import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.res.TangemThemePreviewRedesign +import com.tangem.core.ui.res.generated.icons.* +import com.tangem.features.virtualaccount.common.ui.TangemBalanceHeader +import com.tangem.features.virtualaccount.common.ui.TangemBalanceHeaderState +import com.tangem.features.virtualaccount.common.ui.TangemCircleActionButton +import com.tangem.features.virtualaccount.common.ui.TangemEmptyState +import com.tangem.features.virtualaccount.details.impl.R +import com.tangem.core.ui.R as CoreUiR + +private val InitialTopBarHeight: Dp = 64.dp +private const val TOP_FADE_MID_STOP = 0.8f +private const val TOP_FADE_MID_ALPHA = 0.8f + +@Composable +internal fun VirtualAccountMainScreen(state: VirtualAccountMainUM, modifier: Modifier = Modifier) { + val listState = rememberLazyListState() + val density = LocalDensity.current + val statusBarHeight = with(density) { WindowInsets.systemBars.getTop(this).toDp() } + val bottomBarHeight = with(density) { WindowInsets.systemBars.getBottom(this).toDp() } + var topBarTotalHeight by remember { mutableStateOf(InitialTopBarHeight + statusBarHeight) } + val rootBackground = TangemTheme.colors3.bg.primary + + Box( + modifier = modifier + .fillMaxSize() + .background(rootBackground), + ) { + LazyColumn( + modifier = Modifier + .fillMaxSize() + .topFade( + height = topBarTotalHeight, + 0f to rootBackground, + TOP_FADE_MID_STOP to rootBackground.copy(alpha = TOP_FADE_MID_ALPHA), + 1f to Color.Transparent, + ), + horizontalAlignment = Alignment.CenterHorizontally, + state = listState, + contentPadding = PaddingValues( + top = topBarTotalHeight, + bottom = TangemTheme.dimens2.x4 + bottomBarHeight, + ), + ) { + body( + state = state, + listState = listState, + ) + } + TopBar( + state = state, + onHeightChange = { measuredHeight -> + if (topBarTotalHeight != measuredHeight) topBarTotalHeight = measuredHeight + }, + ) + } +} + +private fun LazyListScope.body(state: VirtualAccountMainUM, listState: LazyListState) { + item("balanceBlock") { + BalanceBlock( + state = state.balance, + isBalanceHidden = state.isBalanceHidden, + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = TangemTheme.dimens2.x4) + .padding(top = TangemTheme.dimens2.x12), + ) + } + item("actionButtonsBlock") { + SpacerH24() + ActionBlock(state = state) + } + item("emptyTransactions") { + SpacerH24() + TangemEmptyState( + icon = Icons.ic_binoculars_20, + text = resourceReference(R.string.virtual_account_transactions_empty), + modifier = Modifier + .heightIn(min = rememberRemainingViewportHeight(listState, "emptyTransactions")) + .fillMaxWidth() + .padding(horizontal = TangemTheme.dimens2.x4, vertical = TangemTheme.dimens2.x3), + ) + } +} + +@Composable +private fun BalanceBlock( + state: VirtualAccountBalanceBlockState, + isBalanceHidden: Boolean, + modifier: Modifier = Modifier, +) { + TangemBalanceHeader( + state = when (state) { + is VirtualAccountBalanceBlockState.Loading -> TangemBalanceHeaderState.Loading + is VirtualAccountBalanceBlockState.Content -> TangemBalanceHeaderState.Content( + balance = state.fiatBalance, + isFlickering = state.isBalanceFlickering, + isBalanceHidden = isBalanceHidden, + ) + is VirtualAccountBalanceBlockState.Error -> TangemBalanceHeaderState.Error + }, + label = resourceReference(R.string.token_details_balance_total), + modifier = modifier, + ) +} + +@Composable +private fun LazyItemScope.ActionBlock(state: VirtualAccountMainUM, modifier: Modifier = Modifier) { + Row( + modifier = modifier.fillMaxWidth(), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.Center, + ) { + TangemCircleActionButton( + modifier = Modifier.padding(horizontal = TangemTheme.dimens2.x4), + title = resourceReference(R.string.common_add_funds), + icon = TangemIconUM.Icon( + imageVector = Icons.ic_arrow_down_24, + tintReference = { TangemTheme.colors3.icon.primary }, + ), + onClick = state.onAddFundsClick, + ) + TangemCircleActionButton( + modifier = Modifier.padding(horizontal = TangemTheme.dimens2.x4), + title = resourceReference(R.string.common_send), + icon = TangemIconUM.Icon( + imageVector = Icons.ic_arrow_up_24, + tintReference = { TangemTheme.colors3.icon.primary }, + ), + onClick = state.onSendClick, + ) + } +} + +@Composable +private fun TopBar(state: VirtualAccountMainUM, onHeightChange: (Dp) -> Unit, modifier: Modifier = Modifier) { + val density = LocalDensity.current + TangemTopBar( + modifier = modifier + .onSizeChanged { size -> onHeightChange(with(density) { size.height.toDp() }) } + .statusBarsPadding(), + title = state.title, + subtitle = state.subtitle, + startContent = { + TangemButton( + iconStart = TangemIconUM.Icon(iconRes = CoreUiR.drawable.ic_arrow_back_28), + onClick = state.onBackClick, + size = TangemButton.Size.X11, + variant = TangemButton.Variant.Material, + ) + }, + endContent = { + TangemButton( + iconStart = TangemIconUM.Icon(imageVector = Icons.ic_dots_vertical_24), + onClick = state.onMenuClick, + size = TangemButton.Size.X11, + variant = TangemButton.Variant.Material, + ) + }, + ) +} + +/** + * Computes the height left between the top of the item identified by [itemKey] and the bottom of the + * list's viewport (excluding bottom content padding). Returns `0.dp` until the item has been laid out. + * + * The item's own height does not affect its offset (only the items above it do), so reading the offset + * back to size the item is stable and does not loop. + */ +@Composable +private fun rememberRemainingViewportHeight(listState: LazyListState, itemKey: Any): Dp { + val density = LocalDensity.current + val remainingPx by remember(listState, itemKey) { + derivedStateOf { + val info = listState.layoutInfo + val item = info.visibleItemsInfo.firstOrNull { it.key == itemKey } + ?: return@derivedStateOf 0 + (info.viewportEndOffset - info.afterContentPadding - item.offset).coerceAtLeast(minimumValue = 0) + } + } + return with(density) { remainingPx.toDp() } +} + +@Preview(device = Devices.PIXEL_7_PRO) +@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES, device = Devices.PIXEL_7_PRO) +@Composable +private fun VirtualAccountMainScreenPreview() { + TangemThemePreviewRedesign { + VirtualAccountMainScreen( + state = VirtualAccountMainUM( + title = resourceReference(R.string.virtual_account_title), + subtitle = resourceReference(R.string.tangempay_usdc_on_polygon_network), + balance = VirtualAccountBalanceBlockState.Content( + fiatBalance = stringReference("$0.00"), + isBalanceFlickering = false, + ), + isBalanceHidden = false, + onBackClick = {}, + onMenuClick = {}, + onAddFundsClick = {}, + onSendClick = {}, + ), + ) + } +} \ No newline at end of file diff --git a/features/virtual-accounts/details/impl/src/main/kotlin/com/tangem/features/virtualaccount/main/VirtualAccountMainUM.kt b/features/virtual-accounts/details/impl/src/main/kotlin/com/tangem/features/virtualaccount/main/VirtualAccountMainUM.kt new file mode 100644 index 0000000000..555fdd5601 --- /dev/null +++ b/features/virtual-accounts/details/impl/src/main/kotlin/com/tangem/features/virtualaccount/main/VirtualAccountMainUM.kt @@ -0,0 +1,29 @@ +package com.tangem.features.virtualaccount.main + +import androidx.compose.runtime.Immutable +import com.tangem.core.ui.extensions.TextReference + +@Immutable +internal data class VirtualAccountMainUM( + val title: TextReference, + val subtitle: TextReference, + val balance: VirtualAccountBalanceBlockState, + val isBalanceHidden: Boolean, + val onBackClick: () -> Unit, + val onMenuClick: () -> Unit, + val onAddFundsClick: () -> Unit, + val onSendClick: () -> Unit, +) + +@Immutable +internal sealed class VirtualAccountBalanceBlockState { + + data object Loading : VirtualAccountBalanceBlockState() + + data class Content( + val fiatBalance: TextReference, + val isBalanceFlickering: Boolean, + ) : VirtualAccountBalanceBlockState() + + data object Error : VirtualAccountBalanceBlockState() +} \ No newline at end of file diff --git a/features/virtual-accounts/details/impl/src/main/kotlin/com/tangem/features/virtualaccount/main/di/VirtualAccountMainComponentModule.kt b/features/virtual-accounts/details/impl/src/main/kotlin/com/tangem/features/virtualaccount/main/di/VirtualAccountMainComponentModule.kt new file mode 100644 index 0000000000..3e3ca68186 --- /dev/null +++ b/features/virtual-accounts/details/impl/src/main/kotlin/com/tangem/features/virtualaccount/main/di/VirtualAccountMainComponentModule.kt @@ -0,0 +1,18 @@ +package com.tangem.features.virtualaccount.main.di + +import com.tangem.features.virtualaccount.main.DefaultVirtualAccountMainComponent +import com.tangem.features.virtualaccount.details.component.VirtualAccountMainComponent +import dagger.Binds +import dagger.Module +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent + +@Module +@InstallIn(SingletonComponent::class) +internal interface VirtualAccountMainComponentModule { + + @Binds + fun bindVirtualAccountMainComponentFactory( + factory: DefaultVirtualAccountMainComponent.Factory, + ): VirtualAccountMainComponent.Factory +} \ No newline at end of file diff --git a/features/virtual-accounts/details/impl/src/main/kotlin/com/tangem/features/virtualaccount/main/di/VirtualAccountMainModelModule.kt b/features/virtual-accounts/details/impl/src/main/kotlin/com/tangem/features/virtualaccount/main/di/VirtualAccountMainModelModule.kt new file mode 100644 index 0000000000..9c621bc6fc --- /dev/null +++ b/features/virtual-accounts/details/impl/src/main/kotlin/com/tangem/features/virtualaccount/main/di/VirtualAccountMainModelModule.kt @@ -0,0 +1,20 @@ +package com.tangem.features.virtualaccount.main.di + +import com.tangem.core.decompose.di.ModelComponent +import com.tangem.core.decompose.model.Model +import com.tangem.features.virtualaccount.main.VirtualAccountMainModel +import dagger.Binds +import dagger.Module +import dagger.hilt.InstallIn +import dagger.multibindings.ClassKey +import dagger.multibindings.IntoMap + +@Module +@InstallIn(ModelComponent::class) +internal interface VirtualAccountMainModelModule { + + @Binds + @IntoMap + @ClassKey(VirtualAccountMainModel::class) + fun bindVirtualAccountMainModel(model: VirtualAccountMainModel): Model +} \ No newline at end of file diff --git a/plugins/configuration/src/main/kotlin/com/tangem/plugin/configuration/configurations/extension/BaseExtensionConfigurations.kt b/plugins/configuration/src/main/kotlin/com/tangem/plugin/configuration/configurations/extension/BaseExtensionConfigurations.kt index e0c1ec5b25..44920f24a3 100644 --- a/plugins/configuration/src/main/kotlin/com/tangem/plugin/configuration/configurations/extension/BaseExtensionConfigurations.kt +++ b/plugins/configuration/src/main/kotlin/com/tangem/plugin/configuration/configurations/extension/BaseExtensionConfigurations.kt @@ -20,6 +20,7 @@ internal fun BaseExtension.configureCompilerOptions() { internal fun BaseExtension.configureCompose(project: Project) { val useCompose = with(project.path) { contains(":ui") || + contains(Regex(pattern = ":common-ui\$")) || // shared Composable UI component modules contains(":common:ui-charts") || contains(":features:onboarding") || // TODO: divide on api/impl after migrating all onboarding to module contains(Regex(pattern = ":presentation\$")) ||