Updated on 2026-08-14

This commit is contained in:
Tangem 2026-03-06 17:11:59 +03:00
parent 47db65fdbe
commit 9f2b2eac71
21 changed files with 658 additions and 72 deletions

View file

@ -6,6 +6,7 @@ import com.tangem.core.decompose.ui.DefaultUiMessageSender
import com.tangem.core.decompose.ui.UiMessageSender import com.tangem.core.decompose.ui.UiMessageSender
import com.tangem.core.ui.DesignFeatureToggles import com.tangem.core.ui.DesignFeatureToggles
import com.tangem.core.ui.UiDependencies import com.tangem.core.ui.UiDependencies
import com.tangem.core.ui.components.snackbar.TangemTopSnackbarHostState
import com.tangem.core.ui.haptic.VibratorHapticManager import com.tangem.core.ui.haptic.VibratorHapticManager
import com.tangem.core.ui.message.EventMessageHandler import com.tangem.core.ui.message.EventMessageHandler
import com.tangem.core.ui.theme.AppThemeModeHolder import com.tangem.core.ui.theme.AppThemeModeHolder
@ -30,6 +31,7 @@ internal object UiDependenciesModule {
override val vibratorHapticManager = vibratorHapticManager override val vibratorHapticManager = vibratorHapticManager
override val appThemeModeHolder = appThemeModeHolder override val appThemeModeHolder = appThemeModeHolder
override val globalSnackbarHostState: SnackbarHostState = SnackbarHostState() override val globalSnackbarHostState: SnackbarHostState = SnackbarHostState()
override val globalTopSnackbarHostState: TangemTopSnackbarHostState = TangemTopSnackbarHostState()
override val eventMessageHandler: EventMessageHandler = EventMessageHandler() override val eventMessageHandler: EventMessageHandler = EventMessageHandler()
override val designFeatureToggles: DesignFeatureToggles = designFeatureToggles override val designFeatureToggles: DesignFeatureToggles = designFeatureToggles
} }

View file

@ -9,6 +9,7 @@ import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.imePadding import androidx.compose.foundation.layout.imePadding
import androidx.compose.foundation.layout.navigationBarsPadding import androidx.compose.foundation.layout.navigationBarsPadding
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.statusBarsPadding
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
@ -28,9 +29,12 @@ import com.arkivanov.essenty.backhandler.BackHandler
import com.tangem.common.routing.AppRoute import com.tangem.common.routing.AppRoute
import com.tangem.core.ui.UiDependencies import com.tangem.core.ui.UiDependencies
import com.tangem.core.ui.components.snackbar.TangemSnackbarHost import com.tangem.core.ui.components.snackbar.TangemSnackbarHost
import com.tangem.core.ui.components.snackbar.TangemTopSnackbarHost
import com.tangem.core.ui.message.EventMessageEffect import com.tangem.core.ui.message.EventMessageEffect
import com.tangem.core.ui.res.LocalRedesignEnabled
import com.tangem.core.ui.res.LocalRootBackgroundColor import com.tangem.core.ui.res.LocalRootBackgroundColor
import com.tangem.core.ui.res.LocalSnackbarHostState import com.tangem.core.ui.res.LocalSnackbarHostState
import com.tangem.core.ui.res.LocalTopSnackbarHostState
import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.security.ProvideSecureFlagController import com.tangem.core.ui.security.ProvideSecureFlagController
import com.tangem.tap.routing.component.RoutingComponent import com.tangem.tap.routing.component.RoutingComponent
@ -93,6 +97,16 @@ internal fun RootContent(
.padding(all = 16.dp), .padding(all = 16.dp),
hostState = snackbarHostState, hostState = snackbarHostState,
) )
if (LocalRedesignEnabled.current) {
TangemTopSnackbarHost(
modifier = Modifier
.align(Alignment.TopCenter)
.statusBarsPadding()
.padding(all = 16.dp),
hostState = LocalTopSnackbarHostState.current,
)
}
} }
} }
EventMessageEffect() EventMessageEffect()

View file

@ -24,7 +24,7 @@ dependencies {
/** Project - Core */ /** Project - Core */
implementation(projects.core.res) implementation(projects.core.res)
implementation(projects.core.utils) implementation(projects.core.utils)
implementation(projects.core.decompose) api(projects.core.decompose)
implementation(projects.core.error) implementation(projects.core.error)
/** AndroidX libraries */ /** AndroidX libraries */

View file

@ -2,6 +2,7 @@ package com.tangem.core.ui
import androidx.compose.material3.SnackbarHostState import androidx.compose.material3.SnackbarHostState
import androidx.compose.runtime.Stable import androidx.compose.runtime.Stable
import com.tangem.core.ui.components.snackbar.TangemTopSnackbarHostState
import com.tangem.core.ui.haptic.VibratorHapticManager import com.tangem.core.ui.haptic.VibratorHapticManager
import com.tangem.core.ui.message.EventMessageHandler import com.tangem.core.ui.message.EventMessageHandler
import com.tangem.core.ui.theme.AppThemeModeHolder import com.tangem.core.ui.theme.AppThemeModeHolder
@ -15,6 +16,8 @@ interface UiDependencies {
val globalSnackbarHostState: SnackbarHostState val globalSnackbarHostState: SnackbarHostState
val globalTopSnackbarHostState: TangemTopSnackbarHostState
val eventMessageHandler: EventMessageHandler val eventMessageHandler: EventMessageHandler
val designFeatureToggles: DesignFeatureToggles val designFeatureToggles: DesignFeatureToggles

View file

@ -1,16 +1,27 @@
package com.tangem.core.ui.components.bottomsheets.internal package com.tangem.core.ui.components.bottomsheets.internal
import androidx.activity.compose.BackHandler import androidx.activity.compose.BackHandler
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.ColumnScope import androidx.compose.foundation.layout.ColumnScope
import androidx.compose.foundation.layout.WindowInsets import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.* import androidx.compose.material3.*
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.Shape import androidx.compose.ui.graphics.Shape
import androidx.compose.ui.input.key.* import androidx.compose.ui.input.key.*
import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import com.tangem.core.ui.components.haze.hazeSourceTangem
import com.tangem.core.ui.components.snackbar.TangemTopSnackbarHost
import com.tangem.core.ui.res.LocalHazeState
import com.tangem.core.ui.res.LocalRedesignEnabled
import com.tangem.core.ui.res.LocalTopSnackbarHostState
import dev.chrisbanes.haze.rememberHazeState
import kotlinx.coroutines.coroutineScope import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@ -31,6 +42,9 @@ fun ModalBottomSheetWithBackHandling(
contentWindowInsets: @Composable () -> WindowInsets = { BottomSheetDefaults.windowInsets }, contentWindowInsets: @Composable () -> WindowInsets = { BottomSheetDefaults.windowInsets },
content: @Composable ColumnScope.() -> Unit, content: @Composable ColumnScope.() -> Unit,
) { ) {
val topSnackbarHostState = LocalTopSnackbarHostState.current
val isRedesignEnabled = LocalRedesignEnabled.current
ModalBottomSheet( ModalBottomSheet(
onDismissRequest = onDismissRequest, onDismissRequest = onDismissRequest,
modifier = modifier, modifier = modifier,
@ -47,7 +61,27 @@ fun ModalBottomSheetWithBackHandling(
shouldDismissOnBackPress = onBack == null, shouldDismissOnBackPress = onBack == null,
), ),
content = { content = {
content() if (isRedesignEnabled) {
Box {
val hazeState = rememberHazeState()
Column(Modifier.hazeSourceTangem(hazeState, zIndex = -1f)) {
content()
}
CompositionLocalProvider(LocalHazeState provides hazeState) {
TangemTopSnackbarHost(
modifier = Modifier
.align(Alignment.TopCenter)
.padding(horizontal = 16.dp)
.padding(top = 24.dp),
hostState = topSnackbarHostState,
)
}
}
} else {
content()
}
BackHandler(enabled = onBack != null && sheetState.targetValue != SheetValue.Hidden) { BackHandler(enabled = onBack != null && sheetState.targetValue != SheetValue.Hidden) {
onBack?.invoke() onBack?.invoke()

View file

@ -167,6 +167,7 @@ inline fun <reified T : TangemBottomSheetConfigContent> BasicModalBottomSheetWit
val model = config.content as? T ?: return val model = config.content as? T ?: return
val bsContent: @Composable ColumnScope.() -> Unit = { val bsContent: @Composable ColumnScope.() -> Unit = {
// FIXME: Use LocalWindowSize.current
val maxHeight = LocalConfiguration.current.screenHeightDp * MODAL_SHEET_MAX_HEIGHT val maxHeight = LocalConfiguration.current.screenHeightDp * MODAL_SHEET_MAX_HEIGHT
val initial = 0 val initial = 0
val scrollState = rememberScrollState(initial = initial) val scrollState = rememberScrollState(initial = initial)

View file

@ -17,6 +17,7 @@ import androidx.compose.ui.Modifier
* *
[REDACTED_AUTHOR] [REDACTED_AUTHOR]
*/ */
@Deprecated("Use TangemTopSnackbarHost instead. Will be removed with redesign")
@Composable @Composable
fun CopiedTextSnackbarHost(hostState: SnackbarHostState, modifier: Modifier = Modifier) { fun CopiedTextSnackbarHost(hostState: SnackbarHostState, modifier: Modifier = Modifier) {
SnackbarHost(hostState = hostState, modifier = modifier) { SnackbarHost(hostState = hostState, modifier = modifier) {

View file

@ -24,6 +24,7 @@ import com.tangem.core.ui.res.TangemThemePreview
* *
[REDACTED_AUTHOR] [REDACTED_AUTHOR]
*/ */
@Deprecated("With redesign, this component is no longer used. Please use both TangemSnackbar and TangemTopSnackbar")
@Composable @Composable
fun TangemSnackbar(data: SnackbarData, modifier: Modifier = Modifier, actionOnNewLine: Boolean = false) { fun TangemSnackbar(data: SnackbarData, modifier: Modifier = Modifier, actionOnNewLine: Boolean = false) {
Snackbar( Snackbar(

View file

@ -27,6 +27,10 @@ import com.tangem.core.ui.res.TangemThemePreview
* *
[REDACTED_AUTHOR] [REDACTED_AUTHOR]
*/ */
@Deprecated(
"With redesign, this component is no longer used. " +
"Please use both TangemSnackbarHost and TangemTopSnackbarHost",
)
@Composable @Composable
fun TangemSnackbarHost( fun TangemSnackbarHost(
modifier: Modifier = Modifier, modifier: Modifier = Modifier,

View file

@ -0,0 +1,180 @@
package com.tangem.core.ui.components.snackbar
import android.content.res.Configuration
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.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.draw.shadow
import androidx.compose.ui.res.painterResource
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.R
import com.tangem.core.ui.components.SpacerW
import com.tangem.core.ui.components.haze.hazeEffectTangem
import com.tangem.core.ui.ds.button.SecondaryTangemButton
import com.tangem.core.ui.ds.button.TangemButtonShape
import com.tangem.core.ui.ds.button.TangemButtonSize
import com.tangem.core.ui.extensions.resolveReference
import com.tangem.core.ui.extensions.stringReference
import com.tangem.core.ui.message.SnackbarMessage
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreviewRedesign
/**
* Top snackbar with an optional leading icon and an optional action button.
* Intended to be used with [TangemTopSnackbarHost].
*
* @param snackbarMessage message data
* @param modifier modifier
*/
@Suppress("LongMethod")
@Composable
fun TangemTopSnackbar(snackbarMessage: SnackbarMessage, modifier: Modifier = Modifier) {
val actionLabel = snackbarMessage.actionLabel
val action = snackbarMessage.action
val hasAction = actionLabel != null && action != null
var isTextOverflowing by remember { mutableStateOf(false) }
val shape = if (isTextOverflowing) {
RoundedCornerShape(TangemTheme.dimens2.x5)
} else {
TangemTheme.shapes.roundedCornersXLarge
}
Column(
modifier = modifier
.shadow(elevation = TangemTheme.dimens.elevation4, shape = shape, clip = false)
.background(color = TangemTheme.colors2.controls.backgroundDefault, shape = shape)
.clip(shape)
.hazeEffectTangem()
.sizeIn(minHeight = TangemTheme.dimens2.x11)
.padding(start = TangemTheme.dimens2.x5, end = TangemTheme.dimens2.x1)
.padding(vertical = TangemTheme.dimens2.x1),
verticalArrangement = Arrangement.Center,
) {
Row(
modifier = Modifier.padding(top = if (isTextOverflowing) TangemTheme.dimens2.x3 else 0.dp),
verticalAlignment = Alignment.CenterVertically,
) {
if (snackbarMessage.startIconId != null) {
Icon(
painter = painterResource(id = snackbarMessage.startIconId),
contentDescription = null,
modifier = Modifier.size(TangemTheme.dimens2.x5),
tint = TangemTheme.colors2.graphic.neutral.secondary,
)
SpacerW(TangemTheme.dimens2.x2)
}
Text(
text = snackbarMessage.message.resolveReference(),
modifier = Modifier.weight(1f, fill = false),
color = TangemTheme.colors.text.secondary,
overflow = TextOverflow.Ellipsis,
maxLines = 1,
style = TangemTheme.typography.body2,
onTextLayout = { if (hasAction) isTextOverflowing = it.hasVisualOverflow },
)
SpacerW(TangemTheme.dimens2.x4)
if (hasAction && !isTextOverflowing) {
SecondaryTangemButton(
text = actionLabel,
onClick = action,
size = TangemButtonSize.X9,
shape = TangemButtonShape.Rounded,
)
}
}
if (hasAction && isTextOverflowing) {
Box(
modifier = Modifier
.fillMaxWidth()
.padding(
top = TangemTheme.dimens2.x4,
bottom = TangemTheme.dimens2.x1,
),
contentAlignment = Alignment.CenterEnd,
) {
SecondaryTangemButton(
text = actionLabel,
onClick = action,
size = TangemButtonSize.X9,
shape = TangemButtonShape.Rounded,
)
}
}
}
}
@Preview(showBackground = true, widthDp = 360)
@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES)
@Composable
private fun Preview_TangemTopSnackbar_WithAction() {
TangemThemePreviewRedesign {
Column(
modifier = Modifier.padding(TangemTheme.dimens.spacing16),
verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing16),
) {
TangemTopSnackbar(
snackbarMessage = SnackbarMessage(
startIconId = R.drawable.ic_eye_off_outline_24,
message = stringReference("Balances hidden"),
actionLabel = stringReference("Undo"),
action = {},
),
)
TangemTopSnackbar(
snackbarMessage = SnackbarMessage(
startIconId = R.drawable.ic_eye_off_outline_24,
message = stringReference(
"Balances hidden long long text that should be truncated with ellipsis at the end",
),
actionLabel = stringReference("Undo"),
action = {},
),
)
}
}
}
@Preview(showBackground = true, widthDp = 360)
@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES)
@Composable
private fun Preview_TangemTopSnackbar_NoAction() {
TangemThemePreviewRedesign {
Column(
modifier = Modifier.padding(TangemTheme.dimens.spacing16),
verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing16),
) {
TangemTopSnackbar(
snackbarMessage = SnackbarMessage(
startIconId = R.drawable.ic_check_24,
message = stringReference("Text copied to clipboard"),
),
)
TangemTopSnackbar(
snackbarMessage = SnackbarMessage(
message = stringReference("Operation completed"),
),
)
TangemTopSnackbar(
snackbarMessage = SnackbarMessage(
message = stringReference(
"Operation completed long long text that should be truncated with ellipsis at the end",
),
),
)
}
}
}

View file

@ -0,0 +1,274 @@
@file:Suppress("MagicNumber")
package com.tangem.core.ui.components.snackbar
import androidx.compose.animation.core.Animatable
import androidx.compose.animation.core.AnimationSpec
import androidx.compose.animation.core.Spring
import androidx.compose.animation.core.spring
import androidx.compose.animation.core.tween
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.TransformOrigin
import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.platform.AccessibilityManager
import androidx.compose.ui.platform.LocalAccessibilityManager
import com.tangem.core.ui.extensions.stringReference
import com.tangem.core.ui.message.SnackbarMessage
import com.tangem.core.ui.res.TangemTheme
import kotlinx.coroutines.delay
import kotlinx.coroutines.suspendCancellableCoroutine
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import kotlin.coroutines.resume
/**
* Snackbar host that displays a [TangemTopSnackbar] sliding and scaling in from the top of the screen.
* Typical placement: at the top of the screen's root [Box], above the main content.
*
* ```
* Box(Modifier.fillMaxSize()) {
* MainContent()
* TangemTopSnackbarHost(
* hostState = topSnackbarHostState,
* modifier = Modifier
* .align(Alignment.TopCenter)
* .statusBarsPadding()
* .padding(top = TangemTheme.dimens.spacing8),
* )
* }
* ```
*
* @param hostState state that controls which snackbar is shown
* @param modifier modifier applied to the host container
*/
@Composable
fun TangemTopSnackbarHost(hostState: TangemTopSnackbarHostState, modifier: Modifier = Modifier) {
val myDepth = remember(hostState) { hostState.registerHost() }
DisposableEffect(hostState) {
onDispose { hostState.unregisterHost(myDepth) }
}
// Only the deepest host in the composition tree handles the snackbar.
val isDeepest = hostState.activeHostDepth == myDepth
val currentSnackbar = if (isDeepest) hostState.currentSnackbar else null
val accessibilityManager = LocalAccessibilityManager.current
LaunchedEffect(currentSnackbar) {
if (currentSnackbar == null) return@LaunchedEffect
val duration = currentSnackbar.duration.toMillis(
hasAction = currentSnackbar.action != null,
accessibilityManager = accessibilityManager,
)
delay(duration)
currentSnackbar.onDismissRequest()
}
ScaleFromTopWithFade(
current = currentSnackbar,
modifier = modifier,
) { snackbar ->
TangemTopSnackbar(
snackbarMessage = snackbar,
modifier = Modifier.padding(horizontal = TangemTheme.dimens.spacing16),
)
}
}
private fun SnackbarMessage.Duration.toMillis(hasAction: Boolean, accessibilityManager: AccessibilityManager?): Long {
val original =
when (this) {
SnackbarMessage.Duration.Indefinite -> Long.MAX_VALUE
SnackbarMessage.Duration.Long -> 10000L
SnackbarMessage.Duration.Short -> 4000L
}
if (accessibilityManager == null) {
return original
}
return accessibilityManager.calculateRecommendedTimeoutMillis(
originalTimeoutMillis = original,
containsIcons = true,
containsText = true,
containsControls = hasAction,
)
}
@Stable
class TangemTopSnackbarHostState {
private val mutex = Mutex()
var currentSnackbar by mutableStateOf<SnackbarMessage?>(null)
// Tracks which depth level is the deepest registered host.
// Composed as observable state so hosts recompose when a deeper/shallower one is added/removed.
var activeHostDepth by mutableIntStateOf(0)
private set
private var hostDepthCounter = 0
internal fun registerHost(): Int {
hostDepthCounter++
activeHostDepth = hostDepthCounter
return hostDepthCounter
}
internal fun unregisterHost(depth: Int) {
if (depth == hostDepthCounter) {
hostDepthCounter--
activeHostDepth = hostDepthCounter
}
}
suspend fun showSnackbar(
message: String,
actionLabel: String? = null,
withDismissAction: Boolean = false,
duration: SnackbarMessage.Duration =
if (actionLabel == null) SnackbarMessage.Duration.Short else SnackbarMessage.Duration.Indefinite,
) {
showSnackbar(
SnackbarMessage(
message = stringReference(message),
actionLabel = actionLabel?.let { stringReference(it) },
duration = duration,
onDismissRequest = {},
action = if (withDismissAction) {
null
} else {
{}
},
),
)
}
suspend fun showSnackbar(message: SnackbarMessage) {
mutex.withLock {
try {
suspendCancellableCoroutine { continuation ->
currentSnackbar = message.withDismiss(
onDismiss = { if (continuation.isActive) continuation.resume(Unit) },
)
}
} finally {
currentSnackbar = null
}
}
}
}
private fun SnackbarMessage.withDismiss(onDismiss: () -> Unit): SnackbarMessage {
return copy(
onDismissRequest = {
onDismissRequest()
onDismiss()
},
action = if (action != null) {
{
action.invoke()
onDismiss()
}
} else {
null
},
)
}
// Adapted from Material3's FadeInFadeOutWithScale, with scale pivot anchored at the top center.
@Suppress("UnsafeCallOnNullableType")
@Composable
private fun ScaleFromTopWithFade(
current: SnackbarMessage?,
modifier: Modifier = Modifier,
content: @Composable (SnackbarMessage) -> Unit,
) {
val state = remember { ScaleFromTopState<SnackbarMessage?>() }
if (current != state.current) {
state.current = current
val keys = state.items.map { it.key }.toMutableList()
if (!keys.contains(current)) keys.add(current)
state.items.clear()
keys.filterNotNull().mapTo(state.items) { key ->
ScaleFromTopItem(key) { children ->
val isVisible = key == current
val opacity = animatedOpacity(
animation = tween(durationMillis = 200),
visible = isVisible,
onAnimationFinish = {
if (key != state.current) {
state.items.removeAll { it.key == key }
state.scope?.invalidate()
}
},
)
val scale = animatedScale(
enterAnimation = spring(
dampingRatio = Spring.DampingRatioMediumBouncy,
stiffness = Spring.StiffnessMedium,
),
exitAnimation = tween(durationMillis = 180),
visible = isVisible,
)
Box(
Modifier.graphicsLayer(
scaleX = scale.value,
scaleY = scale.value,
alpha = opacity.value,
transformOrigin = TransformOrigin(pivotFractionX = 0.5f, pivotFractionY = 0f),
),
) {
children()
}
}
}
}
Box(modifier = modifier.fillMaxWidth(), contentAlignment = Alignment.TopCenter) {
state.scope = currentRecomposeScope
state.items.forEach { (item, transition) -> key(item) { transition { content(item!!) } } }
}
}
@Suppress("DoubleMutabilityForCollection")
private class ScaleFromTopState<T> {
var current: Any? = Any()
var items = mutableListOf<ScaleFromTopItem<T>>()
var scope: RecomposeScope? = null
}
private data class ScaleFromTopItem<T>(
val key: T,
val transition: @Composable (content: @Composable () -> Unit) -> Unit,
)
@Composable
private fun animatedOpacity(
animation: AnimationSpec<Float>,
visible: Boolean,
onAnimationFinish: () -> Unit = {},
): State<Float> {
val alpha = remember { Animatable(if (visible) 0f else 1f) }
LaunchedEffect(visible) {
alpha.animateTo(if (visible) 1f else 0f, animationSpec = animation)
onAnimationFinish()
}
return alpha.asState()
}
@Composable
private fun animatedScale(
enterAnimation: AnimationSpec<Float>,
exitAnimation: AnimationSpec<Float>,
visible: Boolean,
): State<Float> {
val scale = remember { Animatable(if (visible) 0.85f else 1f) }
LaunchedEffect(visible) {
scale.animateTo(
targetValue = if (visible) 1f else 0.85f,
animationSpec = if (visible) enterAnimation else exitAnimation,
)
}
return scale.asState()
}

View file

@ -18,6 +18,7 @@ import com.tangem.core.ui.extensions.resourceReference
@Immutable @Immutable
sealed interface EventMessage : UiMessage sealed interface EventMessage : UiMessage
@Immutable
data class ToastMessage( data class ToastMessage(
val message: TextReference, val message: TextReference,
val duration: Duration = ToastMessage.Duration.Short, val duration: Duration = ToastMessage.Duration.Short,
@ -48,8 +49,10 @@ data class ToastMessage(
* @param actionLabel The label of the action button. Optional, `null` by default. * @param actionLabel The label of the action button. Optional, `null` by default.
* @param action The action to perform when the action button is clicked. Optional, `null` by default. * @param action The action to perform when the action button is clicked. Optional, `null` by default.
* */ * */
@Immutable
data class SnackbarMessage( data class SnackbarMessage(
val message: TextReference, val message: TextReference,
@field:DrawableRes val startIconId: Int? = null,
val duration: Duration = Duration.Short, val duration: Duration = Duration.Short,
val onDismissRequest: () -> Unit = {}, val onDismissRequest: () -> Unit = {},
val actionLabel: TextReference? = null, val actionLabel: TextReference? = null,
@ -92,6 +95,7 @@ data class SnackbarMessage(
* `true` by default. * `true` by default.
* @param onDismissRequest The action to perform when the dialog is dismissed. * @param onDismissRequest The action to perform when the dialog is dismissed.
* */ * */
@Immutable
data class DialogMessage( data class DialogMessage(
val message: TextReference, val message: TextReference,
val title: TextReference? = null, val title: TextReference? = null,
@ -146,6 +150,7 @@ data class DialogMessage(
} }
} }
@Immutable
data class GlobalLoadingMessage(val isShow: Boolean) : EventMessage data class GlobalLoadingMessage(val isShow: Boolean) : EventMessage
/** /**
@ -159,6 +164,7 @@ data class GlobalLoadingMessage(val isShow: Boolean) : EventMessage
* @param secondAction The second action to perform. Optional, `null` by default. * @param secondAction The second action to perform. Optional, `null` by default.
* @param onDismissRequest The action to perform when the bottom sheet is dismissed. * @param onDismissRequest The action to perform when the bottom sheet is dismissed.
* */ * */
@Immutable
data class BottomSheetMessage( data class BottomSheetMessage(
@DrawableRes val iconResId: Int? = null, @DrawableRes val iconResId: Int? = null,
val title: TextReference? = null, val title: TextReference? = null,
@ -202,6 +208,7 @@ data class BottomSheetMessage(
} }
} }
@Immutable
data class BottomSheetMessageV2( data class BottomSheetMessageV2(
val messageBottomSheetUMV2: MessageBottomSheetUMV2, val messageBottomSheetUMV2: MessageBottomSheetUMV2,
) : EventMessage ) : EventMessage

View file

@ -23,10 +23,13 @@ import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig
import com.tangem.core.ui.components.bottomsheets.message.MessageBottomSheet import com.tangem.core.ui.components.bottomsheets.message.MessageBottomSheet
import com.tangem.core.ui.components.bottomsheets.message.MessageBottomSheetUM import com.tangem.core.ui.components.bottomsheets.message.MessageBottomSheetUM
import com.tangem.core.ui.components.bottomsheets.message.MessageBottomSheetV2 import com.tangem.core.ui.components.bottomsheets.message.MessageBottomSheetV2
import com.tangem.core.ui.components.snackbar.TangemTopSnackbarHostState
import com.tangem.core.ui.event.EventEffect import com.tangem.core.ui.event.EventEffect
import com.tangem.core.ui.extensions.resolveReference import com.tangem.core.ui.extensions.resolveReference
import com.tangem.core.ui.res.LocalEventMessageHandler import com.tangem.core.ui.res.LocalEventMessageHandler
import com.tangem.core.ui.res.LocalRedesignEnabled
import com.tangem.core.ui.res.LocalSnackbarHostState import com.tangem.core.ui.res.LocalSnackbarHostState
import com.tangem.core.ui.res.LocalTopSnackbarHostState
import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemTheme
@Composable @Composable
@ -36,6 +39,10 @@ fun EventMessageEffect(
onShowSnackbar: suspend (SnackbarMessage, Context) -> Unit = { message, context -> onShowSnackbar: suspend (SnackbarMessage, Context) -> Unit = { message, context ->
showSnackbar(snackbarHostState, message, context) showSnackbar(snackbarHostState, message, context)
}, },
topSnackbarHostState: TangemTopSnackbarHostState = LocalTopSnackbarHostState.current,
onShowTopSnackbar: suspend (SnackbarMessage) -> Unit = { message ->
topSnackbarHostState.showSnackbar(message)
},
onShowToast: (ToastMessage, Context) -> Unit = { message, context -> showToast(message, context) }, onShowToast: (ToastMessage, Context) -> Unit = { message, context -> showToast(message, context) },
) { ) {
val messageEvent by messageHandler.collectAsState() val messageEvent by messageHandler.collectAsState()
@ -45,11 +52,16 @@ fun EventMessageEffect(
var bottomSheetMessage: BottomSheetMessage? by remember { mutableStateOf(value = null) } var bottomSheetMessage: BottomSheetMessage? by remember { mutableStateOf(value = null) }
var bottomSheetMessageV2: BottomSheetMessageV2? by remember { mutableStateOf(value = null) } var bottomSheetMessageV2: BottomSheetMessageV2? by remember { mutableStateOf(value = null) }
var loadingMessage: GlobalLoadingMessage? by remember { mutableStateOf(value = null) } var loadingMessage: GlobalLoadingMessage? by remember { mutableStateOf(value = null) }
val isRedesignEnabled = LocalRedesignEnabled.current
EventEffect(event = messageEvent) { message -> EventEffect(event = messageEvent) { message ->
when (message) { when (message) {
is SnackbarMessage -> { is SnackbarMessage -> {
onShowSnackbar(message, context) if (isRedesignEnabled) {
onShowTopSnackbar(message)
} else {
onShowSnackbar(message, context)
}
} }
is DialogMessage -> { is DialogMessage -> {
dialogMessage = message dialogMessage = message
@ -220,7 +232,8 @@ private fun showToast(message: ToastMessage, context: Context) {
Toast.makeText( Toast.makeText(
/* context = */ context, /* context = */ context,
/* text = */ message.message.resolveReference(context.resources), /* text = */ message.message.resolveReference(context.resources),
/* duration = */ when (message.duration) { /* duration = */
when (message.duration) {
ToastMessage.Duration.Short -> Toast.LENGTH_SHORT ToastMessage.Duration.Short -> Toast.LENGTH_SHORT
ToastMessage.Duration.Long -> Toast.LENGTH_LONG ToastMessage.Duration.Long -> Toast.LENGTH_LONG
}, },

View file

@ -20,6 +20,7 @@ import com.tangem.core.ui.components.SystemBarsIconsController
import com.tangem.core.ui.components.TangemShimmer import com.tangem.core.ui.components.TangemShimmer
import com.tangem.core.ui.components.powersaving.PowerSavingState import com.tangem.core.ui.components.powersaving.PowerSavingState
import com.tangem.core.ui.components.powersaving.rememberPowerSavingState import com.tangem.core.ui.components.powersaving.rememberPowerSavingState
import com.tangem.core.ui.components.snackbar.TangemTopSnackbarHostState
import com.tangem.core.ui.components.text.BladeAnimation import com.tangem.core.ui.components.text.BladeAnimation
import com.tangem.core.ui.components.text.rememberBladeAnimation import com.tangem.core.ui.components.text.rememberBladeAnimation
import com.tangem.core.ui.haptic.DefaultHapticManager import com.tangem.core.ui.haptic.DefaultHapticManager
@ -54,6 +55,7 @@ fun TangemTheme(
vibratorHapticManager = uiDependencies.vibratorHapticManager, vibratorHapticManager = uiDependencies.vibratorHapticManager,
snackbarHostState = uiDependencies.globalSnackbarHostState, snackbarHostState = uiDependencies.globalSnackbarHostState,
eventMessageHandler = uiDependencies.eventMessageHandler, eventMessageHandler = uiDependencies.eventMessageHandler,
topSnackbarHostState = uiDependencies.globalTopSnackbarHostState,
overrideSystemBarColors = overrideSystemBarColors, overrideSystemBarColors = overrideSystemBarColors,
typography = typography, typography = typography,
dimens = dimens, dimens = dimens,
@ -76,6 +78,7 @@ fun TangemTheme(
vibratorHapticManager: VibratorHapticManager? = null, vibratorHapticManager: VibratorHapticManager? = null,
eventMessageHandler: EventMessageHandler = remember { EventMessageHandler() }, eventMessageHandler: EventMessageHandler = remember { EventMessageHandler() },
snackbarHostState: SnackbarHostState = remember { SnackbarHostState() }, snackbarHostState: SnackbarHostState = remember { SnackbarHostState() },
topSnackbarHostState: TangemTopSnackbarHostState = remember { TangemTopSnackbarHostState() },
overrideSystemBarColors: Boolean = true, overrideSystemBarColors: Boolean = true,
content: @Composable () -> Unit, content: @Composable () -> Unit,
) { ) {
@ -119,6 +122,7 @@ fun TangemTheme(
LocalIsInDarkTheme provides isDark, LocalIsInDarkTheme provides isDark,
LocalHapticManager provides hapticManager, LocalHapticManager provides hapticManager,
LocalSnackbarHostState provides snackbarHostState, LocalSnackbarHostState provides snackbarHostState,
LocalTopSnackbarHostState provides topSnackbarHostState,
LocalEventMessageHandler provides eventMessageHandler, LocalEventMessageHandler provides eventMessageHandler,
LocalWindowSize provides windowSize, LocalWindowSize provides windowSize,
LocalBladeAnimation provides rememberBladeAnimation(), LocalBladeAnimation provides rememberBladeAnimation(),
@ -389,6 +393,10 @@ val LocalSnackbarHostState = staticCompositionLocalOf<SnackbarHostState> {
error("No SnackbarHostState provided") error("No SnackbarHostState provided")
} }
val LocalTopSnackbarHostState = staticCompositionLocalOf<TangemTopSnackbarHostState> {
error("No TangemTopSnackbarHostState provided")
}
val LocalEventMessageHandler = staticCompositionLocalOf<EventMessageHandler> { val LocalEventMessageHandler = staticCompositionLocalOf<EventMessageHandler> {
error("No EventMessageHandler provided") error("No EventMessageHandler provided")
} }

View file

@ -33,8 +33,12 @@ import com.tangem.core.ui.components.account.AccountIconSize
import com.tangem.core.ui.components.rows.RoundableCornersRow import com.tangem.core.ui.components.rows.RoundableCornersRow
import com.tangem.core.ui.extensions.pluralStringResourceSafe import com.tangem.core.ui.extensions.pluralStringResourceSafe
import com.tangem.core.ui.extensions.resolveReference import com.tangem.core.ui.extensions.resolveReference
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.extensions.stringReference import com.tangem.core.ui.extensions.stringReference
import com.tangem.core.ui.extensions.stringResourceSafe import com.tangem.core.ui.extensions.stringResourceSafe
import com.tangem.core.ui.message.SnackbarMessage
import com.tangem.core.ui.res.LocalRedesignEnabled
import com.tangem.core.ui.res.LocalTopSnackbarHostState
import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreview import com.tangem.core.ui.res.TangemThemePreview
import com.tangem.feature.referral.domain.models.ExpectedAward import com.tangem.feature.referral.domain.models.ExpectedAward
@ -352,6 +356,8 @@ private fun AdditionalButtons(
val coroutineScope = rememberCoroutineScope() val coroutineScope = rememberCoroutineScope()
val resources = LocalContext.current.resources val resources = LocalContext.current.resources
val isRedesignEnabled = LocalRedesignEnabled.current
val tangemTopSnackbarHostState = LocalTopSnackbarHostState.current
Row( Row(
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
@ -366,10 +372,19 @@ private fun AdditionalButtons(
clipboardManager.setText(AnnotatedString(code)) clipboardManager.setText(AnnotatedString(code))
coroutineScope.launch { coroutineScope.launch {
snackbarHostState.showSnackbar( if (isRedesignEnabled) {
message = resources.getStringSafe(R.string.referral_promo_code_copied), tangemTopSnackbarHostState.showSnackbar(
duration = SnackbarDuration.Short, SnackbarMessage(
) startIconId = R.drawable.ic_check_24,
message = resourceReference(R.string.referral_promo_code_copied),
),
)
} else {
snackbarHostState.showSnackbar(
message = resources.getStringSafe(R.string.referral_promo_code_copied),
duration = SnackbarDuration.Short,
)
}
} }
}, },
modifier = Modifier.weight(1f), modifier = Modifier.weight(1f),

View file

@ -45,6 +45,9 @@ import com.tangem.core.ui.components.token.state.TokenItemState
import com.tangem.core.ui.components.token.state.TokenItemState.FiatAmountState import com.tangem.core.ui.components.token.state.TokenItemState.FiatAmountState
import com.tangem.core.ui.extensions.stringReference import com.tangem.core.ui.extensions.stringReference
import com.tangem.core.ui.extensions.stringResourceSafe import com.tangem.core.ui.extensions.stringResourceSafe
import com.tangem.core.ui.message.SnackbarMessage
import com.tangem.core.ui.res.LocalRedesignEnabled
import com.tangem.core.ui.res.LocalTopSnackbarHostState
import com.tangem.core.ui.res.TangemColorPalette import com.tangem.core.ui.res.TangemColorPalette
import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreview import com.tangem.core.ui.res.TangemThemePreview
@ -104,10 +107,24 @@ internal fun ReferralScreen(stateHolder: ReferralStateHolder) {
val errorSnackbar = stateHolder.errorSnackbar val errorSnackbar = stateHolder.errorSnackbar
val coroutineScope = rememberCoroutineScope() val coroutineScope = rememberCoroutineScope()
val resources = LocalContext.current.resources val resources = LocalContext.current.resources
val isRedesignEnabled = LocalRedesignEnabled.current
val topSnackbarHostState = LocalTopSnackbarHostState.current
SideEffect { SideEffect {
if (errorSnackbar != null) { if (errorSnackbar != null) {
coroutineScope.launch { coroutineScope.launch {
if (isRedesignEnabled) {
topSnackbarHostState.showSnackbar(
message = resources.getMessageForErrorSnackbar(errorSnackbar.throwable),
actionLabel = resources.getStringSafe(R.string.warning_button_ok),
duration = SnackbarMessage.Duration.Indefinite,
)
errorSnackbar.onOkClicked()
return@launch
}
val result = snackbarHostState.showSnackbar( val result = snackbarHostState.showSnackbar(
message = resources.getMessageForErrorSnackbar(errorSnackbar.throwable), message = resources.getMessageForErrorSnackbar(errorSnackbar.throwable),
actionLabel = resources.getStringSafe(R.string.warning_button_ok), actionLabel = resources.getStringSafe(R.string.warning_button_ok),

View file

@ -8,16 +8,23 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import com.tangem.core.ui.components.snackbar.CopiedTextSnackbarHost import com.tangem.core.ui.components.snackbar.CopiedTextSnackbarHost
import com.tangem.core.ui.res.LocalRedesignEnabled
@Deprecated(
"Use TangemTopSnackbarHost instead. Will be removed with redesign",
)
@Composable @Composable
internal fun ContainerWithSnackbarHost(snackbarHostState: SnackbarHostState, content: @Composable () -> Unit) { internal fun ContainerWithSnackbarHost(snackbarHostState: SnackbarHostState, content: @Composable () -> Unit) {
Box { Box {
content() content()
CopiedTextSnackbarHost(
hostState = snackbarHostState, if (LocalRedesignEnabled.current.not()) {
modifier = Modifier CopiedTextSnackbarHost(
.align(Alignment.BottomCenter) hostState = snackbarHostState,
.padding(bottom = 80.dp), modifier = Modifier
) .align(Alignment.BottomCenter)
.padding(bottom = 80.dp),
)
}
} }
} }

View file

@ -46,6 +46,9 @@ import com.tangem.core.ui.components.currency.icon.CurrencyIcon
import com.tangem.core.ui.components.currency.icon.CurrencyIconState import com.tangem.core.ui.components.currency.icon.CurrencyIconState
import com.tangem.core.ui.components.notifications.Notification import com.tangem.core.ui.components.notifications.Notification
import com.tangem.core.ui.extensions.* import com.tangem.core.ui.extensions.*
import com.tangem.core.ui.message.SnackbarMessage
import com.tangem.core.ui.res.LocalRedesignEnabled
import com.tangem.core.ui.res.LocalTopSnackbarHostState
import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreview import com.tangem.core.ui.res.TangemThemePreview
import com.tangem.features.tokenreceive.entity.ReceiveAddress import com.tangem.features.tokenreceive.entity.ReceiveAddress
@ -203,6 +206,8 @@ private fun AddressBlock(assetsUM: ReceiveAssetsUM, snackbarHostState: SnackbarH
val coroutineScope = rememberCoroutineScope() val coroutineScope = rememberCoroutineScope()
val context = LocalContext.current val context = LocalContext.current
val resources = context.resources val resources = context.resources
val isRedesignEnabled = LocalRedesignEnabled.current
val topSnackbarHostState = LocalTopSnackbarHostState.current
assetsUM.addresses assetsUM.addresses
.fastFilter { it.type is ReceiveAddress.Type.Ens } .fastFilter { it.type is ReceiveAddress.Type.Ens }
@ -214,11 +219,20 @@ private fun AddressBlock(assetsUM: ReceiveAssetsUM, snackbarHostState: SnackbarH
assetsUM.onCopyClick(address) assetsUM.onCopyClick(address)
hapticFeedback.performHapticFeedback(HapticFeedbackType.LongPress) hapticFeedback.performHapticFeedback(HapticFeedbackType.LongPress)
coroutineScope.launch { coroutineScope.launch {
snackbarHostState.showSnackbar( if (isRedesignEnabled) {
message = resources.getStringSafe( topSnackbarHostState.showSnackbar(
R.string.wallet_notification_address_copied, SnackbarMessage(
), startIconId = R.drawable.ic_check_24,
) message = resourceReference(R.string.wallet_notification_address_copied),
),
)
} else {
snackbarHostState.showSnackbar(
message = resources.getStringSafe(
R.string.wallet_notification_address_copied,
),
)
}
} }
}, },
address = address.value, address = address.value,
@ -413,6 +427,8 @@ private fun ButtonsBlock(snackbarHostState: SnackbarHostState, onCopyClick: () -
val coroutineScope = rememberCoroutineScope() val coroutineScope = rememberCoroutineScope()
val context = LocalContext.current val context = LocalContext.current
val resources = context.resources val resources = context.resources
val isRedesignEnabled = LocalRedesignEnabled.current
val topSnackbarHostState = LocalTopSnackbarHostState.current
Row( Row(
modifier = Modifier.width(IntrinsicSize.Min), modifier = Modifier.width(IntrinsicSize.Min),
@ -427,9 +443,18 @@ private fun ButtonsBlock(snackbarHostState: SnackbarHostState, onCopyClick: () -
onCopyClick() onCopyClick()
hapticFeedback.performHapticFeedback(HapticFeedbackType.LongPress) hapticFeedback.performHapticFeedback(HapticFeedbackType.LongPress)
coroutineScope.launch { coroutineScope.launch {
snackbarHostState.showSnackbar( if (isRedesignEnabled) {
message = resources.getStringSafe(R.string.wallet_notification_address_copied), topSnackbarHostState.showSnackbar(
) SnackbarMessage(
startIconId = R.drawable.ic_check_24,
message = resourceReference(R.string.wallet_notification_address_copied),
),
)
} else {
snackbarHostState.showSnackbar(
message = resources.getStringSafe(R.string.wallet_notification_address_copied),
)
}
} }
}, },
), ),

View file

@ -31,6 +31,9 @@ import androidx.compose.ui.unit.dp
import com.tangem.core.res.getStringSafe import com.tangem.core.res.getStringSafe
import com.tangem.core.ui.components.* import com.tangem.core.ui.components.*
import com.tangem.core.ui.extensions.* import com.tangem.core.ui.extensions.*
import com.tangem.core.ui.message.SnackbarMessage
import com.tangem.core.ui.res.LocalRedesignEnabled
import com.tangem.core.ui.res.LocalTopSnackbarHostState
import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreview import com.tangem.core.ui.res.TangemThemePreview
import com.tangem.core.ui.test.TokenReceiveQrCodeBottomSheetTestTags import com.tangem.core.ui.test.TokenReceiveQrCodeBottomSheetTestTags
@ -147,6 +150,9 @@ private fun Buttons(
val context = LocalContext.current val context = LocalContext.current
val resources = context.resources val resources = context.resources
val isRedesignEnabled = LocalRedesignEnabled.current
val tangemTopSnackbarHostState = LocalTopSnackbarHostState.current
Row( Row(
modifier = modifier, modifier = modifier,
horizontalArrangement = Arrangement.spacedBy(16.dp), horizontalArrangement = Arrangement.spacedBy(16.dp),
@ -159,9 +165,18 @@ private fun Buttons(
hapticFeedback.performHapticFeedback(HapticFeedbackType.LongPress) hapticFeedback.performHapticFeedback(HapticFeedbackType.LongPress)
onCopyClick() onCopyClick()
coroutineScope.launch { coroutineScope.launch {
snackbarHostState.showSnackbar( if (isRedesignEnabled) {
message = resources.getStringSafe(R.string.wallet_notification_address_copied), tangemTopSnackbarHostState.showSnackbar(
) SnackbarMessage(
startIconId = R.drawable.ic_check_24,
message = resourceReference(R.string.wallet_notification_address_copied),
),
)
} else {
snackbarHostState.showSnackbar(
message = resources.getStringSafe(R.string.wallet_notification_address_copied),
)
}
} }
}, },
) )

View file

@ -10,6 +10,9 @@ import com.tangem.core.res.getStringSafe
import com.tangem.core.ui.event.EventEffect import com.tangem.core.ui.event.EventEffect
import com.tangem.core.ui.event.StateEvent import com.tangem.core.ui.event.StateEvent
import com.tangem.core.ui.extensions.resolveReference import com.tangem.core.ui.extensions.resolveReference
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.message.SnackbarMessage
import com.tangem.core.ui.res.LocalTopSnackbarHostState
import com.tangem.core.ui.utils.requestPermission import com.tangem.core.ui.utils.requestPermission
import com.tangem.feature.wallet.impl.R import com.tangem.feature.wallet.impl.R
import com.tangem.feature.wallet.presentation.wallet.state.model.WalletEvent import com.tangem.feature.wallet.presentation.wallet.state.model.WalletEvent
@ -69,18 +72,17 @@ internal fun WalletEventEffectLegacy(
@Composable @Composable
internal fun WalletEventEffect( internal fun WalletEventEffect(
walletsPagerState: PagerState, walletsPagerState: PagerState,
snackbarHostState: SnackbarHostState,
event: StateEvent<WalletEvent>, event: StateEvent<WalletEvent>,
onCollapseBalance: () -> Unit, onCollapseBalance: () -> Unit,
) { ) {
val resources = LocalContext.current.resources
var showPermissionRequest by remember { mutableStateOf<Pair<() -> Unit, () -> Unit>?>(null) } var showPermissionRequest by remember { mutableStateOf<Pair<() -> Unit, () -> Unit>?>(null) }
HandlePermissionRequest( HandlePermissionRequest(
permissionRequestParams = showPermissionRequest, permissionRequestParams = showPermissionRequest,
onPermissionRequestResult = { showPermissionRequest = null }, onPermissionRequestResult = { showPermissionRequest = null },
) )
val tangemTopSnackbarHostState = LocalTopSnackbarHostState.current
EventEffect( EventEffect(
event = event, event = event,
onTrigger = { value -> onTrigger = { value ->
@ -92,12 +94,14 @@ internal fun WalletEventEffect(
walletsPagerState.scrollToPage(page = value.newIndex) walletsPagerState.scrollToPage(page = value.newIndex)
} }
is WalletEvent.ShowError -> { is WalletEvent.ShowError -> {
snackbarHostState.showSnackbar(message = value.text.resolveReference(resources)) tangemTopSnackbarHostState.showSnackbar(SnackbarMessage(message = value.text))
} }
is WalletEvent.CopyAddress -> { is WalletEvent.CopyAddress -> {
snackbarHostState.showSnackbar( tangemTopSnackbarHostState.showSnackbar(
message = resources.getStringSafe(R.string.wallet_notification_address_copied), SnackbarMessage(
duration = SnackbarDuration.Short, startIconId = R.drawable.ic_check_24,
message = resourceReference(R.string.wallet_notification_address_copied),
),
) )
} }
is WalletEvent.DemonstrateWalletsScrollPreview -> { is WalletEvent.DemonstrateWalletsScrollPreview -> {

View file

@ -18,8 +18,6 @@ import androidx.compose.foundation.pager.HorizontalPager
import androidx.compose.foundation.pager.PagerState import androidx.compose.foundation.pager.PagerState
import androidx.compose.foundation.pager.rememberPagerState import androidx.compose.foundation.pager.rememberPagerState
import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.SnackbarHost
import androidx.compose.material3.SnackbarHostState
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.runtime.* import androidx.compose.runtime.*
import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.saveable.rememberSaveable
@ -47,18 +45,14 @@ import com.tangem.core.ui.components.bottomsheets.state.BottomSheetState
import com.tangem.core.ui.components.haze.hazeSourceTangem import com.tangem.core.ui.components.haze.hazeSourceTangem
import com.tangem.core.ui.components.rememberIsKeyboardVisible import com.tangem.core.ui.components.rememberIsKeyboardVisible
import com.tangem.core.ui.components.sheetscaffold.* 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.collapsing.TangemCollapsingAppBarBehavior import com.tangem.core.ui.ds.topbar.collapsing.TangemCollapsingAppBarBehavior
import com.tangem.core.ui.ds.topbar.collapsing.TangemCollapsingTopBar import com.tangem.core.ui.ds.topbar.collapsing.TangemCollapsingTopBar
import com.tangem.core.ui.ds.topbar.collapsing.rememberTangemExitUntilCollapsedScrollBehavior import com.tangem.core.ui.ds.topbar.collapsing.rememberTangemExitUntilCollapsedScrollBehavior
import com.tangem.core.ui.event.StateEvent
import com.tangem.core.ui.extensions.TextReference import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.res.* import com.tangem.core.ui.res.*
import com.tangem.feature.wallet.presentation.common.preview.WalletScreenPreviewData import com.tangem.feature.wallet.presentation.common.preview.WalletScreenPreviewData
import com.tangem.feature.wallet.presentation.wallet.state.model.NOT_INITIALIZED_WALLET_INDEX import com.tangem.feature.wallet.presentation.wallet.state.model.NOT_INITIALIZED_WALLET_INDEX
import com.tangem.feature.wallet.presentation.wallet.state.model.WalletBalanceUM import com.tangem.feature.wallet.presentation.wallet.state.model.WalletBalanceUM
import com.tangem.feature.wallet.presentation.wallet.state.model.WalletEvent
import com.tangem.feature.wallet.presentation.wallet.state.model.WalletScreenState import com.tangem.feature.wallet.presentation.wallet.state.model.WalletScreenState
import com.tangem.feature.wallet.presentation.wallet.ui.components.MarketsHint import com.tangem.feature.wallet.presentation.wallet.ui.components.MarketsHint
import com.tangem.feature.wallet.presentation.wallet.ui.components.MarketsTooltip import com.tangem.feature.wallet.presentation.wallet.ui.components.MarketsTooltip
@ -85,7 +79,6 @@ internal fun WalletScreen2(
val statusBarHeight = with(LocalDensity.current) { WindowInsets.systemBars.getTop(this).toDp() } val statusBarHeight = with(LocalDensity.current) { WindowInsets.systemBars.getTop(this).toDp() }
val snackbarHostState = remember(::SnackbarHostState)
val walletsPagerState = rememberPagerState( val walletsPagerState = rememberPagerState(
initialPage = state.selectedWalletIndex, initialPage = state.selectedWalletIndex,
pageCount = { state.wallets2.size }, pageCount = { state.wallets2.size },
@ -104,7 +97,6 @@ internal fun WalletScreen2(
WalletContent2( WalletContent2(
state = state, state = state,
walletsPagerState = walletsPagerState, walletsPagerState = walletsPagerState,
snackbarHostState = snackbarHostState,
behavior = behavior, behavior = behavior,
bottomSheetContent = bottomSheetContent, bottomSheetContent = bottomSheetContent,
bottomSheetHeaderHeightProvider = bottomSheetHeaderHeightProvider, bottomSheetHeaderHeightProvider = bottomSheetHeaderHeightProvider,
@ -113,7 +105,6 @@ internal fun WalletScreen2(
WalletEventEffect( WalletEventEffect(
walletsPagerState = walletsPagerState, walletsPagerState = walletsPagerState,
snackbarHostState = snackbarHostState,
event = state.event, event = state.event,
onCollapseBalance = { onCollapseBalance = {
if (behavior.state.collapsedFraction < 1f) { if (behavior.state.collapsedFraction < 1f) {
@ -131,7 +122,6 @@ private fun WalletContent2(
state: WalletScreenState, state: WalletScreenState,
walletsPagerState: PagerState, walletsPagerState: PagerState,
behavior: TangemCollapsingAppBarBehavior, behavior: TangemCollapsingAppBarBehavior,
snackbarHostState: SnackbarHostState,
bottomSheetHeaderHeightProvider: () -> Dp, bottomSheetHeaderHeightProvider: () -> Dp,
onBottomSheetStateChange: (BottomSheetState) -> Unit, onBottomSheetStateChange: (BottomSheetState) -> Unit,
bottomSheetContent: @Composable (() -> Unit), bottomSheetContent: @Composable (() -> Unit),
@ -143,7 +133,6 @@ private fun WalletContent2(
BaseScaffoldWithMarkets( BaseScaffoldWithMarkets(
state = state, state = state,
snackbarHostState = snackbarHostState,
bottomSheetHeaderHeightProvider = bottomSheetHeaderHeightProvider, bottomSheetHeaderHeightProvider = bottomSheetHeaderHeightProvider,
onBottomSheetStateChange = onBottomSheetStateChange, onBottomSheetStateChange = onBottomSheetStateChange,
bottomSheetContent = bottomSheetContent, bottomSheetContent = bottomSheetContent,
@ -278,7 +267,6 @@ private fun WalletContent2(
@Composable @Composable
private inline fun BaseScaffoldWithMarkets( private inline fun BaseScaffoldWithMarkets(
state: WalletScreenState, state: WalletScreenState,
snackbarHostState: SnackbarHostState,
bottomSheetHeaderHeightProvider: () -> Dp, bottomSheetHeaderHeightProvider: () -> Dp,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
noinline onBottomSheetStateChange: (BottomSheetState) -> Unit, noinline onBottomSheetStateChange: (BottomSheetState) -> Unit,
@ -290,10 +278,7 @@ private inline fun BaseScaffoldWithMarkets(
val isKeyboardVisible by rememberIsKeyboardVisible() val isKeyboardVisible by rememberIsKeyboardVisible()
val scaffoldState = rememberTangemBottomSheetScaffoldState( val scaffoldState = rememberTangemBottomSheetScaffoldState(bottomSheetState = bottomSheetState)
bottomSheetState = bottomSheetState,
snackbarHostState = snackbarHostState,
)
val density = LocalDensity.current val density = LocalDensity.current
val bottomBarHeight = with(density) { WindowInsets.systemBars.getBottom(density = this).toDp() } val bottomBarHeight = with(density) { WindowInsets.systemBars.getBottom(density = this).toDp() }
@ -320,15 +305,6 @@ private inline fun BaseScaffoldWithMarkets(
Box(modifier = modifier) { Box(modifier = modifier) {
TangemBottomSheetScaffold( TangemBottomSheetScaffold(
snackbarHost = { snackbarHostState ->
WalletSnackbarHost(
snackbarHostState = snackbarHostState,
event = state.event,
modifier = Modifier
.padding(bottom = TangemTheme.dimens2.x1)
.navigationBarsPadding(),
)
},
containerColor = Color.Unspecified, containerColor = Color.Unspecified,
sheetContainerColor = backgroundColor.value, sheetContainerColor = backgroundColor.value,
scaffoldState = scaffoldState, scaffoldState = scaffoldState,
@ -482,21 +458,6 @@ private fun BottomSheetStateEffects(
} }
} }
@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)
}
}
}
@Composable @Composable
private fun rememberPageAlpha(pagerState: PagerState, currentPageIndex: Int): State<Float> { private fun rememberPageAlpha(pagerState: PagerState, currentPageIndex: Int): State<Float> {
return remember { return remember {