Updated on 2026-08-14

This commit is contained in:
Tangem 2026-07-08 10:19:22 +01:00
parent 332927dc9b
commit 16fb5487ee
4 changed files with 153 additions and 54 deletions

View file

@ -1,5 +1,6 @@
package com.tangem.feature.tester.presentation.featuretoggles.state
import com.tangem.core.ui.ds2.search.TangemSearch
import com.tangem.feature.tester.presentation.common.components.appbar.TopBarWithRefreshUM
import kotlinx.collections.immutable.ImmutableList
@ -8,6 +9,7 @@ import kotlinx.collections.immutable.ImmutableList
*
* @property topBar top bar state
* @property appVersion app version
* @property searchBar search bar state used to filter [featureToggleGroups] by toggle name
* @property featureToggleGroups feature toggles grouped by release status
* @property onToggleValueChange the lambda to be invoked when switch button is pressed
* @property onRestartAppClick the lambda to be invoked when restart app button is pressed
@ -15,6 +17,7 @@ import kotlinx.collections.immutable.ImmutableList
internal data class FeatureTogglesScreenUM(
val topBar: TopBarWithRefreshUM,
val appVersion: String,
val searchBar: TangemSearch.State,
val featureToggleGroups: ImmutableList<FeatureToggleGroupUM>,
val onToggleValueChange: (String, Boolean) -> Unit,
val onRestartAppClick: () -> Unit,

View file

@ -5,6 +5,7 @@ import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.LazyListScope
import androidx.compose.foundation.lazy.items
import androidx.compose.material3.Text
import androidx.compose.runtime.*
@ -17,7 +18,9 @@ import androidx.compose.ui.util.fastForEachIndexed
import com.tangem.core.ui.components.PrimaryButton
import com.tangem.core.ui.components.TangemSwitch
import com.tangem.core.ui.components.divider.DividerWithPadding
import com.tangem.core.ui.ds2.search.TangemSearch
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.extensions.wrappedList
import com.tangem.core.ui.res.TangemTheme
@ -34,6 +37,8 @@ import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.persistentListOf
import kotlinx.collections.immutable.toImmutableList
private val RESTART_BUTTON_OVERLAY_HEIGHT = 80.dp
/**
* Screen with feature toggles list
*
@ -42,69 +47,117 @@ import kotlinx.collections.immutable.toImmutableList
@OptIn(ExperimentalFoundationApi::class)
@Composable
internal fun FeatureTogglesScreen(state: FeatureTogglesScreenUM) {
LazyColumn(
Box(
modifier = Modifier
.fillMaxSize()
.background(TangemTheme.colors.background.secondary),
) {
stickyHeader { TopBarWithRefresh(state = state.topBar) }
if (state.topBar.refreshButton.isVisible) {
item(key = "warning_notification", contentType = "warning_notification") {
CustomSetupNotification(
subtitle = resourceReference(
id = R.string.feature_toggles_custom_setup_warning_description,
wrappedList(state.appVersion),
),
modifier = Modifier
.animateItem()
.padding(horizontal = 16.dp, vertical = 8.dp),
)
}
} else {
item(key = "warning_notification", contentType = "warning_notification") {
InitialSetupNotification(
subtitle = resourceReference(
id = R.string.feature_toggles_initial_setup_warning_description,
wrappedList(state.appVersion),
),
modifier = Modifier
.animateItem()
.padding(horizontal = 16.dp, vertical = 8.dp),
)
}
}
state.featureToggleGroups.fastForEachIndexed { index, group ->
if (index != 0) {
item(key = "divider_${group.status.name}", contentType = "group_divider") {
DividerWithPadding(horizontal = 16.dp, vertical = 8.dp)
LazyColumn(
modifier = Modifier.fillMaxSize(),
contentPadding = PaddingValues(bottom = RESTART_BUTTON_OVERLAY_HEIGHT),
) {
stickyHeader {
Column(modifier = Modifier.background(TangemTheme.colors.background.secondary)) {
TopBarWithRefresh(state = state.topBar)
TangemSearch(
state = state.searchBar,
modifier = Modifier.padding(horizontal = 16.dp, vertical = 12.dp),
)
}
}
item(key = "header_${group.status.name}", contentType = "group_header") {
GroupHeader(title = "${group.status.emoji} ${group.status.title}")
}
customSetup(state.topBar, state.appVersion)
items(
items = group.toggles,
key = TesterFeatureToggleUM::name,
contentType = { "feature_toggle_item" },
) { featureToggle ->
FeatureToggleItem(
toggle = featureToggle,
onCheckedChange = { isChange -> state.onToggleValueChange(featureToggle.name, isChange) },
)
}
emptySearch(state.featureToggleGroups, state.searchBar)
featureTogglesList(state.featureToggleGroups, state.onToggleValueChange)
}
item {
PrimaryButton(
text = stringResourceSafe(id = R.string.restart_app),
onClick = state.onRestartAppClick,
RestartButton(
onRestartAppClick = state.onRestartAppClick,
modifier = Modifier.align(Alignment.BottomCenter),
)
}
}
private fun LazyListScope.customSetup(topBar: TopBarWithRefreshUM, appVersion: String) {
if (topBar.refreshButton.isVisible) {
item(key = "warning_notification", contentType = "warning_notification") {
CustomSetupNotification(
subtitle = resourceReference(
id = R.string.feature_toggles_custom_setup_warning_description,
wrappedList(appVersion),
),
modifier = Modifier
.animateItem()
.padding(horizontal = 16.dp, vertical = 8.dp),
)
}
} else {
item(key = "warning_notification", contentType = "warning_notification") {
InitialSetupNotification(
subtitle = resourceReference(
id = R.string.feature_toggles_initial_setup_warning_description,
wrappedList(appVersion),
),
modifier = Modifier
.animateItem()
.padding(horizontal = 16.dp, vertical = 8.dp),
)
}
}
}
private fun LazyListScope.emptySearch(featureToggleGroups: List<FeatureToggleGroupUM>, searchBar: TangemSearch.State) {
if (featureToggleGroups.isEmpty() && searchBar.query.isNotBlank()) {
item(key = "nothing_found", contentType = "nothing_found") {
Text(
text = stringResourceSafe(R.string.feature_toggles_no_feature_toggles_found),
modifier = Modifier
.fillMaxWidth()
.padding(TangemTheme.dimens.spacing16),
color = TangemTheme.colors.text.tertiary,
style = TangemTheme.typography.body2,
)
}
}
}
@Composable
private fun RestartButton(onRestartAppClick: () -> Unit, modifier: Modifier = Modifier) {
PrimaryButton(
text = stringResourceSafe(id = R.string.restart_app),
onClick = onRestartAppClick,
modifier = modifier
.fillMaxWidth()
.background(TangemTheme.colors.background.secondary)
.padding(TangemTheme.dimens.spacing16),
)
}
private fun LazyListScope.featureTogglesList(
featureToggleGroups: List<FeatureToggleGroupUM>,
onToggleValueChange: (String, Boolean) -> Unit,
) {
featureToggleGroups.fastForEachIndexed { index, group ->
if (index != 0) {
item(key = "divider_${group.status.name}", contentType = "group_divider") {
DividerWithPadding(horizontal = 16.dp, vertical = 8.dp)
}
}
item(key = "header_${group.status.name}", contentType = "group_header") {
GroupHeader(title = "${group.status.emoji} ${group.status.title}")
}
items(
items = group.toggles,
key = TesterFeatureToggleUM::name,
contentType = { "feature_toggle_item" },
) { featureToggle ->
FeatureToggleItem(
toggle = featureToggle,
onCheckedChange = { isChange -> onToggleValueChange(featureToggle.name, isChange) },
)
}
}
@ -189,6 +242,13 @@ private fun PreviewFeatureTogglesScreen() {
),
),
appVersion = "5.38",
searchBar = TangemSearch.State(
placeholderText = stringReference(value = "Search"),
query = "",
onQueryChange = {},
isActive = false,
onActiveChange = {},
),
featureToggleGroups = previewFeatureToggleGroups(),
onToggleValueChange = { _, _ -> isCustomSetup = true },
onRestartAppClick = {},

View file

@ -8,6 +8,8 @@ import com.tangem.core.configtoggle.feature.FeatureTogglesManager
import com.tangem.core.configtoggle.feature.MutableFeatureTogglesManager
import com.tangem.core.configtoggle.version.Version
import com.tangem.core.navigation.finisher.AppFinisher
import com.tangem.core.ui.ds2.search.TangemSearch
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.feature.tester.impl.R
import com.tangem.feature.tester.presentation.common.components.appbar.TopBarWithRefreshUM
import com.tangem.feature.tester.presentation.featuretoggles.state.FeatureToggleGroupUM
@ -65,12 +67,42 @@ internal class FeatureTogglesViewModel @Inject constructor(
return FeatureTogglesScreenUM(
topBar = getConfigSetupState(isPrimarySetup = true),
appVersion = appInfoProvider.appVersion,
featureToggleGroups = mutableFeatureTogglesManager.getTesterFeatureToggleGroups(),
searchBar = initSearchBar(),
featureToggleGroups = mutableFeatureTogglesManager.getTesterFeatureToggleGroups(query = ""),
onToggleValueChange = ::onToggleValueChange,
onRestartAppClick = {},
)
}
private fun initSearchBar(): TangemSearch.State {
return TangemSearch.State(
placeholderText = resourceReference(com.tangem.core.ui.R.string.common_search),
query = "",
onQueryChange = ::onQueryChange,
isActive = false,
onActiveChange = ::onActiveChange,
onClearClick = ::onClearQuery,
)
}
private fun onQueryChange(query: String) {
val groups = mutableFeatureTogglesManager.getTesterFeatureToggleGroups(query = query)
state.update { screenUM ->
screenUM.copy(
searchBar = screenUM.searchBar.copy(query = query),
featureToggleGroups = groups,
)
}
}
private fun onActiveChange(isActive: Boolean) {
state.update { it.copy(searchBar = it.searchBar.copy(isActive = isActive)) }
}
private fun onClearQuery() {
onQueryChange(query = "")
}
private fun getConfigSetupState(isPrimarySetup: Boolean): TopBarWithRefreshUM {
val isMatchLocalConfig = mutableFeatureTogglesManager.isMatchLocalConfig()
@ -95,7 +127,7 @@ internal class FeatureTogglesViewModel @Inject constructor(
viewModelScope.launch {
mutableFeatureTogglesManager.changeToggle(name = name, isEnabled = isEnabled)
val groups = mutableFeatureTogglesManager.getTesterFeatureToggleGroups()
val groups = mutableFeatureTogglesManager.getTesterFeatureToggleGroups(query = state.value.searchBar.query)
state.update { it.copy(featureToggleGroups = groups) }
// delay for smoothly update animations
@ -111,13 +143,16 @@ internal class FeatureTogglesViewModel @Inject constructor(
mutableFeatureTogglesManager.recoverLocalConfig()
val topBar = getConfigSetupState(isPrimarySetup = false)
val groups = mutableFeatureTogglesManager.getTesterFeatureToggleGroups()
val groups = mutableFeatureTogglesManager.getTesterFeatureToggleGroups(query = state.value.searchBar.query)
state.update { it.copy(topBar = topBar, featureToggleGroups = groups) }
}
}
private fun MutableFeatureTogglesManager.getTesterFeatureToggleGroups(): ImmutableList<FeatureToggleGroupUM> {
private fun MutableFeatureTogglesManager.getTesterFeatureToggleGroups(
query: String,
): ImmutableList<FeatureToggleGroupUM> {
val togglesByStatus = getFeatureToggles()
.filter { it.name.contains(query, ignoreCase = true) }
.sortedWith(featureToggleComparator)
.map { info ->
TesterFeatureToggleUM(

View file

@ -6,6 +6,7 @@
<string name="initial_setup_warning_title" translatable="false">Initial setup</string>
<string name="feature_toggles_custom_setup_warning_description" translatable="false">Toggles differs from v%s config</string>
<string name="feature_toggles_initial_setup_warning_description" translatable="false">Toggles matches v%s config</string>
<string name="feature_toggles_no_feature_toggles_found" translatable="false">Cannot find any feature toggle by this query</string>
<string name="blockchain_providers_custom_setup_warning_description" translatable="false">Providers differs from release config</string>
<string name="restart_app" translatable="false">Restart app</string>
<string name="environment_toggles" translatable="false">Environment toggles</string>