Updated on 2026-08-14
This commit is contained in:
parent
2be3a915ea
commit
2348d3456f
10 changed files with 528 additions and 240 deletions
|
|
@ -20,6 +20,7 @@ import com.tangem.core.ui.res.TangemThemePreview
|
|||
fun TangemPullToRefreshContainer(
|
||||
config: PullToRefreshConfig,
|
||||
modifier: Modifier = Modifier,
|
||||
indicatorModifier: Modifier = Modifier,
|
||||
content: @Composable () -> Unit,
|
||||
) {
|
||||
val state = rememberPullToRefreshState()
|
||||
|
|
@ -32,7 +33,7 @@ fun TangemPullToRefreshContainer(
|
|||
modifier = modifier,
|
||||
indicator = {
|
||||
Indicator(
|
||||
modifier = Modifier.align(Alignment.TopCenter),
|
||||
modifier = indicatorModifier.align(Alignment.TopCenter),
|
||||
isRefreshing = config.isRefreshing,
|
||||
state = state,
|
||||
containerColor = TangemTheme.colors.background.tertiary,
|
||||
|
|
|
|||
|
|
@ -9,6 +9,12 @@ plugins {
|
|||
|
||||
android {
|
||||
namespace = "com.tangem.feature.wallet.impl"
|
||||
packaging {
|
||||
resources {
|
||||
// To build and run composable preview
|
||||
merges += "paymentrequest.proto"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
|
|
@ -44,6 +50,16 @@ dependencies {
|
|||
exclude(group = "com.google.firebase", module = "protolite-well-known-types")
|
||||
exclude(group = "com.google.protobuf", module = "protobuf-javalite")
|
||||
}
|
||||
implementation(deps.haze) {
|
||||
exclude(module = "activity-compose")
|
||||
exclude(module = "activity")
|
||||
exclude(module = "activity-ktx")
|
||||
}
|
||||
implementation(deps.haze.materials) {
|
||||
exclude(module = "activity-compose")
|
||||
exclude(module = "activity")
|
||||
exclude(module = "activity-ktx")
|
||||
}
|
||||
|
||||
/** DI */
|
||||
implementation(deps.hilt.android)
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import androidx.compose.ui.Modifier
|
|||
import androidx.compose.ui.unit.Dp
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import com.arkivanov.decompose.ExperimentalDecomposeApi
|
||||
import com.arkivanov.decompose.extensions.compose.subscribeAsState
|
||||
import com.arkivanov.decompose.router.slot.childSlot
|
||||
import com.arkivanov.decompose.router.slot.dismiss
|
||||
|
|
@ -14,6 +15,7 @@ import com.tangem.core.decompose.context.AppComponentContext
|
|||
import com.tangem.core.decompose.context.child
|
||||
import com.tangem.core.decompose.context.childByContext
|
||||
import com.tangem.core.decompose.model.getOrCreateModel
|
||||
import com.tangem.core.ui.DesignFeatureToggles
|
||||
import com.tangem.core.ui.components.bottomsheets.state.BottomSheetState
|
||||
import com.tangem.core.ui.decompose.ComposableBottomSheetComponent
|
||||
import com.tangem.core.ui.decompose.ComposableContentComponent
|
||||
|
|
@ -23,6 +25,7 @@ import com.tangem.feature.wallet.child.wallet.model.WalletModel
|
|||
import com.tangem.feature.wallet.navigation.WalletRoute
|
||||
import com.tangem.feature.wallet.presentation.wallet.state.model.WalletDialogConfig
|
||||
import com.tangem.feature.wallet.presentation.wallet.ui.WalletScreen
|
||||
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
|
||||
|
|
@ -38,6 +41,7 @@ import dagger.assisted.AssistedFactory
|
|||
import dagger.assisted.AssistedInject
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
@OptIn(ExperimentalDecomposeApi::class)
|
||||
@Suppress("LongParameterList")
|
||||
internal class WalletComponent @AssistedInject constructor(
|
||||
@Assisted appComponentContext: AppComponentContext,
|
||||
|
|
@ -50,6 +54,7 @@ internal class WalletComponent @AssistedInject constructor(
|
|||
private val tokenReceiveComponentFactory: TokenReceiveComponent.Factory,
|
||||
private val yieldSupplyDepositedWarningComponent: YieldSupplyDepositedWarningComponent.Factory,
|
||||
private val feedFeatureToggle: FeedFeatureToggle,
|
||||
private val designFeatureToggles: DesignFeatureToggles,
|
||||
) : ComposableContentComponent, AppComponentContext by appComponentContext {
|
||||
|
||||
private val model: WalletModel = getOrCreateModel()
|
||||
|
|
@ -148,18 +153,33 @@ internal class WalletComponent @AssistedInject constructor(
|
|||
var headerSize by remember { mutableStateOf(0.dp) }
|
||||
val dialog by dialog.subscribeAsState()
|
||||
|
||||
WalletScreen(
|
||||
state = model.uiState.collectAsStateWithLifecycle().value,
|
||||
bottomSheetContent = {
|
||||
BottomSheetContent(
|
||||
bottomSheetState = bottomSheetState,
|
||||
onHeaderSizeChange = { headerSize = it },
|
||||
modifier = modifier,
|
||||
)
|
||||
},
|
||||
bottomSheetHeaderHeightProvider = { headerSize },
|
||||
onBottomSheetStateChange = { bottomSheetState.value = it },
|
||||
)
|
||||
if (designFeatureToggles.isRedesignEnabled) {
|
||||
WalletScreen2(
|
||||
state = model.uiState.collectAsStateWithLifecycle().value,
|
||||
bottomSheetContent = {
|
||||
BottomSheetContent(
|
||||
bottomSheetState = bottomSheetState,
|
||||
onHeaderSizeChange = { headerSize = it },
|
||||
modifier = modifier,
|
||||
)
|
||||
},
|
||||
bottomSheetHeaderHeightProvider = { headerSize },
|
||||
onBottomSheetStateChange = { bottomSheetState.value = it },
|
||||
)
|
||||
} else {
|
||||
WalletScreen(
|
||||
state = model.uiState.collectAsStateWithLifecycle().value,
|
||||
bottomSheetContent = {
|
||||
BottomSheetContent(
|
||||
bottomSheetState = bottomSheetState,
|
||||
onHeaderSizeChange = { headerSize = it },
|
||||
modifier = modifier,
|
||||
)
|
||||
},
|
||||
bottomSheetHeaderHeightProvider = { headerSize },
|
||||
onBottomSheetStateChange = { bottomSheetState.value = it },
|
||||
)
|
||||
}
|
||||
|
||||
when (val dialog = dialog.child?.instance) {
|
||||
is ComposableDialogComponent -> dialog.Dialog()
|
||||
|
|
|
|||
|
|
@ -1,6 +1,5 @@
|
|||
package com.tangem.feature.wallet.presentation.common
|
||||
|
||||
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig
|
||||
import com.tangem.core.ui.components.currency.icon.CurrencyIconState
|
||||
import com.tangem.core.ui.components.token.state.TokenItemState
|
||||
import com.tangem.core.ui.event.consumedEvent
|
||||
|
|
@ -186,17 +185,6 @@ internal object WalletPreviewData {
|
|||
)
|
||||
}
|
||||
|
||||
val bottomSheet by lazy {
|
||||
TangemBottomSheetConfig(
|
||||
isShown = false,
|
||||
onDismissRequest = {},
|
||||
content = WalletBottomSheetConfig.UnlockWallets(
|
||||
onUnlockClick = {},
|
||||
onScanClick = {},
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
val actionsBottomSheet = ActionsBottomSheetConfig(
|
||||
actions = listOf(
|
||||
TokenActionButtonConfig(
|
||||
|
|
|
|||
|
|
@ -0,0 +1,38 @@
|
|||
package com.tangem.feature.wallet.presentation.preview
|
||||
|
||||
import com.tangem.core.ui.extensions.combinedReference
|
||||
import com.tangem.core.ui.extensions.stringReference
|
||||
import com.tangem.core.ui.extensions.styledStringReference
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.domain.models.wallet.UserWalletId
|
||||
import com.tangem.feature.wallet.presentation.wallet.state.model.WalletBalanceUM
|
||||
|
||||
internal object WalletBalancePreview {
|
||||
|
||||
val content: WalletBalanceUM.Content = WalletBalanceUM.Content(
|
||||
id = UserWalletId("0"),
|
||||
name = "My Wallet",
|
||||
balance = combinedReference(
|
||||
stringReference("1,234"),
|
||||
styledStringReference(
|
||||
".56",
|
||||
{
|
||||
TangemTheme.typography2.headingRegular28.toSpanStyle()
|
||||
},
|
||||
),
|
||||
stringReference(" $"),
|
||||
),
|
||||
isBalanceFlickering = false,
|
||||
isZeroBalance = false,
|
||||
)
|
||||
|
||||
val loading: WalletBalanceUM.Loading = WalletBalanceUM.Loading(
|
||||
id = UserWalletId("1"),
|
||||
name = "My Wallet",
|
||||
)
|
||||
|
||||
val error: WalletBalanceUM.Error = WalletBalanceUM.Error(
|
||||
id = UserWalletId("2"),
|
||||
name = "My Wallet",
|
||||
)
|
||||
}
|
||||
|
|
@ -1,51 +0,0 @@
|
|||
package com.tangem.feature.wallet.presentation.wallet.state.model
|
||||
|
||||
import androidx.annotation.DrawableRes
|
||||
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfigContent
|
||||
import com.tangem.core.ui.extensions.TextReference
|
||||
import com.tangem.core.ui.extensions.resourceReference
|
||||
import com.tangem.core.ui.extensions.wrappedList
|
||||
import com.tangem.feature.wallet.impl.R
|
||||
|
||||
/**
|
||||
* Wallet bottom sheet config
|
||||
*
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
sealed class WalletBottomSheetConfig(
|
||||
open val title: TextReference,
|
||||
open val subtitle: TextReference,
|
||||
@DrawableRes open val iconResId: Int,
|
||||
val primaryButtonConfig: ButtonConfig,
|
||||
val secondaryButtonConfig: ButtonConfig,
|
||||
) : TangemBottomSheetConfigContent {
|
||||
|
||||
data class ButtonConfig(
|
||||
val text: TextReference,
|
||||
val onClick: () -> Unit,
|
||||
@DrawableRes val iconResId: Int? = null,
|
||||
)
|
||||
|
||||
data class UnlockWallets(val onUnlockClick: () -> Unit, val onScanClick: () -> Unit) : WalletBottomSheetConfig(
|
||||
title = resourceReference(id = R.string.common_access_denied),
|
||||
subtitle = resourceReference(
|
||||
id = R.string.unlock_wallet_description_full,
|
||||
formatArgs = wrappedList(
|
||||
resourceReference(R.string.common_biometrics),
|
||||
),
|
||||
),
|
||||
iconResId = R.drawable.ic_locked_24,
|
||||
primaryButtonConfig = ButtonConfig(
|
||||
text = resourceReference(
|
||||
id = R.string.user_wallet_list_unlock_all_with,
|
||||
formatArgs = wrappedList(resourceReference(R.string.common_biometrics)),
|
||||
),
|
||||
onClick = onUnlockClick,
|
||||
),
|
||||
secondaryButtonConfig = ButtonConfig(
|
||||
text = resourceReference(id = R.string.welcome_unlock_card),
|
||||
onClick = onScanClick,
|
||||
iconResId = R.drawable.ic_tangem_24,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
|
@ -1,22 +0,0 @@
|
|||
package com.tangem.feature.wallet.presentation.wallet.state.transformers
|
||||
|
||||
import com.tangem.domain.models.wallet.UserWalletId
|
||||
import com.tangem.feature.wallet.presentation.wallet.state.model.WalletState
|
||||
import kotlin.reflect.KClass
|
||||
|
||||
internal abstract class TypedWalletStateTransformer<S : WalletState>(
|
||||
userWalletId: UserWalletId,
|
||||
protected val targetStateClass: KClass<S>,
|
||||
) : WalletStateTransformer(userWalletId) {
|
||||
|
||||
abstract fun transformTyped(prevState: S): WalletState
|
||||
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
final override fun transform(prevState: WalletState): WalletState {
|
||||
return if (prevState::class == targetStateClass) {
|
||||
transformTyped(prevState as S)
|
||||
} else {
|
||||
prevState
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -720,7 +720,6 @@ internal fun LazyListScope.nftCollections(state: WalletState, itemModifier: Modi
|
|||
private fun ShowBottomSheet(bottomSheetConfig: TangemBottomSheetConfig?) {
|
||||
if (bottomSheetConfig != null) {
|
||||
when (bottomSheetConfig.content) {
|
||||
is WalletBottomSheetConfig -> WalletBottomSheet(config = bottomSheetConfig)
|
||||
is ActionsBottomSheetConfig -> TokenActionsBottomSheet(config = bottomSheetConfig)
|
||||
is ChooseAddressBottomSheetConfig -> ChooseAddressBottomSheet(config = bottomSheetConfig)
|
||||
is ExpressStatusBottomSheetConfig -> ExpressStatusBottomSheet(config = bottomSheetConfig)
|
||||
|
|
|
|||
|
|
@ -0,0 +1,440 @@
|
|||
package com.tangem.feature.wallet.presentation.wallet.ui
|
||||
|
||||
import android.content.res.Configuration
|
||||
import androidx.activity.compose.BackHandler
|
||||
import androidx.compose.animation.AnimatedVisibility
|
||||
import androidx.compose.animation.core.animateFloatAsState
|
||||
import androidx.compose.animation.core.tween
|
||||
import androidx.compose.foundation.Canvas
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.gestures.detectTapGestures
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.lazy.LazyListState
|
||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||
import androidx.compose.foundation.pager.rememberPagerState
|
||||
import androidx.compose.material3.*
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.drawBehind
|
||||
import androidx.compose.ui.focus.onFocusChanged
|
||||
import androidx.compose.ui.graphics.Brush
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.input.pointer.pointerInput
|
||||
import androidx.compose.ui.platform.LocalDensity
|
||||
import androidx.compose.ui.platform.LocalSoftwareKeyboardController
|
||||
import androidx.compose.ui.platform.testTag
|
||||
import androidx.compose.ui.semantics.clearAndSetSemantics
|
||||
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.Dp
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import com.arkivanov.decompose.ExperimentalDecomposeApi
|
||||
import com.tangem.core.ui.components.atoms.Hand
|
||||
import com.tangem.core.ui.components.atoms.handComposableComponentHeight
|
||||
import com.tangem.core.ui.components.bottomsheets.state.BottomSheetState
|
||||
import com.tangem.core.ui.components.haze.hazeEffectTangem
|
||||
import com.tangem.core.ui.components.haze.hazeSourceTangem
|
||||
import com.tangem.core.ui.components.rememberIsKeyboardVisible
|
||||
import com.tangem.core.ui.components.sheetscaffold.*
|
||||
import com.tangem.core.ui.components.snackbar.CopiedTextSnackbar
|
||||
import com.tangem.core.ui.components.snackbar.TangemSnackbar
|
||||
import com.tangem.core.ui.ds.topbar.TangemTopBar
|
||||
import com.tangem.core.ui.event.StateEvent
|
||||
import com.tangem.core.ui.extensions.stringReference
|
||||
import com.tangem.core.ui.res.*
|
||||
import com.tangem.core.ui.test.MainScreenTestTags
|
||||
import com.tangem.feature.wallet.impl.R
|
||||
import com.tangem.feature.wallet.presentation.common.preview.WalletScreenPreviewData.accountScreenState
|
||||
import com.tangem.feature.wallet.presentation.common.preview.WalletScreenPreviewData.accountScreenWithEmptyTokensState
|
||||
import com.tangem.feature.wallet.presentation.common.preview.WalletScreenPreviewData.walletScreenState
|
||||
import com.tangem.feature.wallet.presentation.wallet.state.model.NOT_INITIALIZED_WALLET_INDEX
|
||||
import com.tangem.feature.wallet.presentation.wallet.state.model.WalletEvent
|
||||
import com.tangem.feature.wallet.presentation.wallet.state.model.WalletScreenState
|
||||
import dev.chrisbanes.haze.HazeProgressive
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
@OptIn(ExperimentalDecomposeApi::class)
|
||||
@Composable
|
||||
internal fun WalletScreen2(
|
||||
state: WalletScreenState,
|
||||
bottomSheetContent: @Composable (() -> Unit),
|
||||
bottomSheetHeaderHeightProvider: () -> Dp,
|
||||
onBottomSheetStateChange: (BottomSheetState) -> Unit,
|
||||
) {
|
||||
// It means that screen is still initializing
|
||||
if (state.selectedWalletIndex == NOT_INITIALIZED_WALLET_INDEX) return
|
||||
|
||||
val walletsListState = rememberLazyListState(initialFirstVisibleItemIndex = state.selectedWalletIndex)
|
||||
val snackbarHostState = remember(::SnackbarHostState)
|
||||
val isAutoScroll = remember { mutableStateOf(value = false) }
|
||||
|
||||
WalletContent2(
|
||||
state = state,
|
||||
walletsListState = walletsListState,
|
||||
snackbarHostState = snackbarHostState,
|
||||
isAutoScroll = isAutoScroll,
|
||||
onAutoScrollReset = { isAutoScroll.value = false },
|
||||
bottomSheetContent = bottomSheetContent,
|
||||
bottomSheetHeaderHeightProvider = bottomSheetHeaderHeightProvider,
|
||||
onBottomSheetStateChange = onBottomSheetStateChange,
|
||||
)
|
||||
|
||||
WalletEventEffect(
|
||||
walletsListState = walletsListState,
|
||||
snackbarHostState = snackbarHostState,
|
||||
event = state.event,
|
||||
onAutoScrollSet = { isAutoScroll.value = true },
|
||||
)
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class, ExperimentalDecomposeApi::class)
|
||||
@Suppress("LongMethod", "LongParameterList", "UnusedPrivateMember")
|
||||
@Composable
|
||||
private fun WalletContent2(
|
||||
state: WalletScreenState,
|
||||
walletsListState: LazyListState,
|
||||
snackbarHostState: SnackbarHostState,
|
||||
isAutoScroll: State<Boolean>,
|
||||
onAutoScrollReset: () -> Unit,
|
||||
bottomSheetHeaderHeightProvider: () -> Dp,
|
||||
onBottomSheetStateChange: (BottomSheetState) -> Unit,
|
||||
bottomSheetContent: @Composable (() -> Unit),
|
||||
) {
|
||||
/*
|
||||
* Don't pass key to remember, because it will brake scroll animation.
|
||||
* selectedWalletIndex will be changed in WalletsListEffects.
|
||||
*/
|
||||
// val selectedWalletIndex by remember(state.selectedWalletIndex) { mutableIntStateOf(state.selectedWalletIndex) }
|
||||
// val selectedWallet = state.wallets2.getOrElse(selectedWalletIndex) { state.wallets2[state.selectedWalletIndex] }
|
||||
|
||||
val statusBarHeight = with(LocalDensity.current) { WindowInsets.statusBars.getBottom(this).toDp() }
|
||||
|
||||
val listState = rememberLazyListState()
|
||||
|
||||
val partialCollapsedHeight = 64.dp + statusBarHeight
|
||||
|
||||
val scaffoldContent: @Composable (PaddingValues?) -> Unit = { _ ->
|
||||
|
||||
val pagerState = rememberPagerState(
|
||||
initialPage = state.selectedWalletIndex,
|
||||
pageCount = { state.wallets2.size },
|
||||
)
|
||||
|
||||
LaunchedEffect(pagerState.currentPage) {
|
||||
if (pagerState.currentPage != state.selectedWalletIndex) {
|
||||
state.onWalletChange(pagerState.currentPage, false)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
BaseScaffoldWithMarkets(
|
||||
state = state,
|
||||
listState = listState,
|
||||
snackbarHostState = snackbarHostState,
|
||||
bottomSheetHeaderHeightProvider = bottomSheetHeaderHeightProvider,
|
||||
onBottomSheetStateChange = onBottomSheetStateChange,
|
||||
bottomSheetContent = bottomSheetContent,
|
||||
content = scaffoldContent,
|
||||
)
|
||||
}
|
||||
|
||||
@Suppress("LongParameterList", "LongMethod", "CyclomaticComplexMethod", "UnusedPrivateMember")
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
private inline fun BaseScaffoldWithMarkets(
|
||||
state: WalletScreenState,
|
||||
snackbarHostState: SnackbarHostState,
|
||||
listState: LazyListState,
|
||||
bottomSheetHeaderHeightProvider: () -> Dp,
|
||||
modifier: Modifier = Modifier,
|
||||
noinline onBottomSheetStateChange: (BottomSheetState) -> Unit,
|
||||
crossinline bottomSheetContent: @Composable () -> Unit,
|
||||
crossinline content: @Composable (PaddingValues) -> Unit,
|
||||
) {
|
||||
val bottomSheetState = rememberTangemStandardBottomSheetState()
|
||||
val isPowerSaving by LocalPowerSavingState.current.isPowerSavingModeEnabled.collectAsStateWithLifecycle()
|
||||
|
||||
val isKeyboardVisible by rememberIsKeyboardVisible()
|
||||
|
||||
val scaffoldState = rememberTangemBottomSheetScaffoldState(
|
||||
bottomSheetState = bottomSheetState,
|
||||
snackbarHostState = snackbarHostState,
|
||||
)
|
||||
|
||||
val density = LocalDensity.current
|
||||
val bottomBarHeight = with(density) { WindowInsets.systemBars.getBottom(density = this).toDp() }
|
||||
val statusBarHeight = with(density) { WindowInsets.statusBars.getTop(density = this).toDp() }
|
||||
val peekHeight = bottomSheetHeaderHeightProvider() + handComposableComponentHeight + bottomBarHeight
|
||||
val maxHeight = LocalWindowSize.current.height
|
||||
|
||||
val coroutineScope = rememberCoroutineScope()
|
||||
val background = if (state.isNewMarketEnabled) {
|
||||
TangemTheme.colors.background.tertiary
|
||||
} else {
|
||||
TangemTheme.colors.background.primary
|
||||
}
|
||||
|
||||
CompositionLocalProvider(
|
||||
LocalMainBottomSheetColor provides remember(background) { mutableStateOf(background) },
|
||||
) {
|
||||
val backgroundColor = LocalMainBottomSheetColor.current
|
||||
var isSearchFieldFocused by remember { mutableStateOf(false) }
|
||||
val isNavBarVisible = remember { mutableStateOf(true) }
|
||||
|
||||
BottomSheetStateEffects(
|
||||
bottomSheetState = bottomSheetState,
|
||||
onBottomSheetStateChange = onBottomSheetStateChange,
|
||||
navigationBarVisible = isNavBarVisible,
|
||||
isSearchFieldFocused = isSearchFieldFocused,
|
||||
)
|
||||
|
||||
Box(modifier = modifier) {
|
||||
TangemBottomSheetScaffold(
|
||||
modifier = Modifier.background(
|
||||
brush = Brush.verticalGradient(
|
||||
listOf(
|
||||
TangemTheme.colors2.surface.level1,
|
||||
TangemTheme.colors2.surface.level2,
|
||||
),
|
||||
),
|
||||
),
|
||||
snackbarHost = { snackbarHostState ->
|
||||
WalletSnackbarHost(
|
||||
snackbarHostState = snackbarHostState,
|
||||
event = state.event,
|
||||
modifier = Modifier
|
||||
.padding(bottom = TangemTheme.dimens.spacing4)
|
||||
.navigationBarsPadding(),
|
||||
)
|
||||
},
|
||||
containerColor = Color.Unspecified,
|
||||
sheetContainerColor = backgroundColor.value,
|
||||
scaffoldState = scaffoldState,
|
||||
sheetPeekHeight = peekHeight,
|
||||
sheetShape = TangemTheme.shapes.bottomSheetLarge,
|
||||
sheetContent = {
|
||||
// hide bottom sheet when back pressed
|
||||
BackHandler(
|
||||
isKeyboardVisible.not() &&
|
||||
bottomSheetState.currentValue == TangemSheetValue.Expanded,
|
||||
) {
|
||||
coroutineScope.launch { bottomSheetState.partialExpand() }
|
||||
}
|
||||
|
||||
Column(
|
||||
modifier = Modifier
|
||||
// expand bottom sheet when clicked on the header
|
||||
.clickable(
|
||||
enabled = bottomSheetState.currentValue == TangemSheetValue.PartiallyExpanded,
|
||||
indication = null,
|
||||
interactionSource = null,
|
||||
) {
|
||||
coroutineScope.launch { bottomSheetState.expand() }
|
||||
}
|
||||
.sizeIn(maxHeight = maxHeight - statusBarHeight),
|
||||
) {
|
||||
Hand(Modifier.drawBehind { drawRect(backgroundColor.value) })
|
||||
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.onFocusChanged {
|
||||
isSearchFieldFocused = it.isFocused
|
||||
},
|
||||
) {
|
||||
bottomSheetContent()
|
||||
}
|
||||
}
|
||||
},
|
||||
content = { paddingValues ->
|
||||
Box {
|
||||
Column(
|
||||
modifier = Modifier.hazeSourceTangem(-1f),
|
||||
) {
|
||||
content(paddingValues)
|
||||
}
|
||||
|
||||
Surface(
|
||||
color = Color.Unspecified,
|
||||
contentColor = Color.Unspecified,
|
||||
modifier = Modifier
|
||||
.hazeEffectTangem {
|
||||
progressive =
|
||||
HazeProgressive.verticalGradient(startIntensity = 1f, endIntensity = 0f)
|
||||
},
|
||||
) {
|
||||
TangemTopBar(
|
||||
title = stringReference(""), // todo balance
|
||||
startIconRes = R.drawable.ic_tangem_24,
|
||||
endIconRes = R.drawable.ic_more_default_24,
|
||||
onEndContentClick = state.topBarConfig.onDetailsClick,
|
||||
isGhostButtons = !isPowerSaving,
|
||||
modifier = Modifier
|
||||
.testTag(MainScreenTestTags.TOP_BAR),
|
||||
)
|
||||
}
|
||||
|
||||
BottomSheetScrim(
|
||||
color = if (state.showMarketsOnboarding) {
|
||||
Color.Black.copy(alpha = .65f)
|
||||
} else {
|
||||
Color.Black.copy(alpha = .40f)
|
||||
},
|
||||
visible = bottomSheetState.targetValue == TangemSheetValue.Expanded ||
|
||||
state.showMarketsOnboarding,
|
||||
onDismissRequest = {
|
||||
coroutineScope.launch { bottomSheetState.partialExpand() }
|
||||
state.onDismissMarketsTooltip()
|
||||
},
|
||||
)
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
AnimatedVisibility(
|
||||
modifier = Modifier.align(Alignment.BottomCenter),
|
||||
visible = isNavBarVisible.value,
|
||||
) {
|
||||
Box(
|
||||
Modifier
|
||||
.align(Alignment.BottomCenter)
|
||||
.background(backgroundColor.value)
|
||||
.height(bottomBarHeight)
|
||||
.fillMaxWidth(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
LaunchedEffect(state.showMarketsOnboarding, bottomSheetState.targetValue) {
|
||||
if (state.showMarketsOnboarding && bottomSheetState.targetValue == TangemSheetValue.Expanded) {
|
||||
state.onDismissMarketsTooltip()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun BottomSheetScrim(color: Color, visible: Boolean, onDismissRequest: () -> Unit) {
|
||||
val alpha by animateFloatAsState(
|
||||
targetValue = if (visible) 1f else 0f,
|
||||
animationSpec = tween(),
|
||||
label = "scrim",
|
||||
)
|
||||
val dismissSheet = if (visible) {
|
||||
Modifier
|
||||
.pointerInput(onDismissRequest) {
|
||||
detectTapGestures {
|
||||
onDismissRequest()
|
||||
}
|
||||
}
|
||||
.clearAndSetSemantics {}
|
||||
} else {
|
||||
Modifier
|
||||
}
|
||||
Canvas(
|
||||
Modifier
|
||||
.fillMaxSize()
|
||||
.then(dismissSheet),
|
||||
) {
|
||||
drawRect(color = color, alpha = alpha)
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("CyclomaticComplexMethod", "MagicNumber", "LongMethod")
|
||||
@Composable
|
||||
private fun BottomSheetStateEffects(
|
||||
bottomSheetState: TangemSheetState,
|
||||
navigationBarVisible: MutableState<Boolean>,
|
||||
onBottomSheetStateChange: (BottomSheetState) -> Unit,
|
||||
isSearchFieldFocused: Boolean,
|
||||
) {
|
||||
LaunchedEffect(bottomSheetState.targetValue) {
|
||||
when (bottomSheetState.targetValue) {
|
||||
TangemSheetValue.Hidden,
|
||||
TangemSheetValue.Expanded,
|
||||
-> navigationBarVisible.value = false
|
||||
TangemSheetValue.PartiallyExpanded,
|
||||
-> navigationBarVisible.value = true
|
||||
}
|
||||
}
|
||||
|
||||
// expand bottom sheet when keyboard appears
|
||||
val isKeyboardVisible by rememberIsKeyboardVisible()
|
||||
|
||||
LaunchedEffect(isKeyboardVisible) {
|
||||
if (isKeyboardVisible && isSearchFieldFocused) {
|
||||
bottomSheetState.expand()
|
||||
}
|
||||
}
|
||||
|
||||
val keyboardController = LocalSoftwareKeyboardController.current
|
||||
// hide keyboard when bottom sheet is about to be hidden
|
||||
LaunchedEffect(Unit) {
|
||||
snapshotFlow {
|
||||
bottomSheetState.currentValue == TangemSheetValue.Expanded &&
|
||||
bottomSheetState.targetValue == TangemSheetValue.PartiallyExpanded
|
||||
}.collect { sheetHasBeenHidden ->
|
||||
if (sheetHasBeenHidden) {
|
||||
keyboardController?.hide()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val isSheetHidden = bottomSheetState.targetValue == TangemSheetValue.PartiallyExpanded
|
||||
LaunchedEffect(isSheetHidden) {
|
||||
onBottomSheetStateChange(
|
||||
if (isSheetHidden) {
|
||||
BottomSheetState.COLLAPSED
|
||||
} else {
|
||||
BottomSheetState.EXPANDED
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun WalletSnackbarHost(
|
||||
snackbarHostState: SnackbarHostState,
|
||||
event: StateEvent<WalletEvent>,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
SnackbarHost(hostState = snackbarHostState, modifier = modifier) { data ->
|
||||
if (event is StateEvent.Triggered && event.data is WalletEvent.CopyAddress) {
|
||||
CopiedTextSnackbar(data)
|
||||
} else {
|
||||
TangemSnackbar(data)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// region Preview
|
||||
@OptIn(ExperimentalDecomposeApi::class)
|
||||
@Preview(showBackground = true, widthDp = 360)
|
||||
@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES)
|
||||
@Composable
|
||||
private fun WalletScreen2_Preview(@PreviewParameter(WalletScreen2PreviewProvider::class) data: WalletScreenState) {
|
||||
TangemThemePreviewRedesign {
|
||||
WalletScreen2(
|
||||
state = data,
|
||||
bottomSheetContent = {
|
||||
Text("Markets Content")
|
||||
},
|
||||
bottomSheetHeaderHeightProvider = { 10.dp },
|
||||
onBottomSheetStateChange = {},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private class WalletScreen2PreviewProvider : PreviewParameterProvider<WalletScreenState> {
|
||||
override val values: Sequence<WalletScreenState>
|
||||
get() = sequenceOf(
|
||||
walletScreenState,
|
||||
walletScreenState.copy(selectedWalletIndex = 1),
|
||||
accountScreenState.copy(selectedWalletIndex = 1),
|
||||
accountScreenWithEmptyTokensState.copy(selectedWalletIndex = 1),
|
||||
)
|
||||
}
|
||||
// endregion
|
||||
|
|
@ -1,141 +0,0 @@
|
|||
package com.tangem.feature.wallet.presentation.wallet.ui.components.common
|
||||
|
||||
import android.content.res.Configuration
|
||||
import androidx.compose.foundation.layout.*
|
||||
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.res.painterResource
|
||||
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 com.tangem.core.ui.components.PrimaryButton
|
||||
import com.tangem.core.ui.components.PrimaryButtonIconStart
|
||||
import com.tangem.core.ui.components.SecondaryButton
|
||||
import com.tangem.core.ui.components.SecondaryButtonIconStart
|
||||
import com.tangem.core.ui.components.bottomsheets.sheet.TangemBottomSheet
|
||||
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig
|
||||
import com.tangem.core.ui.extensions.resolveReference
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.core.ui.res.TangemThemePreview
|
||||
import com.tangem.feature.wallet.presentation.common.WalletPreviewData
|
||||
import com.tangem.feature.wallet.presentation.wallet.state.model.WalletBottomSheetConfig
|
||||
|
||||
/**
|
||||
* Wallet bottom sheet with detail notification information
|
||||
*
|
||||
* @param config component config
|
||||
*
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
@Composable
|
||||
internal fun WalletBottomSheet(config: TangemBottomSheetConfig) {
|
||||
TangemBottomSheet(config) { content: WalletBottomSheetConfig ->
|
||||
BottomSheetContent(config = content)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun BottomSheetContent(config: WalletBottomSheetConfig) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.padding(horizontal = TangemTheme.dimens.spacing16)
|
||||
.padding(top = TangemTheme.dimens.spacing40, bottom = TangemTheme.dimens.spacing16),
|
||||
verticalArrangement = Arrangement.spacedBy(space = TangemTheme.dimens.spacing40),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
) {
|
||||
Icon(
|
||||
painter = painterResource(id = config.iconResId),
|
||||
contentDescription = null,
|
||||
modifier = Modifier.size(size = TangemTheme.dimens.size48),
|
||||
tint = when (config) {
|
||||
is WalletBottomSheetConfig.UnlockWallets -> TangemTheme.colors.icon.primary1
|
||||
},
|
||||
)
|
||||
|
||||
Column(
|
||||
verticalArrangement = Arrangement.spacedBy(space = TangemTheme.dimens.spacing16),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
) {
|
||||
Text(
|
||||
text = config.title.resolveReference(),
|
||||
color = TangemTheme.colors.text.primary1,
|
||||
textAlign = TextAlign.Center,
|
||||
style = TangemTheme.typography.h2,
|
||||
)
|
||||
|
||||
Text(
|
||||
text = config.subtitle.resolveReference(),
|
||||
color = TangemTheme.colors.text.secondary,
|
||||
textAlign = TextAlign.Center,
|
||||
style = TangemTheme.typography.body2,
|
||||
)
|
||||
}
|
||||
|
||||
Column(verticalArrangement = Arrangement.spacedBy(space = TangemTheme.dimens.spacing10)) {
|
||||
val buttonModifier = Modifier.fillMaxWidth()
|
||||
|
||||
PrimaryButton(config = config.primaryButtonConfig, modifier = buttonModifier)
|
||||
|
||||
SecondaryButton(config = config.secondaryButtonConfig, modifier = buttonModifier)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun PrimaryButton(config: WalletBottomSheetConfig.ButtonConfig, modifier: Modifier = Modifier) {
|
||||
if (config.iconResId == null) {
|
||||
PrimaryButton(
|
||||
text = config.text.resolveReference(),
|
||||
onClick = config.onClick,
|
||||
modifier = modifier,
|
||||
)
|
||||
} else {
|
||||
PrimaryButtonIconStart(
|
||||
text = config.text.resolveReference(),
|
||||
iconResId = config.iconResId,
|
||||
onClick = config.onClick,
|
||||
modifier = modifier,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun SecondaryButton(config: WalletBottomSheetConfig.ButtonConfig, modifier: Modifier = Modifier) {
|
||||
if (config.iconResId == null) {
|
||||
SecondaryButton(
|
||||
text = config.text.resolveReference(),
|
||||
onClick = config.onClick,
|
||||
modifier = modifier,
|
||||
)
|
||||
} else {
|
||||
SecondaryButtonIconStart(
|
||||
text = config.text.resolveReference(),
|
||||
iconResId = config.iconResId,
|
||||
onClick = config.onClick,
|
||||
modifier = modifier,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// region Preview
|
||||
@Preview(showBackground = true, widthDp = 360)
|
||||
@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES)
|
||||
@Composable
|
||||
private fun WalletBottomSheetContent_Preview(
|
||||
@PreviewParameter(WalletBottomSheetConfigProvider::class)
|
||||
config: WalletBottomSheetConfig,
|
||||
) {
|
||||
TangemThemePreview {
|
||||
// Use preview of content because ModalBottomSheet isn't supported in Preview mode
|
||||
BottomSheetContent(config = config)
|
||||
}
|
||||
}
|
||||
|
||||
private class WalletBottomSheetConfigProvider : CollectionPreviewParameterProvider<WalletBottomSheetConfig>(
|
||||
collection = listOf(WalletPreviewData.bottomSheet.content as WalletBottomSheetConfig),
|
||||
)
|
||||
// endregion
|
||||
Loading…
Add table
Add a link
Reference in a new issue