From a2bc98f8a838c3c4820e7c034f43e03a1145001f Mon Sep 17 00:00:00 2001 From: Tangem Date: Sun, 22 Sep 2024 22:09:59 +0300 Subject: [PATCH] Updated on 2026-08-14 --- .../manager/DevFeatureTogglesManager.kt | 20 +++- .../manager/MutableFeatureTogglesManager.kt | 6 + .../ui/EnvironmentTogglesScreen.kt | 6 +- .../state/FeatureTogglesContentState.kt | 31 +++++- .../featuretoggles/ui/FeatureTogglesScreen.kt | 103 ++++++++++++++---- .../viewmodels/FeatureTogglesViewModel.kt | 41 +++++-- .../impl/src/main/res/values/strings.xml | 4 +- 7 files changed, 171 insertions(+), 40 deletions(-) diff --git a/core/featuretoggles/src/main/kotlin/com/tangem/core/featuretoggle/manager/DevFeatureTogglesManager.kt b/core/featuretoggles/src/main/kotlin/com/tangem/core/featuretoggle/manager/DevFeatureTogglesManager.kt index 9b29a65f66..303dce3c7b 100644 --- a/core/featuretoggles/src/main/kotlin/com/tangem/core/featuretoggle/manager/DevFeatureTogglesManager.kt +++ b/core/featuretoggles/src/main/kotlin/com/tangem/core/featuretoggle/manager/DevFeatureTogglesManager.kt @@ -24,6 +24,7 @@ internal class DevFeatureTogglesManager( ) : MutableFeatureTogglesManager { private var featureTogglesMap: MutableMap by Delegates.notNull() + private var localFeatureTogglesMap: Map by Delegates.notNull() override suspend fun init() { localFeatureTogglesStorage.init() @@ -32,8 +33,12 @@ internal class DevFeatureTogglesManager( key = PreferencesKeys.FEATURE_TOGGLES_KEY, ) ?: emptyMap() - featureTogglesMap = localFeatureTogglesStorage.featureToggles + val localFeatureToggles = localFeatureTogglesStorage.featureToggles .associateToggles(currentVersion = versionProvider.get().orEmpty()) + + localFeatureTogglesMap = localFeatureToggles + + featureTogglesMap = localFeatureToggles .mapValues { resultToggle -> savedFeatureToggles[resultToggle.key] ?: resultToggle.value } @@ -42,16 +47,27 @@ internal class DevFeatureTogglesManager( override fun isFeatureEnabled(name: String): Boolean = featureTogglesMap[name] ?: false + override fun isMatchLocalConfig(): Boolean = featureTogglesMap == localFeatureTogglesMap + override fun getFeatureToggles(): Map = featureTogglesMap override suspend fun changeToggle(name: String, isEnabled: Boolean) { featureTogglesMap[name] ?: return featureTogglesMap[name] = isEnabled - appPreferencesStore.storeObject(PreferencesKeys.FEATURE_TOGGLES_KEY, featureTogglesMap) + appPreferencesStore.storeFeatureToggles(value = featureTogglesMap) + } + + override suspend fun recoverLocalConfig() { + featureTogglesMap = localFeatureTogglesMap.toMutableMap() + appPreferencesStore.storeFeatureToggles(value = localFeatureTogglesMap) } @VisibleForTesting(otherwise = VisibleForTesting.NONE) fun setFeatureToggles(map: MutableMap) { featureTogglesMap = map } + + private suspend fun AppPreferencesStore.storeFeatureToggles(value: Map) { + storeObject(PreferencesKeys.FEATURE_TOGGLES_KEY, value) + } } \ No newline at end of file diff --git a/core/featuretoggles/src/main/kotlin/com/tangem/core/featuretoggle/manager/MutableFeatureTogglesManager.kt b/core/featuretoggles/src/main/kotlin/com/tangem/core/featuretoggle/manager/MutableFeatureTogglesManager.kt index 8d7a063c5d..f04b423f4f 100644 --- a/core/featuretoggles/src/main/kotlin/com/tangem/core/featuretoggle/manager/MutableFeatureTogglesManager.kt +++ b/core/featuretoggles/src/main/kotlin/com/tangem/core/featuretoggle/manager/MutableFeatureTogglesManager.kt @@ -7,9 +7,15 @@ package com.tangem.core.featuretoggle.manager */ interface MutableFeatureTogglesManager : FeatureTogglesManager { + /** Check if the current state of the feature toggles matches the local config state. */ + fun isMatchLocalConfig(): Boolean + /** Get feature toggles */ fun getFeatureToggles(): Map /** Change availability [isEnabled] of toggle with name [name] */ suspend fun changeToggle(name: String, isEnabled: Boolean) + + /** Recover local config state */ + suspend fun recoverLocalConfig() } \ No newline at end of file diff --git a/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/environments/ui/EnvironmentTogglesScreen.kt b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/environments/ui/EnvironmentTogglesScreen.kt index b9c442ce86..10a459c2f3 100644 --- a/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/environments/ui/EnvironmentTogglesScreen.kt +++ b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/environments/ui/EnvironmentTogglesScreen.kt @@ -9,7 +9,10 @@ import androidx.compose.foundation.layout.* import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.itemsIndexed import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.material3.* +import androidx.compose.material3.SegmentedButton +import androidx.compose.material3.SegmentedButtonDefaults +import androidx.compose.material3.SingleChoiceSegmentedButtonRow +import androidx.compose.material3.Text import androidx.compose.runtime.* import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.RectangleShape @@ -65,7 +68,6 @@ internal fun EnvironmentTogglesScreen(uiModel: EnvironmentTogglesScreenUM) { } } -@OptIn(ExperimentalMaterial3Api::class) @Composable private fun EnvironmentButtons( uiModel: EnvironmentTogglesScreenUM.ApiInfoUM, diff --git a/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/featuretoggles/state/FeatureTogglesContentState.kt b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/featuretoggles/state/FeatureTogglesContentState.kt index c1e4e290bb..fb97f05a55 100644 --- a/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/featuretoggles/state/FeatureTogglesContentState.kt +++ b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/featuretoggles/state/FeatureTogglesContentState.kt @@ -6,14 +6,37 @@ import kotlinx.collections.immutable.ImmutableList /** * Content state of feature toggles screen * + * @property topBarState top bar state + * @property appVersion app version * @property featureToggles feature toggles list * @property onBackClick the lambda to be invoked when back button is pressed * @property onToggleValueChange the lambda to be invoked when switch button is pressed - * @property onApplyChangesClick the lambda to be invoked when apply changes button is pressed + * @property onRestartAppClick the lambda to be invoked when restart app button is pressed */ internal data class FeatureTogglesContentState( + val topBarState: TopBarState, + val appVersion: String, val featureToggles: ImmutableList, - val onToggleValueChange: (String, Boolean) -> Unit, val onBackClick: () -> Unit, - val onApplyChangesClick: () -> Unit, -) \ No newline at end of file + val onToggleValueChange: (String, Boolean) -> Unit, + val onRestartAppClick: () -> Unit, +) { + + /** Top bar state */ + sealed interface TopBarState { + + /** + * Config setup + * + * @property onBackClick the lambda to be invoked when back button is pressed + */ + data object ConfigSetup : TopBarState + + /** + * Custom setup + * + * @property onRecoverClick the lambda to be invoked when recover button is pressed + */ + data class CustomSetup(val onRecoverClick: () -> Unit) : TopBarState + } +} \ No newline at end of file diff --git a/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/featuretoggles/ui/FeatureTogglesScreen.kt b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/featuretoggles/ui/FeatureTogglesScreen.kt index 0ebd8cadbf..6fd2e7e7e9 100644 --- a/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/featuretoggles/ui/FeatureTogglesScreen.kt +++ b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/featuretoggles/ui/FeatureTogglesScreen.kt @@ -6,17 +6,21 @@ import androidx.compose.foundation.background import androidx.compose.foundation.layout.* import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items -import androidx.compose.material.Switch -import androidx.compose.material.SwitchDefaults -import androidx.compose.material.Text -import androidx.compose.runtime.Composable +import androidx.compose.material3.Text +import androidx.compose.runtime.* import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource 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.components.PrimaryButton -import com.tangem.core.ui.components.appbar.AppBarWithBackButton +import com.tangem.core.ui.components.TangemSwitch +import com.tangem.core.ui.components.appbar.AppBarWithBackButtonAndIcon +import com.tangem.core.ui.components.notifications.Notification +import com.tangem.core.ui.components.notifications.NotificationConfig +import com.tangem.core.ui.extensions.resourceReference +import com.tangem.core.ui.extensions.wrappedList import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreview import com.tangem.feature.tester.impl.R @@ -37,22 +41,35 @@ internal fun FeatureTogglesScreen(state: FeatureTogglesContentState) { .fillMaxSize() .background(TangemTheme.colors.background.secondary), ) { - stickyHeader { - AppBarWithBackButton( - onBackClick = state.onBackClick, - text = stringResource(id = R.string.feature_toggles), - ) + stickyHeader { TopAppBar(state = state.topBarState, onBackClick = state.onBackClick) } + + if (state.topBarState is FeatureTogglesContentState.TopBarState.CustomSetup) { + item(key = "warning_notification", contentType = "warning_notification") { + WarningNotification( + appVersion = state.appVersion, + modifier = Modifier + .animateItem() + .padding(horizontal = 16.dp) + .padding(bottom = 8.dp), + ) + } } - items(state.featureToggles) { featureToggle -> + + items( + items = state.featureToggles, + key = TesterFeatureToggle::name, + contentType = { "feature_toggle_item" }, + ) { featureToggle -> FeatureToggleItem( toggle = featureToggle, onCheckedChange = { isChange -> state.onToggleValueChange(featureToggle.name, isChange) }, ) } + item { PrimaryButton( - text = stringResource(id = R.string.apply_changes), - onClick = state.onApplyChangesClick, + text = stringResource(id = R.string.restart_app), + onClick = state.onRestartAppClick, modifier = Modifier .fillMaxWidth() .padding(TangemTheme.dimens.spacing16), @@ -61,12 +78,48 @@ internal fun FeatureTogglesScreen(state: FeatureTogglesContentState) { } } +@Composable +private fun TopAppBar(state: FeatureTogglesContentState.TopBarState, onBackClick: () -> Unit) { + AppBarWithBackButtonAndIcon( + onBackClick = onBackClick, + text = stringResource(id = R.string.feature_toggles), + iconRes = if (state is FeatureTogglesContentState.TopBarState.CustomSetup) { + R.drawable.ic_refresh_24 + } else { + null + }, + onIconClick = if (state is FeatureTogglesContentState.TopBarState.CustomSetup) { + state.onRecoverClick + } else { + null + }, + ) +} + +@Composable +private fun WarningNotification(appVersion: String, modifier: Modifier = Modifier) { + Notification( + config = NotificationConfig( + subtitle = resourceReference( + id = R.string.feature_toggles_custom_setup_warning_description, + wrappedList(appVersion), + ), + iconResId = R.drawable.ic_alert_triangle_20, + title = resourceReference(id = R.string.feature_toggles_custom_setup_warning_title), + ), + modifier = modifier, + ) +} + @Composable private fun FeatureToggleItem(toggle: TesterFeatureToggle, onCheckedChange: (Boolean) -> Unit) { Row( modifier = Modifier .fillMaxWidth() - .padding(horizontal = TangemTheme.dimens.spacing18), + .padding( + horizontal = TangemTheme.dimens.spacing18, + vertical = TangemTheme.dimens.spacing8, + ), horizontalArrangement = Arrangement.SpaceBetween, verticalAlignment = Alignment.CenterVertically, ) { @@ -78,14 +131,8 @@ private fun FeatureToggleItem(toggle: TesterFeatureToggle, onCheckedChange: (Boo maxLines = 1, style = TangemTheme.typography.body2, ) - Switch( - checked = toggle.isEnabled, - onCheckedChange = onCheckedChange, - colors = SwitchDefaults.colors( - checkedThumbColor = TangemTheme.colors.control.checked, - uncheckedThumbColor = TangemTheme.colors.control.unchecked, - ), - ) + + TangemSwitch(onCheckedChange = onCheckedChange, checked = toggle.isEnabled) } } @@ -94,15 +141,23 @@ private fun FeatureToggleItem(toggle: TesterFeatureToggle, onCheckedChange: (Boo @Composable private fun PreviewFeatureTogglesScreen() { TangemThemePreview { + var isCustomSetup by remember { mutableStateOf(value = true) } + FeatureTogglesScreen( state = FeatureTogglesContentState( + topBarState = if (isCustomSetup) { + FeatureTogglesContentState.TopBarState.CustomSetup(onRecoverClick = { isCustomSetup = false }) + } else { + FeatureTogglesContentState.TopBarState.ConfigSetup + }, + appVersion = "5.15", featureToggles = persistentListOf( TesterFeatureToggle(name = "FEATURE_TOGGLE_1", isEnabled = true), TesterFeatureToggle(name = "FEATURE_TOGGLE_2", isEnabled = false), ), - onToggleValueChange = { _, _ -> }, onBackClick = {}, - onApplyChangesClick = {}, + onToggleValueChange = { _, _ -> isCustomSetup = true }, + onRestartAppClick = {}, ), ) } diff --git a/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/featuretoggles/viewmodels/FeatureTogglesViewModel.kt b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/featuretoggles/viewmodels/FeatureTogglesViewModel.kt index 6200dd283a..2af75d3b80 100644 --- a/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/featuretoggles/viewmodels/FeatureTogglesViewModel.kt +++ b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/featuretoggles/viewmodels/FeatureTogglesViewModel.kt @@ -11,10 +11,11 @@ import com.tangem.core.navigation.finisher.AppFinisher import com.tangem.feature.tester.presentation.featuretoggles.models.TesterFeatureToggle import com.tangem.feature.tester.presentation.featuretoggles.state.FeatureTogglesContentState import com.tangem.feature.tester.presentation.navigation.InnerTesterRouter -import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import com.tangem.utils.version.AppVersionProvider import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.toImmutableList +import kotlinx.coroutines.delay import kotlinx.coroutines.launch import javax.inject.Inject @@ -22,14 +23,14 @@ import javax.inject.Inject * ViewModel for screen with list of feature toggles * * @property featureTogglesManager manager for getting information about the availability of feature toggles - * @property dispatchers coroutine dispatchers provider + * @property appVersionProvider app version provider * [REDACTED_AUTHOR] */ @HiltViewModel internal class FeatureTogglesViewModel @Inject constructor( private val featureTogglesManager: FeatureTogglesManager, - private val dispatchers: CoroutineDispatcherProvider, + private val appVersionProvider: AppVersionProvider, ) : ViewModel() { /** Current ui state */ @@ -41,28 +42,54 @@ internal class FeatureTogglesViewModel @Inject constructor( "Feature toggle manager must be mutable (debug build type)" } - /** Setup navigation state property by router [router] and provides app restart method by [appRestarter] */ + /** Setup navigation state property by router [router] and provides app restart method by [appFinisher] */ fun setupInteractions(router: InnerTesterRouter, appFinisher: AppFinisher) { uiState = uiState.copy( onBackClick = router::back, - onApplyChangesClick = appFinisher::restart, + onRestartAppClick = appFinisher::restart, ) } private fun initState(): FeatureTogglesContentState { return FeatureTogglesContentState( + topBarState = getConfigSetupState(), + appVersion = appVersionProvider.versionName, featureToggles = mutableFeatureTogglesManager.getTesterFeatureToggles(), onToggleValueChange = ::onToggleValueChange, onBackClick = {}, - onApplyChangesClick = {}, + onRestartAppClick = {}, ) } private fun onToggleValueChange(name: String, isEnabled: Boolean) { - viewModelScope.launch(dispatchers.main) { + viewModelScope.launch { mutableFeatureTogglesManager.changeToggle(name = name, isEnabled = isEnabled) uiState = uiState.copy(featureToggles = mutableFeatureTogglesManager.getTesterFeatureToggles()) + + // delay for smoothly update animations + delay(timeMillis = 300) + + uiState = uiState.copy(topBarState = getConfigSetupState()) + } + } + + private fun getConfigSetupState(): FeatureTogglesContentState.TopBarState { + return if (mutableFeatureTogglesManager.isMatchLocalConfig()) { + FeatureTogglesContentState.TopBarState.ConfigSetup + } else { + FeatureTogglesContentState.TopBarState.CustomSetup( + onRecoverClick = { + viewModelScope.launch { + mutableFeatureTogglesManager.recoverLocalConfig() + + uiState = uiState.copy( + topBarState = getConfigSetupState(), + featureToggles = mutableFeatureTogglesManager.getTesterFeatureToggles(), + ) + } + }, + ) } } diff --git a/features/tester/impl/src/main/res/values/strings.xml b/features/tester/impl/src/main/res/values/strings.xml index dfd6f4d39c..969d34bce4 100644 --- a/features/tester/impl/src/main/res/values/strings.xml +++ b/features/tester/impl/src/main/res/values/strings.xml @@ -2,7 +2,9 @@ Tester menu Feature toggles - Apply changes + Custom setup + Feature toggles differs from v%s config + Restart app Environment toggles Tester actions Hide all currencies