diff --git a/core/res/src/main/res/values/strings.xml b/core/res/src/main/res/values/strings.xml
index b89491b528..2a762eeab8 100644
--- a/core/res/src/main/res/values/strings.xml
+++ b/core/res/src/main/res/values/strings.xml
@@ -414,6 +414,8 @@
The maximum number of coins or tokens that can ever exist for a particular cryptocurrency
Trading volume (24h)
The total amount of a cryptocurrency that has been traded within the last 24 hours, indicating the level of activity and liquidity in the market
+ Pull this up or tap the search bar to add tokens directly from the market
+ Add tokens
You have to set up a single access code to protect all your cards
Protect
You can set up an individual access code on each card later
diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/WalletScreen.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/WalletScreen.kt
index d384ce9f67..1991328d2c 100644
--- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/WalletScreen.kt
+++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/WalletScreen.kt
@@ -3,10 +3,10 @@ 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.TweenSpec
-import androidx.compose.animation.core.animateFloatAsState
+import androidx.compose.animation.core.*
import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut
+import androidx.compose.animation.slideIn
import androidx.compose.foundation.Canvas
import androidx.compose.foundation.background
import androidx.compose.foundation.gestures.detectTapGestures
@@ -22,7 +22,10 @@ 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.geometry.*
import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.Path
import androidx.compose.ui.graphics.luminance
import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.platform.LocalDensity
@@ -34,6 +37,8 @@ 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.DpSize
+import androidx.compose.ui.unit.IntOffset
import androidx.compose.ui.unit.dp
import androidx.paging.compose.collectAsLazyPagingItems
import com.google.accompanist.systemuicontroller.rememberSystemUiController
@@ -58,6 +63,9 @@ import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreview
import com.tangem.core.ui.test.TestTags
import com.tangem.core.ui.utils.WindowInsetsZero
+import com.tangem.core.ui.utils.lineTo
+import com.tangem.core.ui.utils.moveTo
+import com.tangem.core.ui.utils.toPx
import com.tangem.feature.wallet.impl.R
import com.tangem.feature.wallet.presentation.common.preview.WalletScreenPreviewData.walletScreenState
import com.tangem.feature.wallet.presentation.wallet.state.model.*
@@ -75,10 +83,11 @@ import com.tangem.feature.wallet.presentation.wallet.ui.components.visa.balances
import com.tangem.feature.wallet.presentation.wallet.ui.components.visa.depositButton
import com.tangem.feature.wallet.presentation.wallet.ui.utils.changeWalletAnimator
import com.tangem.features.markets.entry.BottomSheetState
-import com.tangem.features.markets.entry.BottomSheetState.*
import com.tangem.features.markets.entry.MarketsEntryComponent
import kotlinx.collections.immutable.toImmutableList
+import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
+import kotlin.math.roundToInt
@Composable
internal fun WalletScreen(state: WalletScreenState, marketsEntryComponent: MarketsEntryComponent?) {
@@ -223,7 +232,7 @@ private fun WalletContent(
}
if (marketsEntryComponent != null) {
- val bottomSheetState = remember { mutableStateOf(COLLAPSED) }
+ val bottomSheetState = remember { mutableStateOf(BottomSheetState.COLLAPSED) }
var headerSize by remember { mutableStateOf(0.dp) }
@@ -357,8 +366,9 @@ private inline fun BaseScaffoldWithMarkets(
snackbarHostState = snackbarHostState,
)
- val bottomBarHeight = with(LocalDensity.current) { WindowInsets.systemBars.getBottom(this).toDp() }
- val statusBarHeight = with(LocalDensity.current) { WindowInsets.statusBars.getTop(this).toDp() }
+ 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
@@ -420,28 +430,139 @@ private inline fun BaseScaffoldWithMarkets(
},
)
- Column {
- WalletTopBar(config = state.topBarConfig)
- Box(
- modifier = Modifier.pullRefresh(pullRefreshState),
- ) {
- content()
+ Box {
+ Column {
+ WalletTopBar(config = state.topBarConfig)
+ Box(
+ modifier = Modifier.pullRefresh(pullRefreshState),
+ ) {
+ content()
- WalletPullToRefreshIndicator(
- isRefreshing = selectedWallet.pullToRefreshConfig.isRefreshing,
- state = pullRefreshState,
- modifier = Modifier.align(Alignment.TopCenter),
- )
+ WalletPullToRefreshIndicator(
+ isRefreshing = selectedWallet.pullToRefreshConfig.isRefreshing,
+ state = pullRefreshState,
+ modifier = Modifier.align(Alignment.TopCenter),
+ )
+ }
}
- }
- BottomSheetScrim(
- color = BottomSheetDefaults.ScrimColor,
- visible = bottomSheetState.targetValue == SheetValue.Expanded,
- onDismissRequest = { coroutineScope.launch { bottomSheetState.partialExpand() } },
- )
+ BottomSheetScrim(
+ color = BottomSheetDefaults.ScrimColor,
+ visible = bottomSheetState.targetValue == SheetValue.Expanded || state.showMarketsOnboarding,
+ onDismissRequest = { coroutineScope.launch { bottomSheetState.partialExpand() } },
+ )
+
+ MarketsTooltip(
+ modifier = Modifier
+ .align(Alignment.BottomCenter)
+ .padding(bottom = 24.dp)
+ .fillMaxWidth(fraction = 0.7f),
+ isVisible = state.showMarketsOnboarding,
+ availableHeight = maxHeight - statusBarHeight - bottomBarHeight,
+ bottomSheetState = bottomSheetState,
+ )
+ }
},
)
+
+ LaunchedEffect(state.showMarketsOnboarding, bottomSheetState.targetValue) {
+ if (state.showMarketsOnboarding && bottomSheetState.targetValue == SheetValue.Expanded) {
+ state.onDismissMarketsOnboarding()
+ }
+ }
+ }
+}
+
+@OptIn(ExperimentalMaterial3Api::class)
+@Composable
+private fun MarketsTooltip(
+ availableHeight: Dp,
+ bottomSheetState: SheetState,
+ isVisible: Boolean,
+ modifier: Modifier = Modifier,
+) {
+ val density = LocalDensity.current
+ val tooltipOffset by remember {
+ derivedStateOf {
+ val bottomSheetOffset = try {
+ // Can throw exception during the first composition
+ with(density) { bottomSheetState.requireOffset().toDp() }
+ } catch (e: Exception) {
+ 0.dp
+ }
+
+ bottomSheetOffset - availableHeight
+ }
+ }
+
+ var visible by remember { mutableStateOf(value = false) }
+ LaunchedEffect(isVisible) {
+ if (isVisible) {
+ delay(timeMillis = 300)
+ }
+
+ visible = isVisible
+ }
+
+ val slideOffset = 40.dp.toPx()
+ AnimatedVisibility(
+ modifier = modifier.absoluteOffset(y = tooltipOffset),
+ visible = visible,
+ enter = slideIn(
+ animationSpec = spring(
+ stiffness = Spring.StiffnessLow,
+ visibilityThreshold = IntOffset.VisibilityThreshold,
+ ),
+ initialOffset = { _ -> IntOffset(y = -slideOffset.roundToInt(), x = 0) },
+ ) + fadeIn(),
+ exit = fadeOut(),
+ ) {
+ MarketsTooltipContent()
+ }
+}
+
+@Composable
+private fun MarketsTooltipContent(modifier: Modifier = Modifier) {
+ val backgroundColor = TangemTheme.colors.background.primary
+ val cornerRadius = CornerRadius(x = 14.dp.toPx())
+ val tipDpSize = DpSize(width = 20.dp, height = 8.dp)
+
+ Column(
+ modifier = modifier
+ .padding(bottom = tipDpSize.height)
+ .drawBehind {
+ val rect = size.toRect()
+ val tipSize = tipDpSize.toSize()
+ val tipRect = Rect(
+ offset = Offset(
+ x = rect.center.x - tipSize.center.x,
+ y = rect.bottom,
+ ),
+ size = tipSize,
+ )
+ drawRoundRect(color = backgroundColor, cornerRadius = cornerRadius)
+
+ val tipPath = Path().apply {
+ moveTo(tipRect.topLeft)
+ lineTo(tipRect.bottomCenter)
+ lineTo(tipRect.topRight)
+ }
+ drawPath(color = backgroundColor, path = tipPath)
+ }
+ .padding(all = 12.dp),
+ verticalArrangement = Arrangement.spacedBy(space = 4.dp),
+ horizontalAlignment = Alignment.Start,
+ ) {
+ Text(
+ text = stringResource(id = R.string.markets_tooltip_title),
+ style = TangemTheme.typography.subtitle2,
+ color = TangemTheme.colors.text.primary1,
+ )
+ Text(
+ text = stringResource(id = R.string.markets_tooltip_message),
+ style = TangemTheme.typography.caption2,
+ color = TangemTheme.colors.text.secondary,
+ )
}
}
@@ -449,7 +570,7 @@ private inline fun BaseScaffoldWithMarkets(
private fun BottomSheetScrim(color: Color, visible: Boolean, onDismissRequest: () -> Unit) {
val alpha by animateFloatAsState(
targetValue = if (visible) 1f else 0f,
- animationSpec = TweenSpec(),
+ animationSpec = tween(),
label = "scrim",
)
val dismissSheet = if (visible) {
@@ -557,9 +678,9 @@ private fun BottomSheetStateEffects(
LaunchedEffect(isSheetHidden) {
onBottomSheetStateChange(
if (isSheetHidden) {
- COLLAPSED
+ BottomSheetState.COLLAPSED
} else {
- EXPANDED
+ BottomSheetState.EXPANDED
},
)
}