Updated on 2026-08-14

This commit is contained in:
Tangem 2026-07-06 13:39:30 +03:00
commit 92849d43c2
1407 changed files with 64092 additions and 11990 deletions

View file

@ -1,11 +1,13 @@
package com.tangem.feature.tester.presentation.actions
import androidx.compose.runtime.Immutable
import kotlinx.collections.immutable.ImmutableList
import java.io.File
internal data class TesterActionsContentState(
val hideAllCurrenciesUM: HideAllCurrenciesUM,
val toggleHotWalletRestrictionUM: ToggleHotWalletRestrictionUM,
val usedeskTokenTtlUM: UsedeskTokenTtlUM,
val shareLogsUM: ShareLogsUM,
val onBackClick: () -> Unit,
) {
@ -21,6 +23,24 @@ internal data class TesterActionsContentState(
val onClick: () -> Unit,
)
/**
* State of the Usedesk chat token TTL setting.
*
* @param currentLabel human-readable current TTL (e.g. "7 days", "15 minutes")
* @param presets selectable preset durations
* @param onPresetSelected applies one of the [presets] (value in milliseconds)
* @param onCustomMinutesSelected applies a custom TTL entered in minutes
*/
@Immutable
data class UsedeskTokenTtlUM(
val currentLabel: String,
val presets: ImmutableList<Preset>,
val onPresetSelected: (Long) -> Unit,
val onCustomMinutesSelected: (Long) -> Unit,
) {
data class Preset(val label: String, val millis: Long)
}
@Immutable
data class ShareLogsUM(val file: File?)
}

View file

@ -8,15 +8,23 @@ import androidx.compose.foundation.background
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.text.input.TextFieldValue
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.core.app.ShareCompat
import androidx.core.content.ContextCompat
import androidx.core.content.FileProvider
import com.tangem.core.ui.components.AdditionalTextInputDialogUM
import com.tangem.core.ui.components.DialogButtonUM
import com.tangem.core.ui.components.PrimaryButton
import com.tangem.core.ui.components.SelectorDialog
import com.tangem.core.ui.components.TextInputDialog
import com.tangem.core.ui.components.appbar.AppBarWithBackButton
import com.tangem.core.ui.extensions.stringResourceSafe
import com.tangem.core.ui.res.TangemTheme
@ -25,12 +33,19 @@ import com.tangem.core.ui.utils.findActivity
import com.tangem.feature.tester.impl.R
import com.tangem.feature.tester.presentation.actions.TesterActionsContentState.HideAllCurrenciesUM
import com.tangem.feature.tester.presentation.actions.TesterActionsContentState.ToggleHotWalletRestrictionUM
import com.tangem.feature.tester.presentation.actions.TesterActionsContentState.UsedeskTokenTtlUM
import com.tangem.utils.logging.TangemLogger
import kotlinx.collections.immutable.toImmutableList
import java.io.File
import kotlin.time.Duration.Companion.days
import kotlin.time.Duration.Companion.minutes
@OptIn(ExperimentalFoundationApi::class)
@Composable
internal fun TesterActionsScreen(state: TesterActionsContentState, modifier: Modifier = Modifier) {
var shouldShowTtlSelector by remember { mutableStateOf(value = false) }
var shouldShowTtlCustomInput by remember { mutableStateOf(value = false) }
LazyColumn(
modifier = modifier
.fillMaxSize()
@ -65,6 +80,13 @@ internal fun TesterActionsScreen(state: TesterActionsContentState, modifier: Mod
)
}
item {
TesterActionItem(
name = stringResourceSafe(id = R.string.usedesk_token_ttl, state.usedeskTokenTtlUM.currentLabel),
onClick = { shouldShowTtlSelector = true },
)
}
item {
val activity = LocalContext.current.findActivity()
@ -75,6 +97,24 @@ internal fun TesterActionsScreen(state: TesterActionsContentState, modifier: Mod
)
}
}
if (shouldShowTtlSelector) {
UsedeskTtlSelectorDialog(
um = state.usedeskTokenTtlUM,
onCustomRequest = {
shouldShowTtlSelector = false
shouldShowTtlCustomInput = true
},
onDismiss = { shouldShowTtlSelector = false },
)
}
if (shouldShowTtlCustomInput) {
UsedeskTtlCustomInputDialog(
um = state.usedeskTokenTtlUM,
onDismiss = { shouldShowTtlCustomInput = false },
)
}
}
@Composable
@ -90,6 +130,62 @@ private fun TesterActionItem(name: String, onClick: () -> Unit, progress: Boolea
)
}
@Composable
private fun UsedeskTtlSelectorDialog(um: UsedeskTokenTtlUM, onCustomRequest: () -> Unit, onDismiss: () -> Unit) {
val customLabel = stringResourceSafe(R.string.usedesk_token_ttl_custom)
val items = remember(um.presets, customLabel) {
(um.presets.map(UsedeskTokenTtlUM.Preset::label) + customLabel).toImmutableList()
}
val selectedIndex = remember(um.presets, um.currentLabel) {
um.presets.indexOfFirst { it.label == um.currentLabel }
.takeIf { it >= 0 }
?: um.presets.size
}
SelectorDialog(
title = stringResourceSafe(R.string.usedesk_token_ttl_title),
items = items,
selectedItemIndex = selectedIndex,
confirmButton = DialogButtonUM(title = stringResourceSafe(id = R.string.common_close), onClick = onDismiss),
onSelect = { index ->
val preset = um.presets.getOrNull(index)
if (preset != null) {
um.onPresetSelected(preset.millis)
onDismiss()
} else {
onCustomRequest()
}
},
onDismissDialog = onDismiss,
)
}
@Composable
private fun UsedeskTtlCustomInputDialog(um: UsedeskTokenTtlUM, onDismiss: () -> Unit) {
var value by remember { mutableStateOf(TextFieldValue()) }
val minutes = value.text.toLongOrNull()
val isValid = minutes != null && minutes > 0
TextInputDialog(
title = stringResourceSafe(R.string.usedesk_token_ttl_title),
fieldValue = value,
onValueChange = { newValue -> value = newValue.copy(text = newValue.text.filter(Char::isDigit)) },
textFieldParams = AdditionalTextInputDialogUM(
label = stringResourceSafe(R.string.usedesk_token_ttl_custom),
placeholder = "15",
),
confirmButton = DialogButtonUM(
isEnabled = isValid,
onClick = {
if (isValid) um.onCustomMinutesSelected(minutes)
onDismiss()
},
),
dismissButton = DialogButtonUM(title = stringResourceSafe(id = R.string.common_close), onClick = onDismiss),
onDismissDialog = onDismiss,
)
}
private fun Activity.shareFile(file: File?) {
val originalIntent = createEmailShareIntent(activity = this, file = file)
@ -126,6 +222,15 @@ private fun TesterActionsScreenSample(modifier: Modifier = Modifier) {
state = TesterActionsContentState(
hideAllCurrenciesUM = HideAllCurrenciesUM.Clickable {},
toggleHotWalletRestrictionUM = ToggleHotWalletRestrictionUM(isEnabled = true) {},
usedeskTokenTtlUM = UsedeskTokenTtlUM(
currentLabel = "7 days",
presets = listOf(
UsedeskTokenTtlUM.Preset(label = "7 days", millis = 7.days.inWholeMilliseconds),
UsedeskTokenTtlUM.Preset(label = "15 minutes", millis = 15.minutes.inWholeMilliseconds),
).toImmutableList(),
onPresetSelected = {},
onCustomMinutesSelected = {},
),
shareLogsUM = TesterActionsContentState.ShareLogsUM(file = null),
onBackClick = {},
),

View file

@ -9,14 +9,21 @@ import com.tangem.data.common.account.WalletAccountsSaver
import com.tangem.domain.common.wallets.UserWalletsListRepository
import com.tangem.domain.feedback.repository.FeedbackRepository
import com.tangem.domain.settings.HotWalletRestrictionManager
import com.tangem.domain.settings.UsedeskTokenTtlManager
import com.tangem.feature.tester.presentation.actions.TesterActionsContentState.HideAllCurrenciesUM
import com.tangem.feature.tester.presentation.actions.TesterActionsContentState.ToggleHotWalletRestrictionUM
import com.tangem.feature.tester.presentation.actions.TesterActionsContentState.UsedeskTokenTtlUM
import com.tangem.feature.tester.presentation.navigation.InnerTesterRouter
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.collections.immutable.persistentListOf
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.launch
import javax.inject.Inject
import kotlin.time.Duration.Companion.days
import kotlin.time.Duration.Companion.hours
import kotlin.time.Duration.Companion.milliseconds
import kotlin.time.Duration.Companion.minutes
@HiltViewModel
internal class TesterActionsViewModel @Inject constructor(
@ -24,6 +31,7 @@ internal class TesterActionsViewModel @Inject constructor(
private val userWalletsListRepository: UserWalletsListRepository,
private val walletAccountsSaver: WalletAccountsSaver,
private val hotWalletRestrictionManager: HotWalletRestrictionManager,
private val usedeskTokenTtlManager: UsedeskTokenTtlManager,
) : ViewModel() {
var uiState: TesterActionsContentState by mutableStateOf(initialState)
@ -36,12 +44,19 @@ internal class TesterActionsViewModel @Inject constructor(
isEnabled = hotWalletRestrictionManager.isCreationEnabledSync(),
onClick = this::toggleHotWalletRestriction,
),
usedeskTokenTtlUM = UsedeskTokenTtlUM(
currentLabel = formatTtl(usedeskTokenTtlManager.getTokenTtlMillisSync()),
presets = TTL_PRESETS,
onPresetSelected = this::setUsedeskTokenTtl,
onCustomMinutesSelected = { minutes -> setUsedeskTokenTtl(minutes.minutes.inWholeMilliseconds) },
),
shareLogsUM = TesterActionsContentState.ShareLogsUM(file = feedbackRepository.getLogFile()),
onBackClick = { /* no-op */ },
)
init {
bootstrapHotWalletRestrictionUpdates()
bootstrapUsedeskTokenTtlUpdates()
}
fun setupNavigation(router: InnerTesterRouter) {
@ -76,6 +91,10 @@ internal class TesterActionsViewModel @Inject constructor(
hotWalletRestrictionManager.toggleCreationEnabled()
}
private fun setUsedeskTokenTtl(millis: Long) = viewModelScope.launch {
usedeskTokenTtlManager.setTokenTtlMillis(millis)
}
private fun bootstrapHotWalletRestrictionUpdates() {
hotWalletRestrictionManager.isCreationEnabled()
.onEach { isEnabled ->
@ -87,4 +106,52 @@ internal class TesterActionsViewModel @Inject constructor(
}
.launchIn(viewModelScope)
}
private fun bootstrapUsedeskTokenTtlUpdates() {
usedeskTokenTtlManager.getTokenTtlMillis()
.onEach { millis ->
uiState = uiState.copy(
usedeskTokenTtlUM = uiState.usedeskTokenTtlUM.copy(
currentLabel = formatTtl(millis),
),
)
}
.launchIn(viewModelScope)
}
private companion object {
val TTL_PRESETS = persistentListOf(
UsedeskTokenTtlUM.Preset(
label = formatTtl(7.days.inWholeMilliseconds),
millis = 7.days.inWholeMilliseconds,
),
UsedeskTokenTtlUM.Preset(
label = formatTtl(1.days.inWholeMilliseconds),
millis = 1.days.inWholeMilliseconds,
),
UsedeskTokenTtlUM.Preset(
label = formatTtl(1.hours.inWholeMilliseconds),
millis = 1.hours.inWholeMilliseconds,
),
UsedeskTokenTtlUM.Preset(
label = formatTtl(15.minutes.inWholeMilliseconds),
millis = 15.minutes.inWholeMilliseconds,
),
)
/** Picks the largest whole unit (days → hours → minutes → seconds) for a readable label. */
fun formatTtl(millis: Long): String {
val duration = millis.milliseconds
return when {
duration.inWholeDays >= 1 && duration == duration.inWholeDays.days -> duration.inWholeDays.unit("day")
duration.inWholeHours >= 1 && duration == duration.inWholeHours.hours ->
duration.inWholeHours.unit("hour")
duration.inWholeMinutes >= 1 -> duration.inWholeMinutes.unit("minute")
else -> duration.inWholeSeconds.unit("second")
}
}
fun Long.unit(name: String): String = "$this $name${if (this == 1L) "" else "s"}"
}
}

View file

@ -9,7 +9,9 @@ import com.tangem.core.ui.ds.topbar.TangemTopBarType
import com.tangem.core.ui.ds2.badge.TangemBadge
import com.tangem.core.ui.ds2.button.TangemButton
import com.tangem.core.ui.ds2.fade.TangemFade
import com.tangem.core.ui.ds2.glowring.TangemGlowRing
import com.tangem.core.ui.ds2.loader.TangemLoaderSize
import com.tangem.core.ui.ds2.messagebanner.TangemMessageBanner
import com.tangem.core.ui.ds2.row.TangemRowContentLead
import com.tangem.core.ui.ds2.row.TangemRowVerticalAlignment
import com.tangem.core.ui.ds2.topnavigation.TangemTopNavigation
@ -351,6 +353,59 @@ internal data class TangemCheckmarkStory(
val onEnabledToggle: () -> Unit,
) : DsStoryBookPage
internal data class TangemGlowRingStory(
val variant: TangemGlowRing.Variant,
val quality: TangemGlowRing.Quality,
val background: Background,
val isAnimated: Boolean,
val onVariantChange: (TangemGlowRing.Variant) -> Unit,
val onQualityChange: (TangemGlowRing.Quality) -> Unit,
val onBackgroundChange: (Background) -> Unit,
val onAnimatedToggle: () -> Unit,
) : DsStoryBookPage {
/** Backdrop the glow-ring preview is rendered on top of. */
enum class Background(val label: String) {
BgPrimary("bg.primary"),
BgSecondary("bg.secondary"),
BgInverse("bg.inverse"),
}
}
@Suppress("BooleanPropertyNaming")
internal data class TangemMessageBannerStory(
val variant: TangemMessageBanner.Variant,
val contentAlign: TangemMessageBanner.ContentAlign,
val hasGlowRing: Boolean,
val hasDescription: Boolean,
val hasSecondaryButton: Boolean,
val hasPrimaryButton: Boolean,
val hasCloseButton: Boolean,
val hasSlotStart: Boolean,
val hasSlotEnd: Boolean,
val hasExtraContent: Boolean,
val background: Background,
val onVariantChange: (TangemMessageBanner.Variant) -> Unit,
val onContentAlignChange: (TangemMessageBanner.ContentAlign) -> Unit,
val onGlowRingToggle: () -> Unit,
val onDescriptionToggle: () -> Unit,
val onSecondaryButtonToggle: () -> Unit,
val onPrimaryButtonToggle: () -> Unit,
val onCloseButtonToggle: () -> Unit,
val onSlotStartToggle: () -> Unit,
val onSlotEndToggle: () -> Unit,
val onExtraContentToggle: () -> Unit,
val onBackgroundChange: (Background) -> Unit,
) : DsStoryBookPage {
/** Backdrop the banner preview is rendered on top of. */
enum class Background(val label: String) {
BgPrimary("bg.primary"),
BgSecondary("bg.secondary"),
BgInverse("bg.inverse"),
}
}
internal data class TangemBadgeV2Story(
val variant: TangemBadge.Variant,
val status: TangemBadge.Status,

View file

@ -20,7 +20,9 @@ import com.tangem.feature.tester.presentation.storybook.page.ds.button.tangemBut
import com.tangem.feature.tester.presentation.storybook.page.ds.checkbox.tangemCheckboxV2StoryFactory
import com.tangem.feature.tester.presentation.storybook.page.ds.checkmark.tangemCheckmarkStoryFactory
import com.tangem.feature.tester.presentation.storybook.page.ds.fade.tangemFadeStoryFactory
import com.tangem.feature.tester.presentation.storybook.page.ds.glowring.tangemGlowRingStoryFactory
import com.tangem.feature.tester.presentation.storybook.page.ds.loader.tangemLoaderStoryFactory
import com.tangem.feature.tester.presentation.storybook.page.ds.messagebanner.tangemMessageBannerStoryFactory
import com.tangem.feature.tester.presentation.storybook.page.ds.row.tangemRowStoryFactory
import com.tangem.feature.tester.presentation.storybook.page.ds.search.tangemSearchStoryFactory
import com.tangem.feature.tester.presentation.storybook.page.ds.shimmer.tangemShimmerStoryFactory
@ -39,6 +41,8 @@ private fun buildDsStories() = listOf(
DsStoryItem(title = "✨ TangemShimmer", factory = tangemShimmerStoryFactory),
DsStoryItem(title = "🌫️ TangemFade", factory = tangemFadeStoryFactory),
DsStoryItem(title = "🧭 TangemTopNavigation", factory = tangemTopNavigationStoryFactory),
DsStoryItem(title = "💫 TangemGlowRing", factory = tangemGlowRingStoryFactory),
DsStoryItem(title = "📢 TangemMessageBanner", factory = tangemMessageBannerStoryFactory),
)
@Composable

View file

@ -0,0 +1,30 @@
package com.tangem.feature.tester.presentation.storybook.page.ds.glowring
import com.tangem.core.ui.ds2.glowring.TangemGlowRing
import com.tangem.feature.tester.presentation.storybook.entity.TangemGlowRingStory
import com.tangem.feature.tester.presentation.storybook.viewmodel.StateUpdater
import com.tangem.feature.tester.presentation.storybook.viewmodel.storyPageFactory
internal fun StateUpdater<TangemGlowRingStory>.build(): TangemGlowRingStory {
return TangemGlowRingStory(
variant = TangemGlowRing.Variant.Magic,
quality = TangemGlowRing.Quality.Auto,
background = TangemGlowRingStory.Background.BgPrimary,
isAnimated = true,
onVariantChange = { variant ->
updateStory { it.copy(variant = variant) }
},
onQualityChange = { quality ->
updateStory { it.copy(quality = quality) }
},
onBackgroundChange = { background ->
updateStory { it.copy(background = background) }
},
onAnimatedToggle = {
updateStory { it.copy(isAnimated = !it.isAnimated) }
},
)
}
internal val tangemGlowRingStoryFactory
get() = storyPageFactory(StateUpdater<TangemGlowRingStory>::build)

View file

@ -0,0 +1,227 @@
@file:Suppress("MagicNumber")
package com.tangem.feature.tester.presentation.storybook.page.ds.glowring
import androidx.compose.foundation.background
import androidx.compose.foundation.border
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.unit.dp
import com.tangem.core.ui.ds2.glowring.TangemGlowRing
import com.tangem.core.ui.res.TangemTheme
import com.tangem.feature.tester.presentation.storybook.entity.TangemGlowRingStory
import com.tangem.feature.tester.presentation.storybook.entity.TangemGlowRingStory.Background
@Composable
internal fun TangemGlowRingStory(state: TangemGlowRingStory, modifier: Modifier = Modifier) {
Column(
modifier = modifier
.fillMaxSize()
.background(TangemTheme.colors.background.primary)
.padding(vertical = 16.dp),
verticalArrangement = Arrangement.spacedBy(16.dp),
) {
// Preview stays pinned at the top.
ComponentPreview(state = state)
// Only the controls scroll.
Column(
modifier = Modifier.verticalScroll(rememberScrollState()),
verticalArrangement = Arrangement.spacedBy(16.dp),
) {
VariantSelector(selected = state.variant, onSelect = state.onVariantChange)
QualitySelector(selected = state.quality, onSelect = state.onQualityChange)
BackgroundSelector(selected = state.background, onSelect = state.onBackgroundChange)
Toggles(state = state)
}
}
}
@Composable
private fun PreviewBackground(background: Background, modifier: Modifier = Modifier) {
when (background) {
Background.BgPrimary -> Box(modifier.background(TangemTheme.colors3.bg.primary))
Background.BgSecondary -> Box(modifier.background(TangemTheme.colors3.bg.secondary))
Background.BgInverse -> Box(modifier.background(TangemTheme.colors3.bg.inverse))
}
}
@Composable
private fun ComponentPreview(state: TangemGlowRingStory) {
Box(
contentAlignment = Alignment.Center,
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp)
.clip(RoundedCornerShape(16.dp)),
) {
PreviewBackground(
background = state.background,
modifier = Modifier.matchParentSize(),
)
Box(
contentAlignment = Alignment.Center,
modifier = Modifier
.fillMaxWidth()
.padding(vertical = 48.dp),
) {
TangemGlowRing(
modifier = Modifier.size(width = 200.dp, height = 120.dp),
variant = state.variant,
animated = state.isAnimated,
quality = state.quality,
)
}
}
}
@Composable
private fun VariantSelector(selected: TangemGlowRing.Variant, onSelect: (TangemGlowRing.Variant) -> Unit) {
Section(label = "Variant") {
ChipGrid(
items = TangemGlowRing.Variant.entries,
label = { it.name },
isSelected = { it == selected },
onSelect = onSelect,
)
}
}
@Composable
private fun QualitySelector(selected: TangemGlowRing.Quality, onSelect: (TangemGlowRing.Quality) -> Unit) {
Section(label = "Quality (renderer)") {
ChipGrid(
items = TangemGlowRing.Quality.entries,
label = { it.name },
isSelected = { it == selected },
onSelect = onSelect,
)
}
}
@Composable
private fun BackgroundSelector(selected: Background, onSelect: (Background) -> Unit) {
Section(label = "Background") {
ChipGrid(
items = Background.entries,
label = { it.label },
isSelected = { it == selected },
onSelect = onSelect,
)
}
}
@Composable
private fun Toggles(state: TangemGlowRingStory) {
Section(label = "Flags") {
Column(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp),
verticalArrangement = Arrangement.spacedBy(8.dp),
) {
ToggleRow(label = "animated", checked = state.isAnimated, onToggle = state.onAnimatedToggle)
}
}
}
@Composable
private fun Section(label: String, content: @Composable () -> Unit) {
Column(verticalArrangement = Arrangement.spacedBy(8.dp)) {
Text(
modifier = Modifier.padding(horizontal = 16.dp),
text = label,
style = TangemTheme.typography.subtitle1,
color = TangemTheme.colors.text.primary1,
)
content()
}
}
@Composable
private fun <T> ChipGrid(items: List<T>, label: (T) -> String, isSelected: (T) -> Boolean, onSelect: (T) -> Unit) {
val shape = RoundedCornerShape(50)
Row(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp)
.clip(shape)
.background(TangemTheme.colors2.surface.level2)
.border(
width = 1.dp,
color = TangemTheme.colors2.border.neutral.secondary,
shape = shape,
)
.padding(4.dp),
horizontalArrangement = Arrangement.spacedBy(4.dp),
) {
items.forEach { item ->
Chip(
label = label(item),
selected = isSelected(item),
onClick = { onSelect(item) },
modifier = Modifier.weight(1f),
)
}
}
}
@Composable
private fun Chip(label: String, selected: Boolean, onClick: () -> Unit, modifier: Modifier = Modifier) {
val chipShape = RoundedCornerShape(50)
Box(
contentAlignment = Alignment.Center,
modifier = modifier
.clip(chipShape)
.background(
if (selected) TangemTheme.colors2.surface.level3 else TangemTheme.colors2.surface.level2,
)
.clickable(onClick = onClick)
.padding(vertical = 8.dp, horizontal = 4.dp),
) {
Text(
text = label,
style = TangemTheme.typography.caption2,
color = if (selected) TangemTheme.colors.text.primary1 else TangemTheme.colors.text.secondary,
)
}
}
@Composable
private fun ToggleRow(label: String, checked: Boolean, onToggle: () -> Unit) {
Row(
modifier = Modifier
.fillMaxWidth()
.clip(RoundedCornerShape(12.dp))
.background(TangemTheme.colors2.surface.level2)
.clickable(onClick = onToggle)
.padding(horizontal = 12.dp, vertical = 12.dp),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.SpaceBetween,
) {
Text(
text = label,
style = TangemTheme.typography.body2,
color = TangemTheme.colors.text.primary1,
)
Text(
text = if (checked) "ON" else "OFF",
style = TangemTheme.typography.caption2,
color = if (checked) TangemTheme.colors.text.accent else TangemTheme.colors.text.secondary,
)
}
}

View file

@ -0,0 +1,37 @@
package com.tangem.feature.tester.presentation.storybook.page.ds.messagebanner
import com.tangem.core.ui.ds2.messagebanner.TangemMessageBanner
import com.tangem.feature.tester.presentation.storybook.entity.TangemMessageBannerStory
import com.tangem.feature.tester.presentation.storybook.entity.TangemMessageBannerStory.Background
import com.tangem.feature.tester.presentation.storybook.viewmodel.StateUpdater
import com.tangem.feature.tester.presentation.storybook.viewmodel.storyPageFactory
internal fun StateUpdater<TangemMessageBannerStory>.build(): TangemMessageBannerStory {
return TangemMessageBannerStory(
variant = TangemMessageBanner.Variant.Default,
contentAlign = TangemMessageBanner.ContentAlign.Start,
hasGlowRing = true,
hasDescription = true,
hasSecondaryButton = true,
hasPrimaryButton = true,
hasCloseButton = true,
hasSlotStart = true,
hasSlotEnd = true,
hasExtraContent = true,
background = Background.BgSecondary,
onVariantChange = { variant -> updateStory { it.copy(variant = variant) } },
onContentAlignChange = { align -> updateStory { it.copy(contentAlign = align) } },
onGlowRingToggle = { updateStory { it.copy(hasGlowRing = !it.hasGlowRing) } },
onDescriptionToggle = { updateStory { it.copy(hasDescription = !it.hasDescription) } },
onSecondaryButtonToggle = { updateStory { it.copy(hasSecondaryButton = !it.hasSecondaryButton) } },
onPrimaryButtonToggle = { updateStory { it.copy(hasPrimaryButton = !it.hasPrimaryButton) } },
onCloseButtonToggle = { updateStory { it.copy(hasCloseButton = !it.hasCloseButton) } },
onSlotStartToggle = { updateStory { it.copy(hasSlotStart = !it.hasSlotStart) } },
onSlotEndToggle = { updateStory { it.copy(hasSlotEnd = !it.hasSlotEnd) } },
onExtraContentToggle = { updateStory { it.copy(hasExtraContent = !it.hasExtraContent) } },
onBackgroundChange = { background -> updateStory { it.copy(background = background) } },
)
}
internal val tangemMessageBannerStoryFactory
get() = storyPageFactory(StateUpdater<TangemMessageBannerStory>::build)

View file

@ -0,0 +1,325 @@
@file:Suppress("MagicNumber")
package com.tangem.feature.tester.presentation.storybook.page.ds.messagebanner
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.border
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.Icon
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import com.tangem.core.ui.R
import com.tangem.core.ui.ds2.messagebanner.CloseButton
import com.tangem.core.ui.ds2.messagebanner.TangemMessageBanner
import com.tangem.core.ui.extensions.stringReference
import com.tangem.core.ui.res.TangemTheme
import com.tangem.feature.tester.presentation.storybook.entity.TangemMessageBannerStory
import com.tangem.feature.tester.presentation.storybook.entity.TangemMessageBannerStory.Background
@Composable
internal fun TangemMessageBannerStory(state: TangemMessageBannerStory, modifier: Modifier = Modifier) {
Column(
modifier = modifier
.fillMaxSize()
.background(TangemTheme.colors.background.primary)
.padding(vertical = 16.dp),
verticalArrangement = Arrangement.spacedBy(16.dp),
) {
// Preview stays pinned at the top.
ComponentPreview(state = state)
// Only the controls scroll.
Column(
modifier = Modifier.verticalScroll(rememberScrollState()),
verticalArrangement = Arrangement.spacedBy(16.dp),
) {
VariantSelector(selected = state.variant, onSelect = state.onVariantChange)
ContentAlignSelector(selected = state.contentAlign, onSelect = state.onContentAlignChange)
BackgroundSelector(selected = state.background, onSelect = state.onBackgroundChange)
Toggles(state = state)
}
}
}
@Composable
private fun PreviewBackground(background: Background, modifier: Modifier = Modifier) {
when (background) {
Background.BgPrimary -> Box(modifier.background(TangemTheme.colors3.bg.primary))
Background.BgSecondary -> Box(modifier.background(TangemTheme.colors3.bg.secondary))
Background.BgInverse -> Box(modifier.background(TangemTheme.colors3.bg.inverse))
}
}
@Composable
private fun ComponentPreview(state: TangemMessageBannerStory) {
Box(
contentAlignment = Alignment.Center,
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp)
.clip(RoundedCornerShape(16.dp)),
) {
PreviewBackground(background = state.background, modifier = Modifier.matchParentSize())
Box(
modifier = Modifier
.fillMaxWidth()
.padding(16.dp),
) {
PreviewBanner(state = state)
}
}
}
@Composable
private fun PreviewBanner(state: TangemMessageBannerStory) {
TangemMessageBanner(
modifier = Modifier.fillMaxWidth(),
variant = state.variant,
contentAlign = state.contentAlign,
showGlowRing = state.hasGlowRing,
title = stringReference("Would you predict?"),
description = if (state.hasDescription) {
stringReference("France will win FIFA 2026")
} else {
null
},
secondaryButton = if (state.hasSecondaryButton) {
TangemMessageBanner.Button(text = stringReference("Yes"), onClick = {})
} else {
null
},
primaryButton = if (state.hasPrimaryButton) {
TangemMessageBanner.Button(text = stringReference("Oh, yes"), onClick = {})
} else {
null
},
slotStart = if (state.hasSlotStart) {
{ BannerLeadingIcon() }
} else {
null
},
slotEnd = when {
state.hasCloseButton -> {
{ TangemMessageBanner.CloseButton(onClick = {}, contentDescription = "Dismiss") }
}
state.hasSlotEnd -> {
{ CirclePlaceholder(size = 24.dp) }
}
else -> null
},
extraBottomSlot = if (state.hasExtraContent) {
{ ProtectedByRow() }
} else {
null
},
)
}
@Composable
private fun BannerLeadingIcon() {
Image(
painter = painterResource(R.drawable.img_solana_22),
contentDescription = null,
modifier = Modifier
.size(40.dp)
.clip(RoundedCornerShape(percent = 50)),
)
}
@Composable
private fun ProtectedByRow() {
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(6.dp),
) {
Text(
text = "Protected by Tangem Security",
style = TangemTheme.typography3.body.medium,
color = TangemTheme.colors3.text.primary,
)
Icon(
painter = painterResource(R.drawable.ic_shield_check_16),
contentDescription = null,
tint = TangemTheme.colors3.icon.status.info,
)
}
}
@Composable
private fun CirclePlaceholder(size: Dp) {
Box(
modifier = Modifier
.size(size)
.clip(RoundedCornerShape(percent = 50))
.background(TangemTheme.colors3.bg.tertiary),
)
}
@Composable
private fun BackgroundSelector(selected: Background, onSelect: (Background) -> Unit) {
Section(label = "Background") {
ChipGrid(
items = Background.entries,
label = { it.label },
isSelected = { it == selected },
onSelect = onSelect,
)
}
}
@Composable
private fun VariantSelector(selected: TangemMessageBanner.Variant, onSelect: (TangemMessageBanner.Variant) -> Unit) {
Section(label = "Variant") {
ChipGrid(
items = TangemMessageBanner.Variant.entries,
label = { it.name },
isSelected = { it == selected },
onSelect = onSelect,
)
}
}
@Composable
private fun ContentAlignSelector(
selected: TangemMessageBanner.ContentAlign,
onSelect: (TangemMessageBanner.ContentAlign) -> Unit,
) {
Section(label = "Content align") {
ChipGrid(
items = TangemMessageBanner.ContentAlign.entries,
label = { it.name },
isSelected = { it == selected },
onSelect = onSelect,
)
}
}
@Composable
private fun Toggles(state: TangemMessageBannerStory) {
Section(label = "Flags") {
Column(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp),
verticalArrangement = Arrangement.spacedBy(8.dp),
) {
ToggleRow(label = "glowRing", checked = state.hasGlowRing, onToggle = state.onGlowRingToggle)
ToggleRow(label = "description", checked = state.hasDescription, onToggle = state.onDescriptionToggle)
ToggleRow(
label = "secondaryButton",
checked = state.hasSecondaryButton,
onToggle = state.onSecondaryButtonToggle,
)
ToggleRow(label = "primaryButton", checked = state.hasPrimaryButton, onToggle = state.onPrimaryButtonToggle)
ToggleRow(label = "closeButton", checked = state.hasCloseButton, onToggle = state.onCloseButtonToggle)
ToggleRow(label = "slotStart", checked = state.hasSlotStart, onToggle = state.onSlotStartToggle)
ToggleRow(label = "slotEnd", checked = state.hasSlotEnd, onToggle = state.onSlotEndToggle)
ToggleRow(label = "extraContent", checked = state.hasExtraContent, onToggle = state.onExtraContentToggle)
}
}
}
@Composable
private fun Section(label: String, content: @Composable () -> Unit) {
Column(verticalArrangement = Arrangement.spacedBy(8.dp)) {
Text(
modifier = Modifier.padding(horizontal = 16.dp),
text = label,
style = TangemTheme.typography.subtitle1,
color = TangemTheme.colors.text.primary1,
)
content()
}
}
@Composable
private fun <T> ChipGrid(items: List<T>, label: (T) -> String, isSelected: (T) -> Boolean, onSelect: (T) -> Unit) {
val shape = RoundedCornerShape(50)
Row(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp)
.clip(shape)
.background(TangemTheme.colors2.surface.level2)
.border(
width = 1.dp,
color = TangemTheme.colors2.border.neutral.secondary,
shape = shape,
)
.padding(4.dp),
horizontalArrangement = Arrangement.spacedBy(4.dp),
) {
items.forEach { item ->
Chip(
label = label(item),
selected = isSelected(item),
onClick = { onSelect(item) },
modifier = Modifier.weight(1f),
)
}
}
}
@Composable
private fun Chip(label: String, selected: Boolean, onClick: () -> Unit, modifier: Modifier = Modifier) {
val chipShape = RoundedCornerShape(50)
Box(
contentAlignment = Alignment.Center,
modifier = modifier
.clip(chipShape)
.background(
if (selected) TangemTheme.colors2.surface.level3 else Color.Transparent,
)
.clickable(onClick = onClick)
.padding(vertical = 8.dp, horizontal = 4.dp),
) {
Text(
text = label,
style = TangemTheme.typography.caption2,
color = if (selected) TangemTheme.colors.text.primary1 else TangemTheme.colors.text.secondary,
)
}
}
@Composable
private fun ToggleRow(label: String, checked: Boolean, onToggle: () -> Unit) {
Row(
modifier = Modifier
.fillMaxWidth()
.clip(RoundedCornerShape(12.dp))
.background(TangemTheme.colors2.surface.level2)
.clickable(onClick = onToggle)
.padding(horizontal = 12.dp, vertical = 12.dp),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.SpaceBetween,
) {
Text(
text = label,
style = TangemTheme.typography.body2,
color = TangemTheme.colors.text.primary1,
)
Text(
text = if (checked) "ON" else "OFF",
style = TangemTheme.typography.caption2,
color = if (checked) TangemTheme.colors.text.accent else TangemTheme.colors.text.secondary,
)
}
}

View file

@ -42,7 +42,9 @@ import com.tangem.feature.tester.presentation.storybook.page.ds.button.TangemBut
import com.tangem.feature.tester.presentation.storybook.page.ds.checkbox.TangemCheckboxV2Story
import com.tangem.feature.tester.presentation.storybook.page.ds.checkmark.TangemCheckmarkStory
import com.tangem.feature.tester.presentation.storybook.page.ds.fade.TangemFadeStory
import com.tangem.feature.tester.presentation.storybook.page.ds.glowring.TangemGlowRingStory
import com.tangem.feature.tester.presentation.storybook.page.ds.loader.TangemLoaderStory
import com.tangem.feature.tester.presentation.storybook.page.ds.messagebanner.TangemMessageBannerStory
import com.tangem.feature.tester.presentation.storybook.page.ds.row.TangemRowStory
import com.tangem.feature.tester.presentation.storybook.page.ds.search.TangemSearchStory
import com.tangem.feature.tester.presentation.storybook.page.ds.shimmer.TangemShimmerStory
@ -96,11 +98,13 @@ internal fun StoryBookScreen(state: StoryBookUM, modifier: Modifier = Modifier)
is TangemBadgeV2Story -> TangemBadgeV2Story(state = storyState)
is TangemCheckboxV2Story -> TangemCheckboxV2Story(state = storyState)
is TangemCheckmarkStory -> TangemCheckmarkStory(state = storyState)
is TangemGlowRingStory -> TangemGlowRingStory(state = storyState)
is TangemRowStory -> TangemRowStory(state = storyState)
is TangemSearchStory -> TangemSearchStory(state = storyState)
is TangemShimmerStory -> TangemShimmerStory(state = storyState)
is TangemFadeStory -> TangemFadeStory(state = storyState)
is TangemTopNavigationStory -> TangemTopNavigationStory(state = storyState)
is TangemMessageBannerStory -> TangemMessageBannerStory(state = storyState)
}
}
}

View file

@ -15,6 +15,9 @@
<string name="excluded_blockchains_search_placeholder" translatable="false">Filter by name or symbol</string>
<string name="blockchain_providers" translatable="false">Blockchain providers</string>
<string name="toggle_hot_wallet_restriction" translatable="false">Hot wallet creation restriction - %s</string>
<string name="usedesk_token_ttl" translatable="false">Usedesk chat token TTL - %s</string>
<string name="usedesk_token_ttl_title" translatable="false">Usedesk chat token TTL</string>
<string name="usedesk_token_ttl_custom" translatable="false">Custom (minutes)</string>
<string name="share_logs" translatable="false">Share logs</string>
<string name="test_push" translatable="false">Test push</string>
<string name="accounts" translatable="false">Accounts</string>