Updated on 2026-08-14
This commit is contained in:
parent
e3d2e7f2e1
commit
4fbb427a1d
16 changed files with 367 additions and 125 deletions
|
|
@ -126,7 +126,8 @@ internal class TesterActivity : ComposeActivity() {
|
|||
setupInteractions(innerTesterRouter, appFinisher)
|
||||
}
|
||||
|
||||
FeatureTogglesScreen(state = viewModel.uiState)
|
||||
val state by viewModel.state.collectAsStateWithLifecycle()
|
||||
FeatureTogglesScreen(state = state)
|
||||
}
|
||||
|
||||
composable(route = TesterScreen.ENVIRONMENTS_TOGGLES.name) {
|
||||
|
|
|
|||
|
|
@ -1,14 +0,0 @@
|
|||
package com.tangem.feature.tester.presentation.featuretoggles.models
|
||||
|
||||
/**
|
||||
* Presentation model of feature toggle
|
||||
*
|
||||
* @property name name
|
||||
* @property isEnabled availability
|
||||
*
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
internal data class TesterFeatureToggle(
|
||||
val name: String,
|
||||
val isEnabled: Boolean,
|
||||
)
|
||||
|
|
@ -0,0 +1,14 @@
|
|||
package com.tangem.feature.tester.presentation.featuretoggles.state
|
||||
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
|
||||
/**
|
||||
* Group of feature toggles sharing the same release [status]
|
||||
*
|
||||
* @property status release status common for all [toggles]
|
||||
* @property toggles toggles of this group
|
||||
*/
|
||||
internal data class FeatureToggleGroupUM(
|
||||
val status: TesterFeatureToggleUM.Status,
|
||||
val toggles: ImmutableList<TesterFeatureToggleUM>,
|
||||
)
|
||||
|
|
@ -1,22 +1,21 @@
|
|||
package com.tangem.feature.tester.presentation.featuretoggles.state
|
||||
|
||||
import com.tangem.feature.tester.presentation.common.components.appbar.TopBarWithRefreshUM
|
||||
import com.tangem.feature.tester.presentation.featuretoggles.models.TesterFeatureToggle
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
|
||||
/**
|
||||
* Content state of feature toggles screen
|
||||
*
|
||||
* @property topBar top bar state
|
||||
* @property topBar top bar state
|
||||
* @property appVersion app version
|
||||
* @property featureToggles feature toggles list
|
||||
* @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
|
||||
*/
|
||||
internal data class FeatureTogglesContentState(
|
||||
internal data class FeatureTogglesScreenUM(
|
||||
val topBar: TopBarWithRefreshUM,
|
||||
val appVersion: String,
|
||||
val featureToggles: ImmutableList<TesterFeatureToggle>,
|
||||
val featureToggleGroups: ImmutableList<FeatureToggleGroupUM>,
|
||||
val onToggleValueChange: (String, Boolean) -> Unit,
|
||||
val onRestartAppClick: () -> Unit,
|
||||
)
|
||||
|
|
@ -0,0 +1,30 @@
|
|||
package com.tangem.feature.tester.presentation.featuretoggles.state
|
||||
|
||||
/**
|
||||
* Presentation model of feature toggle
|
||||
*
|
||||
* @property name name
|
||||
* @property version release version ("undefined" for permanently disabled toggles)
|
||||
* @property status release status relative to the current app version
|
||||
* @property isEnabled availability
|
||||
*/
|
||||
internal data class TesterFeatureToggleUM(
|
||||
val name: String,
|
||||
val version: String,
|
||||
val status: Status,
|
||||
val isEnabled: Boolean,
|
||||
) {
|
||||
|
||||
/**
|
||||
* Release status relative to the current app version.
|
||||
*
|
||||
* Declaration order defines the display order of groups on the screen
|
||||
* (most interesting first, least interesting last).
|
||||
*/
|
||||
enum class Status(val title: String, val emoji: String) {
|
||||
PENDING("Planned for current release", "⏳"),
|
||||
PLANNED("Planned for next releases", "🗓️"),
|
||||
UNDEFINED("Not planned yet", "❓"),
|
||||
RELEASED("Released", "✅"),
|
||||
}
|
||||
}
|
||||
|
|
@ -13,8 +13,10 @@ import androidx.compose.ui.Modifier
|
|||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
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.extensions.resourceReference
|
||||
import com.tangem.core.ui.extensions.stringResourceSafe
|
||||
import com.tangem.core.ui.extensions.wrappedList
|
||||
|
|
@ -25,9 +27,12 @@ import com.tangem.feature.tester.presentation.common.components.appbar.TopBarWit
|
|||
import com.tangem.feature.tester.presentation.common.components.appbar.TopBarWithRefreshUM
|
||||
import com.tangem.feature.tester.presentation.common.components.notification.CustomSetupNotification
|
||||
import com.tangem.feature.tester.presentation.common.components.notification.InitialSetupNotification
|
||||
import com.tangem.feature.tester.presentation.featuretoggles.models.TesterFeatureToggle
|
||||
import com.tangem.feature.tester.presentation.featuretoggles.state.FeatureTogglesContentState
|
||||
import com.tangem.feature.tester.presentation.featuretoggles.state.FeatureToggleGroupUM
|
||||
import com.tangem.feature.tester.presentation.featuretoggles.state.TesterFeatureToggleUM
|
||||
import com.tangem.feature.tester.presentation.featuretoggles.state.FeatureTogglesScreenUM
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
|
||||
/**
|
||||
* Screen with feature toggles list
|
||||
|
|
@ -36,7 +41,7 @@ import kotlinx.collections.immutable.persistentListOf
|
|||
*/
|
||||
@OptIn(ExperimentalFoundationApi::class)
|
||||
@Composable
|
||||
internal fun FeatureTogglesScreen(state: FeatureTogglesContentState) {
|
||||
internal fun FeatureTogglesScreen(state: FeatureTogglesScreenUM) {
|
||||
LazyColumn(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
|
|
@ -70,15 +75,27 @@ internal fun FeatureTogglesScreen(state: FeatureTogglesContentState) {
|
|||
}
|
||||
}
|
||||
|
||||
items(
|
||||
items = state.featureToggles,
|
||||
key = TesterFeatureToggle::name,
|
||||
contentType = { "feature_toggle_item" },
|
||||
) { featureToggle ->
|
||||
FeatureToggleItem(
|
||||
toggle = featureToggle,
|
||||
onCheckedChange = { isChange -> state.onToggleValueChange(featureToggle.name, isChange) },
|
||||
)
|
||||
state.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 -> state.onToggleValueChange(featureToggle.name, isChange) },
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
item {
|
||||
|
|
@ -94,7 +111,22 @@ internal fun FeatureTogglesScreen(state: FeatureTogglesContentState) {
|
|||
}
|
||||
|
||||
@Composable
|
||||
private fun FeatureToggleItem(toggle: TesterFeatureToggle, onCheckedChange: (Boolean) -> Unit) {
|
||||
private fun GroupHeader(title: String) {
|
||||
Text(
|
||||
text = title,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(
|
||||
horizontal = TangemTheme.dimens.spacing18,
|
||||
vertical = TangemTheme.dimens.spacing8,
|
||||
),
|
||||
color = TangemTheme.colors.text.primary1,
|
||||
style = TangemTheme.typography.h3,
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun FeatureToggleItem(toggle: TesterFeatureToggleUM, onCheckedChange: (Boolean) -> Unit) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
|
|
@ -105,14 +137,35 @@ private fun FeatureToggleItem(toggle: TesterFeatureToggle, onCheckedChange: (Boo
|
|||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
Text(
|
||||
text = toggle.name,
|
||||
modifier = Modifier.weight(1f),
|
||||
color = TangemTheme.colors.text.primary1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
maxLines = 1,
|
||||
style = TangemTheme.typography.body2,
|
||||
)
|
||||
Column(modifier = Modifier.weight(1f)) {
|
||||
Text(
|
||||
text = toggle.name,
|
||||
color = TangemTheme.colors.text.primary1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
maxLines = 1,
|
||||
style = TangemTheme.typography.body2,
|
||||
)
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing4),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
Text(
|
||||
text = toggle.version,
|
||||
color = TangemTheme.colors.text.tertiary,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
maxLines = 1,
|
||||
style = TangemTheme.typography.caption2,
|
||||
)
|
||||
if (toggle.status == TesterFeatureToggleUM.Status.RELEASED && !toggle.isEnabled) {
|
||||
Text(
|
||||
text = "Disabled",
|
||||
color = TangemTheme.colors.text.warning,
|
||||
maxLines = 1,
|
||||
style = TangemTheme.typography.caption2,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
TangemSwitch(onCheckedChange = onCheckedChange, checked = toggle.isEnabled)
|
||||
}
|
||||
|
|
@ -126,7 +179,7 @@ private fun PreviewFeatureTogglesScreen() {
|
|||
var isCustomSetup by remember { mutableStateOf(value = true) }
|
||||
|
||||
FeatureTogglesScreen(
|
||||
state = FeatureTogglesContentState(
|
||||
state = FeatureTogglesScreenUM(
|
||||
topBar = TopBarWithRefreshUM(
|
||||
titleResId = R.string.feature_toggles,
|
||||
onBackClick = {},
|
||||
|
|
@ -135,14 +188,34 @@ private fun PreviewFeatureTogglesScreen() {
|
|||
onRefreshClick = { isCustomSetup = false },
|
||||
),
|
||||
),
|
||||
appVersion = "5.15",
|
||||
featureToggles = persistentListOf(
|
||||
TesterFeatureToggle(name = "FEATURE_TOGGLE_1", isEnabled = true),
|
||||
TesterFeatureToggle(name = "FEATURE_TOGGLE_2", isEnabled = false),
|
||||
),
|
||||
appVersion = "5.38",
|
||||
featureToggleGroups = previewFeatureToggleGroups(),
|
||||
onToggleValueChange = { _, _ -> isCustomSetup = true },
|
||||
onRestartAppClick = {},
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun previewFeatureToggleGroups(): ImmutableList<FeatureToggleGroupUM> {
|
||||
fun group(status: TesterFeatureToggleUM.Status, vararg toggles: Triple<String, String, Boolean>) =
|
||||
FeatureToggleGroupUM(
|
||||
status = status,
|
||||
toggles = toggles
|
||||
.map { (name, version, isEnabled) ->
|
||||
TesterFeatureToggleUM(name = name, version = version, status = status, isEnabled = isEnabled)
|
||||
}
|
||||
.toImmutableList(),
|
||||
)
|
||||
|
||||
return persistentListOf(
|
||||
group(TesterFeatureToggleUM.Status.PENDING, Triple("FEATURE_TOGGLE_2", "5.38", false)),
|
||||
group(TesterFeatureToggleUM.Status.PLANNED, Triple("FEATURE_TOGGLE_1", "5.40", true)),
|
||||
group(TesterFeatureToggleUM.Status.UNDEFINED, Triple("FEATURE_TOGGLE_4", "undefined", false)),
|
||||
group(
|
||||
TesterFeatureToggleUM.Status.RELEASED,
|
||||
Triple("FEATURE_TOGGLE_3", "5.15", true),
|
||||
Triple("FEATURE_TOGGLE_3_OFF", "5.15", false),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
|
@ -1,23 +1,27 @@
|
|||
package com.tangem.feature.tester.presentation.featuretoggles.viewmodels
|
||||
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.tangem.core.configtoggle.feature.DISABLED_FEATURE_TOGGLE_VERSION
|
||||
import com.tangem.core.configtoggle.feature.FeatureToggleInfo
|
||||
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.feature.tester.impl.R
|
||||
import com.tangem.feature.tester.presentation.common.components.appbar.TopBarWithRefreshUM
|
||||
import com.tangem.feature.tester.presentation.featuretoggles.models.TesterFeatureToggle
|
||||
import com.tangem.feature.tester.presentation.featuretoggles.state.FeatureTogglesContentState
|
||||
import com.tangem.feature.tester.presentation.featuretoggles.state.FeatureToggleGroupUM
|
||||
import com.tangem.feature.tester.presentation.featuretoggles.state.TesterFeatureToggleUM
|
||||
import com.tangem.feature.tester.presentation.featuretoggles.state.FeatureTogglesScreenUM
|
||||
import com.tangem.feature.tester.presentation.navigation.InnerTesterRouter
|
||||
import com.tangem.utils.info.AppInfoProvider
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.update
|
||||
import kotlinx.coroutines.launch
|
||||
import javax.inject.Inject
|
||||
|
||||
|
|
@ -35,9 +39,11 @@ internal class FeatureTogglesViewModel @Inject constructor(
|
|||
private val appInfoProvider: AppInfoProvider,
|
||||
) : ViewModel() {
|
||||
|
||||
/** Current ui state */
|
||||
var uiState: FeatureTogglesContentState by mutableStateOf(initState())
|
||||
private set
|
||||
// Declared before `state` so it is initialized before `initState()` runs in the field initializer.
|
||||
private val appVersion: Version? = Version.create(appInfoProvider.appVersion)
|
||||
|
||||
val state: StateFlow<FeatureTogglesScreenUM>
|
||||
field = MutableStateFlow(initState())
|
||||
|
||||
private val mutableFeatureTogglesManager: MutableFeatureTogglesManager
|
||||
get() = requireNotNull(featureTogglesManager as? MutableFeatureTogglesManager) {
|
||||
|
|
@ -46,17 +52,19 @@ internal class FeatureTogglesViewModel @Inject constructor(
|
|||
|
||||
/** Setup navigation state property by router [router] and provides app restart method by [appFinisher] */
|
||||
fun setupInteractions(router: InnerTesterRouter, appFinisher: AppFinisher) {
|
||||
uiState = uiState.copy(
|
||||
topBar = uiState.topBar.copy(onBackClick = router::back),
|
||||
onRestartAppClick = appFinisher::restart,
|
||||
)
|
||||
state.update { current ->
|
||||
current.copy(
|
||||
topBar = current.topBar.copy(onBackClick = router::back),
|
||||
onRestartAppClick = appFinisher::restart,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun initState(): FeatureTogglesContentState {
|
||||
return FeatureTogglesContentState(
|
||||
private fun initState(): FeatureTogglesScreenUM {
|
||||
return FeatureTogglesScreenUM(
|
||||
topBar = getConfigSetupState(isPrimarySetup = true),
|
||||
appVersion = appInfoProvider.appVersion,
|
||||
featureToggles = mutableFeatureTogglesManager.getTesterFeatureToggles(),
|
||||
featureToggleGroups = mutableFeatureTogglesManager.getTesterFeatureToggleGroups(),
|
||||
onToggleValueChange = ::onToggleValueChange,
|
||||
onRestartAppClick = {},
|
||||
)
|
||||
|
|
@ -75,8 +83,9 @@ internal class FeatureTogglesViewModel @Inject constructor(
|
|||
),
|
||||
)
|
||||
} else {
|
||||
uiState.topBar.copy(
|
||||
refreshButton = uiState.topBar.refreshButton.copy(isVisible = !isMatchLocalConfig),
|
||||
val topBar = state.value.topBar
|
||||
topBar.copy(
|
||||
refreshButton = topBar.refreshButton.copy(isVisible = !isMatchLocalConfig),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -85,12 +94,14 @@ internal class FeatureTogglesViewModel @Inject constructor(
|
|||
viewModelScope.launch {
|
||||
mutableFeatureTogglesManager.changeToggle(name = name, isEnabled = isEnabled)
|
||||
|
||||
uiState = uiState.copy(featureToggles = mutableFeatureTogglesManager.getTesterFeatureToggles())
|
||||
val groups = mutableFeatureTogglesManager.getTesterFeatureToggleGroups()
|
||||
state.update { it.copy(featureToggleGroups = groups) }
|
||||
|
||||
// delay for smoothly update animations
|
||||
delay(timeMillis = 300)
|
||||
|
||||
uiState = uiState.copy(topBar = getConfigSetupState(isPrimarySetup = false))
|
||||
val topBar = getConfigSetupState(isPrimarySetup = false)
|
||||
state.update { it.copy(topBar = topBar) }
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -98,17 +109,52 @@ internal class FeatureTogglesViewModel @Inject constructor(
|
|||
viewModelScope.launch {
|
||||
mutableFeatureTogglesManager.recoverLocalConfig()
|
||||
|
||||
uiState = uiState.copy(
|
||||
topBar = getConfigSetupState(isPrimarySetup = false),
|
||||
featureToggles = mutableFeatureTogglesManager.getTesterFeatureToggles(),
|
||||
)
|
||||
val topBar = getConfigSetupState(isPrimarySetup = false)
|
||||
val groups = mutableFeatureTogglesManager.getTesterFeatureToggleGroups()
|
||||
state.update { it.copy(topBar = topBar, featureToggleGroups = groups) }
|
||||
}
|
||||
}
|
||||
|
||||
private fun MutableFeatureTogglesManager.getTesterFeatureToggles(): ImmutableList<TesterFeatureToggle> {
|
||||
return this
|
||||
.getFeatureToggles()
|
||||
.map { TesterFeatureToggle(it.key, it.value) }
|
||||
private fun MutableFeatureTogglesManager.getTesterFeatureToggleGroups(): ImmutableList<FeatureToggleGroupUM> {
|
||||
val togglesByStatus = getFeatureToggles()
|
||||
.sortedWith(featureToggleComparator)
|
||||
.map { info ->
|
||||
TesterFeatureToggleUM(
|
||||
name = info.name,
|
||||
version = info.version,
|
||||
status = statusOf(info.version),
|
||||
isEnabled = info.isEnabled,
|
||||
)
|
||||
}
|
||||
.groupBy(TesterFeatureToggleUM::status)
|
||||
|
||||
return TesterFeatureToggleUM.Status.entries
|
||||
.mapNotNull { status ->
|
||||
togglesByStatus[status]?.let { toggles ->
|
||||
FeatureToggleGroupUM(status = status, toggles = toggles.toImmutableList())
|
||||
}
|
||||
}
|
||||
.toImmutableList()
|
||||
}
|
||||
|
||||
private fun statusOf(toggleVersion: String): TesterFeatureToggleUM.Status {
|
||||
val toggle = parseToggleVersion(toggleVersion) ?: return TesterFeatureToggleUM.Status.UNDEFINED
|
||||
val app = appVersion ?: return TesterFeatureToggleUM.Status.UNDEFINED
|
||||
return when {
|
||||
toggle > app -> TesterFeatureToggleUM.Status.PLANNED
|
||||
toggle < app -> TesterFeatureToggleUM.Status.RELEASED
|
||||
else -> TesterFeatureToggleUM.Status.PENDING
|
||||
}
|
||||
}
|
||||
|
||||
private companion object {
|
||||
// Descending by release version; toggles with no planned release ("undefined") sink to the bottom.
|
||||
private val featureToggleComparator: Comparator<FeatureToggleInfo> =
|
||||
compareByDescending<FeatureToggleInfo> { parseToggleVersion(it.version) }
|
||||
.thenBy { it.name }
|
||||
|
||||
// Avoids Version.create() (which logs on parse failure) for the "no planned release" sentinel.
|
||||
private fun parseToggleVersion(version: String): Version? =
|
||||
if (version == DISABLED_FEATURE_TOGGLE_VERSION) null else Version.create(version)
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue