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.ui.DesignFeatureToggles
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.message.EventMessageHandler
import com.tangem.core.ui.theme.AppThemeModeHolder
@ -30,6 +31,7 @@ internal object UiDependenciesModule {
override val vibratorHapticManager = vibratorHapticManager
override val appThemeModeHolder = appThemeModeHolder
override val globalSnackbarHostState: SnackbarHostState = SnackbarHostState()
override val globalTopSnackbarHostState: TangemTopSnackbarHostState = TangemTopSnackbarHostState()
override val eventMessageHandler: EventMessageHandler = EventMessageHandler()
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.navigationBarsPadding
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.statusBarsPadding
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.ui.Alignment
@ -28,9 +29,12 @@ import com.arkivanov.essenty.backhandler.BackHandler
import com.tangem.common.routing.AppRoute
import com.tangem.core.ui.UiDependencies
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.res.LocalRedesignEnabled
import com.tangem.core.ui.res.LocalRootBackgroundColor
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.security.ProvideSecureFlagController
import com.tangem.tap.routing.component.RoutingComponent
@ -93,6 +97,16 @@ internal fun RootContent(
.padding(all = 16.dp),
hostState = snackbarHostState,
)
if (LocalRedesignEnabled.current) {
TangemTopSnackbarHost(
modifier = Modifier
.align(Alignment.TopCenter)
.statusBarsPadding()
.padding(all = 16.dp),
hostState = LocalTopSnackbarHostState.current,
)
}
}
}
EventMessageEffect()

View file

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

View file

@ -2,6 +2,7 @@ package com.tangem.core.ui
import androidx.compose.material3.SnackbarHostState
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.message.EventMessageHandler
import com.tangem.core.ui.theme.AppThemeModeHolder
@ -15,6 +16,8 @@ interface UiDependencies {
val globalSnackbarHostState: SnackbarHostState
val globalTopSnackbarHostState: TangemTopSnackbarHostState
val eventMessageHandler: EventMessageHandler
val designFeatureToggles: DesignFeatureToggles

View file

@ -1,16 +1,27 @@
package com.tangem.core.ui.components.bottomsheets.internal
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.WindowInsets
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.*
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.Shape
import androidx.compose.ui.input.key.*
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.launch
@ -31,6 +42,9 @@ fun ModalBottomSheetWithBackHandling(
contentWindowInsets: @Composable () -> WindowInsets = { BottomSheetDefaults.windowInsets },
content: @Composable ColumnScope.() -> Unit,
) {
val topSnackbarHostState = LocalTopSnackbarHostState.current
val isRedesignEnabled = LocalRedesignEnabled.current
ModalBottomSheet(
onDismissRequest = onDismissRequest,
modifier = modifier,
@ -47,7 +61,27 @@ fun ModalBottomSheetWithBackHandling(
shouldDismissOnBackPress = onBack == null,
),
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) {
onBack?.invoke()

View file

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

View file

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

View file

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

View file

@ -27,6 +27,10 @@ import com.tangem.core.ui.res.TangemThemePreview
*
[REDACTED_AUTHOR]
*/
@Deprecated(
"With redesign, this component is no longer used. " +
"Please use both TangemSnackbarHost and TangemTopSnackbarHost",
)
@Composable
fun TangemSnackbarHost(
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
sealed interface EventMessage : UiMessage
@Immutable
data class ToastMessage(
val message: TextReference,
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 action The action to perform when the action button is clicked. Optional, `null` by default.
* */
@Immutable
data class SnackbarMessage(
val message: TextReference,
@field:DrawableRes val startIconId: Int? = null,
val duration: Duration = Duration.Short,
val onDismissRequest: () -> Unit = {},
val actionLabel: TextReference? = null,
@ -92,6 +95,7 @@ data class SnackbarMessage(
* `true` by default.
* @param onDismissRequest The action to perform when the dialog is dismissed.
* */
@Immutable
data class DialogMessage(
val message: TextReference,
val title: TextReference? = null,
@ -146,6 +150,7 @@ data class DialogMessage(
}
}
@Immutable
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 onDismissRequest The action to perform when the bottom sheet is dismissed.
* */
@Immutable
data class BottomSheetMessage(
@DrawableRes val iconResId: Int? = null,
val title: TextReference? = null,
@ -202,6 +208,7 @@ data class BottomSheetMessage(
}
}
@Immutable
data class BottomSheetMessageV2(
val messageBottomSheetUMV2: MessageBottomSheetUMV2,
) : 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.MessageBottomSheetUM
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.extensions.resolveReference
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.LocalTopSnackbarHostState
import com.tangem.core.ui.res.TangemTheme
@Composable
@ -36,6 +39,10 @@ fun EventMessageEffect(
onShowSnackbar: suspend (SnackbarMessage, Context) -> Unit = { 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) },
) {
val messageEvent by messageHandler.collectAsState()
@ -45,11 +52,16 @@ fun EventMessageEffect(
var bottomSheetMessage: BottomSheetMessage? by remember { mutableStateOf(value = null) }
var bottomSheetMessageV2: BottomSheetMessageV2? by remember { mutableStateOf(value = null) }
var loadingMessage: GlobalLoadingMessage? by remember { mutableStateOf(value = null) }
val isRedesignEnabled = LocalRedesignEnabled.current
EventEffect(event = messageEvent) { message ->
when (message) {
is SnackbarMessage -> {
onShowSnackbar(message, context)
if (isRedesignEnabled) {
onShowTopSnackbar(message)
} else {
onShowSnackbar(message, context)
}
}
is DialogMessage -> {
dialogMessage = message
@ -220,7 +232,8 @@ private fun showToast(message: ToastMessage, context: Context) {
Toast.makeText(
/* context = */ context,
/* text = */ message.message.resolveReference(context.resources),
/* duration = */ when (message.duration) {
/* duration = */
when (message.duration) {
ToastMessage.Duration.Short -> Toast.LENGTH_SHORT
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.powersaving.PowerSavingState
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.rememberBladeAnimation
import com.tangem.core.ui.haptic.DefaultHapticManager
@ -54,6 +55,7 @@ fun TangemTheme(
vibratorHapticManager = uiDependencies.vibratorHapticManager,
snackbarHostState = uiDependencies.globalSnackbarHostState,
eventMessageHandler = uiDependencies.eventMessageHandler,
topSnackbarHostState = uiDependencies.globalTopSnackbarHostState,
overrideSystemBarColors = overrideSystemBarColors,
typography = typography,
dimens = dimens,
@ -76,6 +78,7 @@ fun TangemTheme(
vibratorHapticManager: VibratorHapticManager? = null,
eventMessageHandler: EventMessageHandler = remember { EventMessageHandler() },
snackbarHostState: SnackbarHostState = remember { SnackbarHostState() },
topSnackbarHostState: TangemTopSnackbarHostState = remember { TangemTopSnackbarHostState() },
overrideSystemBarColors: Boolean = true,
content: @Composable () -> Unit,
) {
@ -119,6 +122,7 @@ fun TangemTheme(
LocalIsInDarkTheme provides isDark,
LocalHapticManager provides hapticManager,
LocalSnackbarHostState provides snackbarHostState,
LocalTopSnackbarHostState provides topSnackbarHostState,
LocalEventMessageHandler provides eventMessageHandler,
LocalWindowSize provides windowSize,
LocalBladeAnimation provides rememberBladeAnimation(),
@ -389,6 +393,10 @@ val LocalSnackbarHostState = staticCompositionLocalOf<SnackbarHostState> {
error("No SnackbarHostState provided")
}
val LocalTopSnackbarHostState = staticCompositionLocalOf<TangemTopSnackbarHostState> {
error("No TangemTopSnackbarHostState provided")
}
val LocalEventMessageHandler = staticCompositionLocalOf<EventMessageHandler> {
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.extensions.pluralStringResourceSafe
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.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.TangemThemePreview
import com.tangem.feature.referral.domain.models.ExpectedAward
@ -352,6 +356,8 @@ private fun AdditionalButtons(
val coroutineScope = rememberCoroutineScope()
val resources = LocalContext.current.resources
val isRedesignEnabled = LocalRedesignEnabled.current
val tangemTopSnackbarHostState = LocalTopSnackbarHostState.current
Row(
modifier = Modifier.fillMaxWidth(),
@ -366,10 +372,19 @@ private fun AdditionalButtons(
clipboardManager.setText(AnnotatedString(code))
coroutineScope.launch {
snackbarHostState.showSnackbar(
message = resources.getStringSafe(R.string.referral_promo_code_copied),
duration = SnackbarDuration.Short,
)
if (isRedesignEnabled) {
tangemTopSnackbarHostState.showSnackbar(
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),

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.extensions.stringReference
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.TangemTheme
import com.tangem.core.ui.res.TangemThemePreview
@ -104,10 +107,24 @@ internal fun ReferralScreen(stateHolder: ReferralStateHolder) {
val errorSnackbar = stateHolder.errorSnackbar
val coroutineScope = rememberCoroutineScope()
val resources = LocalContext.current.resources
val isRedesignEnabled = LocalRedesignEnabled.current
val topSnackbarHostState = LocalTopSnackbarHostState.current
SideEffect {
if (errorSnackbar != null) {
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(
message = resources.getMessageForErrorSnackbar(errorSnackbar.throwable),
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.unit.dp
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
internal fun ContainerWithSnackbarHost(snackbarHostState: SnackbarHostState, content: @Composable () -> Unit) {
Box {
content()
CopiedTextSnackbarHost(
hostState = snackbarHostState,
modifier = Modifier
.align(Alignment.BottomCenter)
.padding(bottom = 80.dp),
)
if (LocalRedesignEnabled.current.not()) {
CopiedTextSnackbarHost(
hostState = snackbarHostState,
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.notifications.Notification
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.TangemThemePreview
import com.tangem.features.tokenreceive.entity.ReceiveAddress
@ -203,6 +206,8 @@ private fun AddressBlock(assetsUM: ReceiveAssetsUM, snackbarHostState: SnackbarH
val coroutineScope = rememberCoroutineScope()
val context = LocalContext.current
val resources = context.resources
val isRedesignEnabled = LocalRedesignEnabled.current
val topSnackbarHostState = LocalTopSnackbarHostState.current
assetsUM.addresses
.fastFilter { it.type is ReceiveAddress.Type.Ens }
@ -214,11 +219,20 @@ private fun AddressBlock(assetsUM: ReceiveAssetsUM, snackbarHostState: SnackbarH
assetsUM.onCopyClick(address)
hapticFeedback.performHapticFeedback(HapticFeedbackType.LongPress)
coroutineScope.launch {
snackbarHostState.showSnackbar(
message = resources.getStringSafe(
R.string.wallet_notification_address_copied,
),
)
if (isRedesignEnabled) {
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,
),
)
}
}
},
address = address.value,
@ -413,6 +427,8 @@ private fun ButtonsBlock(snackbarHostState: SnackbarHostState, onCopyClick: () -
val coroutineScope = rememberCoroutineScope()
val context = LocalContext.current
val resources = context.resources
val isRedesignEnabled = LocalRedesignEnabled.current
val topSnackbarHostState = LocalTopSnackbarHostState.current
Row(
modifier = Modifier.width(IntrinsicSize.Min),
@ -427,9 +443,18 @@ private fun ButtonsBlock(snackbarHostState: SnackbarHostState, onCopyClick: () -
onCopyClick()
hapticFeedback.performHapticFeedback(HapticFeedbackType.LongPress)
coroutineScope.launch {
snackbarHostState.showSnackbar(
message = resources.getStringSafe(R.string.wallet_notification_address_copied),
)
if (isRedesignEnabled) {
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.ui.components.*
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.TangemThemePreview
import com.tangem.core.ui.test.TokenReceiveQrCodeBottomSheetTestTags
@ -147,6 +150,9 @@ private fun Buttons(
val context = LocalContext.current
val resources = context.resources
val isRedesignEnabled = LocalRedesignEnabled.current
val tangemTopSnackbarHostState = LocalTopSnackbarHostState.current
Row(
modifier = modifier,
horizontalArrangement = Arrangement.spacedBy(16.dp),
@ -159,9 +165,18 @@ private fun Buttons(
hapticFeedback.performHapticFeedback(HapticFeedbackType.LongPress)
onCopyClick()
coroutineScope.launch {
snackbarHostState.showSnackbar(
message = resources.getStringSafe(R.string.wallet_notification_address_copied),
)
if (isRedesignEnabled) {
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.StateEvent
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.feature.wallet.impl.R
import com.tangem.feature.wallet.presentation.wallet.state.model.WalletEvent
@ -69,18 +72,17 @@ internal fun WalletEventEffectLegacy(
@Composable
internal fun WalletEventEffect(
walletsPagerState: PagerState,
snackbarHostState: SnackbarHostState,
event: StateEvent<WalletEvent>,
onCollapseBalance: () -> Unit,
) {
val resources = LocalContext.current.resources
var showPermissionRequest by remember { mutableStateOf<Pair<() -> Unit, () -> Unit>?>(null) }
HandlePermissionRequest(
permissionRequestParams = showPermissionRequest,
onPermissionRequestResult = { showPermissionRequest = null },
)
val tangemTopSnackbarHostState = LocalTopSnackbarHostState.current
EventEffect(
event = event,
onTrigger = { value ->
@ -92,12 +94,14 @@ internal fun WalletEventEffect(
walletsPagerState.scrollToPage(page = value.newIndex)
}
is WalletEvent.ShowError -> {
snackbarHostState.showSnackbar(message = value.text.resolveReference(resources))
tangemTopSnackbarHostState.showSnackbar(SnackbarMessage(message = value.text))
}
is WalletEvent.CopyAddress -> {
snackbarHostState.showSnackbar(
message = resources.getStringSafe(R.string.wallet_notification_address_copied),
duration = SnackbarDuration.Short,
tangemTopSnackbarHostState.showSnackbar(
SnackbarMessage(
startIconId = R.drawable.ic_check_24,
message = resourceReference(R.string.wallet_notification_address_copied),
),
)
}
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.rememberPagerState
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.SnackbarHost
import androidx.compose.material3.SnackbarHostState
import androidx.compose.material3.Text
import androidx.compose.runtime.*
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.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.collapsing.TangemCollapsingAppBarBehavior
import com.tangem.core.ui.ds.topbar.collapsing.TangemCollapsingTopBar
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.res.*
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.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.ui.components.MarketsHint
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 snackbarHostState = remember(::SnackbarHostState)
val walletsPagerState = rememberPagerState(
initialPage = state.selectedWalletIndex,
pageCount = { state.wallets2.size },
@ -104,7 +97,6 @@ internal fun WalletScreen2(
WalletContent2(
state = state,
walletsPagerState = walletsPagerState,
snackbarHostState = snackbarHostState,
behavior = behavior,
bottomSheetContent = bottomSheetContent,
bottomSheetHeaderHeightProvider = bottomSheetHeaderHeightProvider,
@ -113,7 +105,6 @@ internal fun WalletScreen2(
WalletEventEffect(
walletsPagerState = walletsPagerState,
snackbarHostState = snackbarHostState,
event = state.event,
onCollapseBalance = {
if (behavior.state.collapsedFraction < 1f) {
@ -131,7 +122,6 @@ private fun WalletContent2(
state: WalletScreenState,
walletsPagerState: PagerState,
behavior: TangemCollapsingAppBarBehavior,
snackbarHostState: SnackbarHostState,
bottomSheetHeaderHeightProvider: () -> Dp,
onBottomSheetStateChange: (BottomSheetState) -> Unit,
bottomSheetContent: @Composable (() -> Unit),
@ -143,7 +133,6 @@ private fun WalletContent2(
BaseScaffoldWithMarkets(
state = state,
snackbarHostState = snackbarHostState,
bottomSheetHeaderHeightProvider = bottomSheetHeaderHeightProvider,
onBottomSheetStateChange = onBottomSheetStateChange,
bottomSheetContent = bottomSheetContent,
@ -278,7 +267,6 @@ private fun WalletContent2(
@Composable
private inline fun BaseScaffoldWithMarkets(
state: WalletScreenState,
snackbarHostState: SnackbarHostState,
bottomSheetHeaderHeightProvider: () -> Dp,
modifier: Modifier = Modifier,
noinline onBottomSheetStateChange: (BottomSheetState) -> Unit,
@ -290,10 +278,7 @@ private inline fun BaseScaffoldWithMarkets(
val isKeyboardVisible by rememberIsKeyboardVisible()
val scaffoldState = rememberTangemBottomSheetScaffoldState(
bottomSheetState = bottomSheetState,
snackbarHostState = snackbarHostState,
)
val scaffoldState = rememberTangemBottomSheetScaffoldState(bottomSheetState = bottomSheetState)
val density = LocalDensity.current
val bottomBarHeight = with(density) { WindowInsets.systemBars.getBottom(density = this).toDp() }
@ -320,15 +305,6 @@ private inline fun BaseScaffoldWithMarkets(
Box(modifier = modifier) {
TangemBottomSheetScaffold(
snackbarHost = { snackbarHostState ->
WalletSnackbarHost(
snackbarHostState = snackbarHostState,
event = state.event,
modifier = Modifier
.padding(bottom = TangemTheme.dimens2.x1)
.navigationBarsPadding(),
)
},
containerColor = Color.Unspecified,
sheetContainerColor = backgroundColor.value,
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
private fun rememberPageAlpha(pagerState: PagerState, currentPageIndex: Int): State<Float> {
return remember {