Updated on 2026-08-14

This commit is contained in:
Tangem 2025-01-06 18:23:30 +03:00
parent a98ecb4182
commit 6417281c76
11 changed files with 165 additions and 116 deletions

View file

@ -0,0 +1,18 @@
package com.tangem.feature.tester.presentation.common.components.appbar
import androidx.compose.runtime.Composable
import com.tangem.core.ui.components.appbar.AppBarWithBackButtonAndIcon
import com.tangem.core.ui.extensions.stringResourceSafe
import com.tangem.core.ui.res.TangemTheme
import com.tangem.feature.tester.impl.R
@Composable
internal fun TopBarWithRefresh(state: TopBarWithRefreshUM) {
AppBarWithBackButtonAndIcon(
onBackClick = state.onBackClick,
text = stringResourceSafe(state.titleResId),
iconRes = R.drawable.ic_refresh_24.takeIf { state.refreshButton.isVisible },
onIconClick = state.refreshButton.onRefreshClick,
backgroundColor = TangemTheme.colors.background.primary,
)
}

View file

@ -0,0 +1,12 @@
package com.tangem.feature.tester.presentation.common.components.appbar
import androidx.annotation.StringRes
internal data class TopBarWithRefreshUM(
@StringRes val titleResId: Int,
val onBackClick: () -> Unit,
val refreshButton: RefreshButton,
) {
data class RefreshButton(val isVisible: Boolean, val onRefreshClick: () -> Unit)
}

View file

@ -0,0 +1,21 @@
package com.tangem.feature.tester.presentation.common.components.notification
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import com.tangem.core.ui.components.notifications.Notification
import com.tangem.core.ui.components.notifications.NotificationConfig
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.feature.tester.impl.R
@Composable
internal fun CustomSetupNotification(subtitle: TextReference, modifier: Modifier = Modifier) {
Notification(
config = NotificationConfig(
subtitle = subtitle,
iconResId = R.drawable.ic_alert_triangle_20,
title = resourceReference(id = R.string.custom_setup_warning_title),
),
modifier = modifier,
)
}

View file

@ -114,7 +114,7 @@ private fun TogglesNotification(appVersion: String, isVisible: Boolean, modifier
wrappedList(appVersion),
),
iconResId = R.drawable.ic_alert_triangle_20,
title = resourceReference(id = R.string.feature_toggles_custom_setup_warning_title),
title = resourceReference(id = R.string.custom_setup_warning_title),
),
)
}

View file

@ -1,42 +1,22 @@
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 topBarState top bar state
* @property topBar 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 onRestartAppClick the lambda to be invoked when restart app button is pressed
*/
internal data class FeatureTogglesContentState(
val topBarState: TopBarState,
val topBar: TopBarWithRefreshUM,
val appVersion: String,
val featureToggles: ImmutableList<TesterFeatureToggle>,
val onBackClick: () -> Unit,
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
}
}
)

View file

@ -15,15 +15,15 @@ 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.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.stringResourceSafe
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
import com.tangem.feature.tester.presentation.common.components.appbar.TopBarWithRefresh
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.featuretoggles.models.TesterFeatureToggle
import com.tangem.feature.tester.presentation.featuretoggles.state.FeatureTogglesContentState
import kotlinx.collections.immutable.persistentListOf
@ -41,12 +41,15 @@ internal fun FeatureTogglesScreen(state: FeatureTogglesContentState) {
.fillMaxSize()
.background(TangemTheme.colors.background.secondary),
) {
stickyHeader { TopAppBar(state = state.topBarState, onBackClick = state.onBackClick) }
stickyHeader { TopBarWithRefresh(state = state.topBar) }
if (state.topBarState is FeatureTogglesContentState.TopBarState.CustomSetup) {
if (state.topBar.refreshButton.isVisible) {
item(key = "warning_notification", contentType = "warning_notification") {
WarningNotification(
appVersion = state.appVersion,
CustomSetupNotification(
subtitle = resourceReference(
id = R.string.feature_toggles_custom_setup_warning_description,
wrappedList(state.appVersion),
),
modifier = Modifier
.animateItem()
.padding(horizontal = 16.dp)
@ -78,39 +81,6 @@ internal fun FeatureTogglesScreen(state: FeatureTogglesContentState) {
}
}
@Composable
private fun TopAppBar(state: FeatureTogglesContentState.TopBarState, onBackClick: () -> Unit) {
AppBarWithBackButtonAndIcon(
onBackClick = onBackClick,
text = stringResourceSafe(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(
@ -145,17 +115,19 @@ private fun PreviewFeatureTogglesScreen() {
FeatureTogglesScreen(
state = FeatureTogglesContentState(
topBarState = if (isCustomSetup) {
FeatureTogglesContentState.TopBarState.CustomSetup(onRecoverClick = { isCustomSetup = false })
} else {
FeatureTogglesContentState.TopBarState.ConfigSetup
},
topBar = TopBarWithRefreshUM(
titleResId = R.string.feature_toggles,
onBackClick = {},
refreshButton = TopBarWithRefreshUM.RefreshButton(
isVisible = isCustomSetup,
onRefreshClick = { isCustomSetup = false },
),
),
appVersion = "5.15",
featureToggles = persistentListOf(
TesterFeatureToggle(name = "FEATURE_TOGGLE_1", isEnabled = true),
TesterFeatureToggle(name = "FEATURE_TOGGLE_2", isEnabled = false),
),
onBackClick = {},
onToggleValueChange = { _, _ -> isCustomSetup = true },
onRestartAppClick = {},
),

View file

@ -8,6 +8,8 @@ import androidx.lifecycle.viewModelScope
import com.tangem.core.configtoggle.feature.FeatureTogglesManager
import com.tangem.core.configtoggle.feature.MutableFeatureTogglesManager
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.navigation.InnerTesterRouter
@ -45,22 +47,40 @@ 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(
onBackClick = router::back,
topBar = uiState.topBar.copy(onBackClick = router::back),
onRestartAppClick = appFinisher::restart,
)
}
private fun initState(): FeatureTogglesContentState {
return FeatureTogglesContentState(
topBarState = getConfigSetupState(),
topBar = getConfigSetupState(isPrimarySetup = true),
appVersion = appVersionProvider.versionName,
featureToggles = mutableFeatureTogglesManager.getTesterFeatureToggles(),
onToggleValueChange = ::onToggleValueChange,
onBackClick = {},
onRestartAppClick = {},
)
}
private fun getConfigSetupState(isPrimarySetup: Boolean): TopBarWithRefreshUM {
val isMatchLocalConfig = mutableFeatureTogglesManager.isMatchLocalConfig()
return if (isPrimarySetup) {
TopBarWithRefreshUM(
titleResId = R.string.feature_toggles,
onBackClick = {},
refreshButton = TopBarWithRefreshUM.RefreshButton(
isVisible = false,
onRefreshClick = ::onRefreshClick,
),
)
} else {
uiState.topBar.copy(
refreshButton = uiState.topBar.refreshButton.copy(isVisible = isMatchLocalConfig),
)
}
}
private fun onToggleValueChange(name: String, isEnabled: Boolean) {
viewModelScope.launch {
mutableFeatureTogglesManager.changeToggle(name = name, isEnabled = isEnabled)
@ -70,25 +90,17 @@ internal class FeatureTogglesViewModel @Inject constructor(
// delay for smoothly update animations
delay(timeMillis = 300)
uiState = uiState.copy(topBarState = getConfigSetupState())
uiState = uiState.copy(topBar = getConfigSetupState(isPrimarySetup = false))
}
}
private fun getConfigSetupState(): FeatureTogglesContentState.TopBarState {
return if (mutableFeatureTogglesManager.isMatchLocalConfig()) {
FeatureTogglesContentState.TopBarState.ConfigSetup
} else {
FeatureTogglesContentState.TopBarState.CustomSetup(
onRecoverClick = {
viewModelScope.launch {
mutableFeatureTogglesManager.recoverLocalConfig()
private fun onRefreshClick() {
viewModelScope.launch {
mutableFeatureTogglesManager.recoverLocalConfig()
uiState = uiState.copy(
topBarState = getConfigSetupState(),
featureToggles = mutableFeatureTogglesManager.getTesterFeatureToggles(),
)
}
},
uiState = uiState.copy(
topBar = getConfigSetupState(isPrimarySetup = false),
featureToggles = mutableFeatureTogglesManager.getTesterFeatureToggles(),
)
}
}

View file

@ -2,10 +2,11 @@ package com.tangem.feature.tester.presentation.providers.entity
import androidx.compose.runtime.Immutable
import com.tangem.blockchain.common.network.providers.ProviderType
import com.tangem.feature.tester.presentation.common.components.appbar.TopBarWithRefreshUM
import kotlinx.collections.immutable.ImmutableList
internal data class BlockchainProvidersUM(
val onBackClick: () -> Unit,
val topBar: TopBarWithRefreshUM,
val blockchainProviders: ImmutableList<ProvidersUM>,
) {

View file

@ -27,13 +27,13 @@ import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import androidx.compose.ui.util.fastForEachIndexed
import com.tangem.core.ui.components.SpacerH8
import com.tangem.core.ui.components.appbar.AppBarWithBackButton
import com.tangem.core.ui.components.rows.RowText
import com.tangem.core.ui.extensions.getActiveIconRes
import com.tangem.core.ui.extensions.stringResourceSafe
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.res.TangemTheme
import com.tangem.feature.tester.impl.R
import com.tangem.feature.tester.presentation.common.components.appbar.TopBarWithRefresh
import com.tangem.feature.tester.presentation.common.components.notification.CustomSetupNotification
import com.tangem.feature.tester.presentation.providers.entity.BlockchainProvidersUM
import com.tangem.feature.tester.presentation.providers.entity.BlockchainProvidersUM.ProviderUM
import com.tangem.feature.tester.presentation.providers.entity.BlockchainProvidersUM.ProvidersUM
@ -50,12 +50,20 @@ internal fun BlockchainProvidersScreen(state: BlockchainProvidersUM) {
.background(TangemTheme.colors.background.primary),
) {
LazyColumn {
stickyHeader {
AppBarWithBackButton(
onBackClick = state.onBackClick,
text = stringResourceSafe(id = R.string.blockchain_providers),
containerColor = TangemTheme.colors.background.primary,
)
stickyHeader { TopBarWithRefresh(state = state.topBar) }
if (state.topBar.refreshButton.isVisible) {
item(key = "notification", contentType = "notification") {
CustomSetupNotification(
subtitle = resourceReference(
id = R.string.blockchain_providers_custom_setup_warning_description,
),
modifier = Modifier
.animateItem()
.padding(horizontal = 16.dp)
.padding(bottom = 8.dp),
)
}
}
items(
@ -74,8 +82,8 @@ private fun BlockchainProvidersItem(state: ProvidersUM) {
ExpandableBlockchainRow(
state = state,
modifier = Modifier.clickable { isExpanded = !isExpanded },
content = { Providers(state = state, isExpanded = isExpanded) },
onExpandStateChange = { isExpanded = !isExpanded },
)
}
@ -83,25 +91,23 @@ private fun BlockchainProvidersItem(state: ProvidersUM) {
private fun ExpandableBlockchainRow(
state: ProvidersUM,
content: @Composable () -> Unit,
modifier: Modifier = Modifier,
onExpandStateChange: () -> Unit,
) {
Column(
modifier = modifier
.fillMaxWidth()
.padding(horizontal = 16.dp),
) {
BlockchainRow(state)
Column {
BlockchainRow(state = state, onExpandStateChange = onExpandStateChange)
content()
}
}
@Composable
private fun BlockchainRow(state: ProvidersUM) {
private fun BlockchainRow(state: ProvidersUM, onExpandStateChange: () -> Unit) {
Row(
modifier = Modifier
.clickable { onExpandStateChange() }
.padding(horizontal = 16.dp)
.fillMaxWidth()
.padding(vertical = 16.dp),
.padding(vertical = 12.dp),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically,
) {
@ -145,11 +151,15 @@ private fun BlockchainRow(state: ProvidersUM) {
private fun Providers(state: ProvidersUM, isExpanded: Boolean) {
AnimatedVisibility(
visible = isExpanded,
modifier = Modifier.padding(horizontal = 16.dp),
enter = fadeIn() + expandVertically(),
exit = fadeOut() + shrinkVertically(),
label = "providers_visibility",
) {
Column(verticalArrangement = Arrangement.spacedBy(12.dp)) {
Column(
modifier = Modifier.padding(vertical = 8.dp),
verticalArrangement = Arrangement.spacedBy(12.dp),
) {
state.providers.fastForEachIndexed { index, provider ->
key("${provider.name}_$index") {
ProviderItem(
@ -157,8 +167,6 @@ private fun Providers(state: ProvidersUM, isExpanded: Boolean) {
state = provider,
onDrop = { prev, new -> state.onDrop(state.blockchainId, prev, new) },
)
if (index == state.providers.lastIndex) SpacerH8()
}
}
}

View file

@ -6,6 +6,8 @@ import com.tangem.blockchain.common.Blockchain
import com.tangem.blockchain.common.network.providers.ProviderType
import com.tangem.blockchainsdk.providers.BlockchainProvidersTypesManager
import com.tangem.blockchainsdk.providers.MutableBlockchainProvidersTypesManager
import com.tangem.feature.tester.impl.R
import com.tangem.feature.tester.presentation.common.components.appbar.TopBarWithRefreshUM
import com.tangem.feature.tester.presentation.navigation.InnerTesterRouter
import com.tangem.feature.tester.presentation.providers.entity.BlockchainProvidersUM
import com.tangem.feature.tester.presentation.providers.entity.BlockchainProvidersUM.ProviderUM
@ -26,7 +28,14 @@ internal class BlockchainProvidersViewModel @Inject constructor(
val state: StateFlow<BlockchainProvidersUM> get() = _state.asStateFlow()
private val _state = MutableStateFlow(
value = BlockchainProvidersUM(onBackClick = {}, blockchainProviders = persistentListOf()),
value = BlockchainProvidersUM(
topBar = TopBarWithRefreshUM(
titleResId = R.string.blockchain_providers,
onBackClick = {},
refreshButton = TopBarWithRefreshUM.RefreshButton(isVisible = false, onRefreshClick = ::onRefreshClick),
),
blockchainProviders = persistentListOf(),
),
)
private val manager = blockchainProvidersTypesManager as MutableBlockchainProvidersTypesManager
@ -37,15 +46,24 @@ internal class BlockchainProvidersViewModel @Inject constructor(
fun setupNavigation(router: InnerTesterRouter) {
_state.update { state ->
state.copy(onBackClick = router::back)
state.copy(
topBar = state.topBar.copy(onBackClick = router::back),
)
}
}
private fun subscribeOnBlockchainProviderTypesUpdates() {
manager.get()
.onEach { blockchainProviderTypes ->
val isChanged = !manager.isMatchWithMerged()
_state.update { state ->
state.copy(blockchainProviders = blockchainProviderTypes.toProvidersUM())
state.copy(
topBar = state.topBar.copy(
refreshButton = state.topBar.refreshButton.copy(isVisible = isChanged),
),
blockchainProviders = blockchainProviderTypes.toProvidersUM(),
)
}
}
.launchIn(viewModelScope)
@ -67,6 +85,12 @@ internal class BlockchainProvidersViewModel @Inject constructor(
.toImmutableList()
}
private fun onRefreshClick() {
viewModelScope.launch {
manager.update()
}
}
private fun onDrop(id: String, prev: Int, current: Int) {
val providers = _state.value
.blockchainProviders.first { it.blockchainId == id }

View file

@ -2,8 +2,9 @@
<resources>
<string name="tester_menu" translatable="false">Tester menu</string>
<string name="feature_toggles" translatable="false">Feature toggles</string>
<string name="feature_toggles_custom_setup_warning_title" translatable="false">Custom setup</string>
<string name="custom_setup_warning_title" translatable="false">Custom setup</string>
<string name="feature_toggles_custom_setup_warning_description" translatable="false">Toggles differs from v%s config</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>
<string name="tester_actions" translatable="false">Tester actions</string>