Updated on 2026-08-14

This commit is contained in:
Tangem 2024-08-09 12:25:31 +03:00
parent c63e6daf3c
commit 6aa15f2322
12 changed files with 364 additions and 23 deletions

View file

@ -16,6 +16,7 @@ dependencies {
/** Compose */
implementation(deps.compose.material)
implementation(deps.compose.material3)
implementation(deps.compose.foundation)
implementation(deps.compose.navigation)
implementation(deps.compose.navigation.hilt)
@ -33,9 +34,11 @@ dependencies {
/** Other libraries */
implementation(deps.arrow.core)
implementation(deps.kotlin.immutable.collections)
implementation(deps.timber)
/** Core modules */
implementation(projects.core.datasource)
implementation(projects.core.featuretoggles)
implementation(projects.core.ui)
implementation(projects.core.utils)
@ -45,5 +48,6 @@ dependencies {
implementation(projects.features.tester.api)
/** Other modules */
implementation(projects.common.routing)
implementation(projects.libs.crypto)
}

View file

@ -1,18 +1,22 @@
package com.tangem.feature.tester.presentation
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.ui.Modifier
import androidx.hilt.navigation.compose.hiltViewModel
import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable
import androidx.navigation.compose.rememberNavController
import com.google.accompanist.systemuicontroller.rememberSystemUiController
import com.tangem.common.routing.AppRouter
import com.tangem.core.navigation.finisher.AppFinisher
import com.tangem.core.ui.UiDependencies
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.screen.ComposeActivity
import com.tangem.feature.tester.presentation.actions.TesterActionsScreen
import com.tangem.feature.tester.presentation.actions.TesterActionsViewModel
import com.tangem.feature.tester.presentation.environments.ui.EnvironmentTogglesScreen
import com.tangem.feature.tester.presentation.environments.viewmodels.EnvironmentsTogglesViewModel
import com.tangem.feature.tester.presentation.featuretoggles.ui.FeatureTogglesScreen
import com.tangem.feature.tester.presentation.featuretoggles.viewmodels.FeatureTogglesViewModel
import com.tangem.feature.tester.presentation.menu.state.TesterMenuContentState
@ -37,6 +41,9 @@ internal class TesterActivity : ComposeActivity() {
@Inject
lateinit var appFinisher: AppFinisher
@Inject
lateinit var appRouter: AppRouter
private val innerTesterRouter: InnerTesterRouter
get() = requireNotNull(testerRouter as? InnerTesterRouter) {
"TesterRouter must be InnerTesterRouter for tester feature"
@ -60,8 +67,9 @@ internal class TesterActivity : ComposeActivity() {
composable(route = TesterScreen.MENU.name) {
TesterMenuScreen(
state = TesterMenuContentState(
onBackClick = innerTesterRouter::back,
onBackClick = { appRouter.pop { finish() } },
onFeatureTogglesClick = { innerTesterRouter.open(TesterScreen.FEATURE_TOGGLES) },
onEnvironmentTogglesClick = { innerTesterRouter.open(TesterScreen.ENVIRONMENTS_TOGGLES) },
onTesterActionsClick = { innerTesterRouter.open(TesterScreen.TESTER_ACTIONS) },
),
)
@ -75,6 +83,16 @@ internal class TesterActivity : ComposeActivity() {
FeatureTogglesScreen(state = viewModel.uiState)
}
composable(route = TesterScreen.ENVIRONMENTS_TOGGLES.name) {
val viewModel = hiltViewModel<EnvironmentsTogglesViewModel>().apply {
setupNavigation(innerTesterRouter)
}
EnvironmentTogglesScreen(
uiModel = viewModel.uiState.collectAsState().value,
)
}
composable(route = TesterScreen.TESTER_ACTIONS.name) {
val viewModel = hiltViewModel<TesterActionsViewModel>().apply {
setupNavigation(innerTesterRouter)

View file

@ -0,0 +1,37 @@
package com.tangem.feature.tester.presentation.environments.state
import androidx.annotation.StringRes
import kotlinx.collections.immutable.ImmutableSet
/**
* Content state of environment toggles screen
*
* @property title title
* @property apiInfoList environment toggles list
* @property onEnvironmentSelect the lambda to be invoked when button is pressed
* @property onBackClick the lambda to be invoked when back button is pressed
* @property onApplyChangesClick the lambda to be invoked when apply changes button is pressed
*/
internal data class EnvironmentTogglesScreenUM(
@StringRes val title: Int,
val apiInfoList: ImmutableSet<ApiInfoUM>,
val onEnvironmentSelect: (id: String, environment: String) -> Unit,
val onBackClick: () -> Unit,
val onApplyChangesClick: () -> Unit,
) {
/**
* Api info
*
* @property name api name
* @property select select environment
* @property url select url
* @property environments list of environments
*/
data class ApiInfoUM(
val name: String,
val select: String,
val url: String,
val environments: ImmutableSet<String>,
)
}

View file

@ -0,0 +1,181 @@
package com.tangem.feature.tester.presentation.environments.ui
import android.content.res.Configuration
import androidx.compose.animation.AnimatedContent
import androidx.compose.foundation.BorderStroke
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.itemsIndexed
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.RectangleShape
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.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreview
import com.tangem.datasource.api.common.config.ApiConfig
import com.tangem.datasource.api.common.config.ApiEnvironment
import com.tangem.feature.tester.impl.R
import com.tangem.feature.tester.presentation.environments.state.EnvironmentTogglesScreenUM
import kotlinx.collections.immutable.persistentSetOf
/**
* Screen with environment toggles list
*
* @param uiModel screen state
*/
@OptIn(ExperimentalFoundationApi::class)
@Composable
internal fun EnvironmentTogglesScreen(uiModel: EnvironmentTogglesScreenUM) {
LazyColumn(
modifier = Modifier
.fillMaxSize()
.background(TangemTheme.colors.background.secondary),
) {
stickyHeader {
AppBarWithBackButton(
onBackClick = uiModel.onBackClick,
text = stringResource(id = uiModel.title),
)
}
itemsIndexed(
items = uiModel.apiInfoList.toTypedArray(),
key = { _, info -> info.name },
) { index, info ->
EnvironmentButtons(
uiModel = info,
onSelect = { isChange -> uiModel.onEnvironmentSelect(info.name, isChange) },
modifier = Modifier.padding(
bottom = if (index == uiModel.apiInfoList.toTypedArray().lastIndex) {
TangemTheme.dimens.spacing0
} else {
TangemTheme.dimens.spacing10
},
),
)
}
item {
PrimaryButton(
text = stringResource(id = R.string.apply_changes),
onClick = uiModel.onApplyChangesClick,
modifier = Modifier
.fillMaxWidth()
.padding(TangemTheme.dimens.spacing16),
)
}
}
}
@OptIn(ExperimentalMaterial3Api::class)
@Composable
private fun EnvironmentButtons(
uiModel: EnvironmentTogglesScreenUM.ApiInfoUM,
onSelect: (String) -> Unit,
modifier: Modifier = Modifier,
) {
Column(
modifier = modifier
.fillMaxWidth()
.padding(horizontal = TangemTheme.dimens.spacing16),
verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing6),
) {
Column(modifier = Modifier.fillMaxWidth()) {
Text(
text = uiModel.name,
color = TangemTheme.colors.text.primary1,
maxLines = 1,
style = TangemTheme.typography.h3,
)
AnimatedContent(targetState = uiModel.url, label = "") {
Text(
text = it,
color = TangemTheme.colors.text.accent,
overflow = TextOverflow.Ellipsis,
maxLines = 1,
style = TangemTheme.typography.body2,
)
}
}
SingleChoiceSegmentedButtonRow(modifier = Modifier.fillMaxWidth()) {
uiModel.environments.onEachIndexed { index, environment ->
key(environment) {
SegmentedButton(
selected = environment == uiModel.select,
onClick = { onSelect(environment) },
shape = when (index) {
0 -> RoundedCornerShape(
topStart = TangemTheme.dimens.radius12,
bottomStart = TangemTheme.dimens.radius12,
)
uiModel.environments.toTypedArray().lastIndex -> RoundedCornerShape(
topEnd = TangemTheme.dimens.radius12,
bottomEnd = TangemTheme.dimens.radius12,
)
else -> RectangleShape
},
colors = SegmentedButtonDefaults.colors(
activeContainerColor = TangemTheme.colors.control.checked,
activeContentColor = TangemTheme.colors.text.primary2,
inactiveContainerColor = TangemTheme.colors.control.unchecked,
inactiveContentColor = TangemTheme.colors.text.primary1,
),
border = BorderStroke(0.dp, TangemTheme.colors.background.tertiary),
) {
Text(text = environment, style = TangemTheme.typography.subtitle2)
}
}
}
}
}
}
@Preview
@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES)
@Composable
private fun PreviewFeatureTogglesScreen() {
TangemThemePreview {
var select by remember { mutableStateOf(ApiEnvironment.DEV.name) }
EnvironmentTogglesScreen(
uiModel = EnvironmentTogglesScreenUM(
title = R.string.environment_toggles,
apiInfoList = persistentSetOf(
EnvironmentTogglesScreenUM.ApiInfoUM(
name = ApiConfig.ID.Express.name,
select = select,
url = "https://api.express.tangem.com",
environments = persistentSetOf(
ApiEnvironment.DEV.name,
ApiEnvironment.STAGE.name,
ApiEnvironment.PROD.name,
),
),
EnvironmentTogglesScreenUM.ApiInfoUM(
name = ApiConfig.ID.TangemTech.name,
select = select,
url = "https://api.express.tangem.com",
environments = persistentSetOf(
ApiEnvironment.DEV.name,
ApiEnvironment.PROD.name,
),
),
),
onEnvironmentSelect = { _: String, s1: String -> select = s1 },
onBackClick = {},
onApplyChangesClick = {},
),
)
}
}

View file

@ -0,0 +1,95 @@
package com.tangem.feature.tester.presentation.environments.viewmodels
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.tangem.datasource.api.common.config.ApiConfig
import com.tangem.datasource.api.common.config.ApiEnvironment
import com.tangem.datasource.api.common.config.managers.ApiConfigsManager
import com.tangem.datasource.api.common.config.managers.MutableApiConfigsManager
import com.tangem.feature.tester.impl.BuildConfig
import com.tangem.feature.tester.impl.R
import com.tangem.feature.tester.presentation.environments.state.EnvironmentTogglesScreenUM
import com.tangem.feature.tester.presentation.navigation.InnerTesterRouter
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.collections.immutable.ImmutableSet
import kotlinx.collections.immutable.persistentSetOf
import kotlinx.collections.immutable.toImmutableSet
import kotlinx.coroutines.flow.*
import kotlinx.coroutines.launch
import javax.inject.Inject
/**
* ViewModel for screen with list of environment toggles
*
* @param apiConfigsManager manager for getting information about the api configs
*
[REDACTED_AUTHOR]
*/
@HiltViewModel
internal class EnvironmentsTogglesViewModel @Inject constructor(
apiConfigsManager: ApiConfigsManager,
) : ViewModel() {
/** Current ui state */
val uiState: StateFlow<EnvironmentTogglesScreenUM>
get() = _uiState
private val _uiState = MutableStateFlow(value = getInitialState())
private val mutableApiConfigsManager: MutableApiConfigsManager =
requireNotNull(apiConfigsManager as? MutableApiConfigsManager) {
"MutableApiConfigsManager isn't available in build type ${BuildConfig.BUILD_TYPE}."
}
init {
subscribeOnApiConfigs()
}
/** Setup navigation state property by router [router] */
fun setupNavigation(router: InnerTesterRouter) {
_uiState.update {
it.copy(onBackClick = router::back, onApplyChangesClick = router::back)
}
}
private fun subscribeOnApiConfigs() {
mutableApiConfigsManager.configs
.onEach { configs ->
_uiState.update {
it.copy(apiInfoList = configs.toUiModel())
}
}
.launchIn(viewModelScope)
}
private fun List<ApiConfig>.toUiModel(): ImmutableSet<EnvironmentTogglesScreenUM.ApiInfoUM> {
return map { config ->
EnvironmentTogglesScreenUM.ApiInfoUM(
name = config.id.name,
select = config.currentEnvironment.name,
url = config.environments[config.currentEnvironment]
?: error("Current environment's url isn't found"),
environments = config.environments
.map { environment -> environment.key.name }
.toImmutableSet(),
)
}
.toImmutableSet()
}
private fun getInitialState(): EnvironmentTogglesScreenUM {
return EnvironmentTogglesScreenUM(
title = R.string.environment_toggles,
apiInfoList = persistentSetOf(),
onEnvironmentSelect = ::onToggleValueChange,
onBackClick = {},
onApplyChangesClick = {},
)
}
private fun onToggleValueChange(id: String, name: String) {
viewModelScope.launch {
mutableApiConfigsManager.changeEnvironment(id = id, environment = ApiEnvironment.valueOf(name))
}
}
}

View file

@ -1,6 +1,7 @@
package com.tangem.feature.tester.presentation.featuretoggles.state
import com.tangem.feature.tester.presentation.featuretoggles.models.TesterFeatureToggle
import kotlinx.collections.immutable.ImmutableList
/**
* Content state of feature toggles screen
@ -11,7 +12,7 @@ import com.tangem.feature.tester.presentation.featuretoggles.models.TesterFeatur
* @property onApplyChangesClick the lambda to be invoked when apply changes button is pressed
*/
internal data class FeatureTogglesContentState(
val featureToggles: List<TesterFeatureToggle>,
val featureToggles: ImmutableList<TesterFeatureToggle>,
val onToggleValueChange: (String, Boolean) -> Unit,
val onBackClick: () -> Unit,
val onApplyChangesClick: () -> Unit,

View file

@ -3,11 +3,7 @@ package com.tangem.feature.tester.presentation.featuretoggles.ui
import android.content.res.Configuration
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement
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.*
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.material.Switch
@ -26,6 +22,7 @@ import com.tangem.core.ui.res.TangemThemePreview
import com.tangem.feature.tester.impl.R
import com.tangem.feature.tester.presentation.featuretoggles.models.TesterFeatureToggle
import com.tangem.feature.tester.presentation.featuretoggles.state.FeatureTogglesContentState
import kotlinx.collections.immutable.persistentListOf
/**
* Screen with feature toggles list
@ -56,7 +53,8 @@ internal fun FeatureTogglesScreen(state: FeatureTogglesContentState) {
PrimaryButton(
text = stringResource(id = R.string.apply_changes),
onClick = state.onApplyChangesClick,
modifier = Modifier.fillMaxWidth()
modifier = Modifier
.fillMaxWidth()
.padding(TangemTheme.dimens.spacing16),
)
}
@ -98,7 +96,7 @@ private fun PreviewFeatureTogglesScreen() {
TangemThemePreview {
FeatureTogglesScreen(
state = FeatureTogglesContentState(
featureToggles = listOf(
featureToggles = persistentListOf(
TesterFeatureToggle(name = "FEATURE_TOGGLE_1", isEnabled = true),
TesterFeatureToggle(name = "FEATURE_TOGGLE_2", isEnabled = false),
),

View file

@ -13,6 +13,8 @@ import com.tangem.feature.tester.presentation.featuretoggles.state.FeatureToggle
import com.tangem.feature.tester.presentation.navigation.InnerTesterRouter
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.toImmutableList
import kotlinx.coroutines.launch
import javax.inject.Inject
@ -64,9 +66,10 @@ internal class FeatureTogglesViewModel @Inject constructor(
}
}
private fun MutableFeatureTogglesManager.getTesterFeatureToggles(): List<TesterFeatureToggle> {
private fun MutableFeatureTogglesManager.getTesterFeatureToggles(): ImmutableList<TesterFeatureToggle> {
return this
.getFeatureToggles()
.map { TesterFeatureToggle(it.key, it.value) }
.toImmutableList()
}
}

View file

@ -3,12 +3,14 @@ package com.tangem.feature.tester.presentation.menu.state
/**
* Content state of tester menu screen
*
* @property onBackClick the lambda to be invoked when back button is pressed
* @property onFeatureTogglesClick the lambda to be invoked when feature toggles button is pressed
* @property onTesterActionsClick the lambda to be invoked when tester actions button is pressed
* @property onBackClick the lambda to be invoked when back button is pressed
* @property onFeatureTogglesClick the lambda to be invoked when feature toggles button is pressed
* @property onEnvironmentTogglesClick the lambda to be invoked when environment toggles button is pressed
* @property onTesterActionsClick the lambda to be invoked when tester actions button is pressed
*/
data class TesterMenuContentState(
val onBackClick: () -> Unit,
val onFeatureTogglesClick: () -> Unit,
val onEnvironmentTogglesClick: () -> Unit,
val onTesterActionsClick: () -> Unit,
)

View file

@ -1,12 +1,9 @@
package com.tangem.feature.tester.presentation.menu.ui
import android.content.res.Configuration
import androidx.activity.compose.BackHandler
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.runtime.Composable
@ -27,6 +24,8 @@ import com.tangem.feature.tester.presentation.menu.state.TesterMenuContentState
*/
@Composable
internal fun TesterMenuScreen(state: TesterMenuContentState) {
BackHandler(onBack = state.onBackClick)
Column(
modifier = Modifier
.fillMaxSize()
@ -36,6 +35,7 @@ internal fun TesterMenuScreen(state: TesterMenuContentState) {
onBackClick = state.onBackClick,
text = stringResource(id = R.string.tester_menu),
)
Column(
modifier = Modifier
.padding(
@ -50,12 +50,13 @@ internal fun TesterMenuScreen(state: TesterMenuContentState) {
onClick = state.onFeatureTogglesClick,
modifier = Modifier.fillMaxWidth(),
)
PrimaryButton(
text = stringResource(R.string.stand_toggles),
onClick = state.onFeatureTogglesClick,
text = stringResource(R.string.environment_toggles),
onClick = state.onEnvironmentTogglesClick,
modifier = Modifier.fillMaxWidth(),
enabled = false,
)
PrimaryButton(
modifier = Modifier.fillMaxWidth(),
text = stringResource(id = R.string.tester_actions),
@ -74,6 +75,7 @@ private fun PreviewTesterMenuScreen() {
state = TesterMenuContentState(
onBackClick = {},
onFeatureTogglesClick = {},
onEnvironmentTogglesClick = {},
onTesterActionsClick = {},
),
)

View file

@ -6,5 +6,5 @@ package com.tangem.feature.tester.presentation.navigation
[REDACTED_AUTHOR]
*/
internal enum class TesterScreen {
MENU, FEATURE_TOGGLES, TESTER_ACTIONS
MENU, FEATURE_TOGGLES, ENVIRONMENTS_TOGGLES, TESTER_ACTIONS
}

View file

@ -3,7 +3,7 @@
<string name="tester_menu" translatable="false">Tester menu</string>
<string name="feature_toggles" translatable="false">Feature toggles</string>
<string name="apply_changes" translatable="false">Apply changes</string>
<string name="stand_toggles" translatable="false">Stand toggles</string>
<string name="environment_toggles" translatable="false">Environment toggles</string>
<string name="tester_actions" translatable="false">Tester actions</string>
<string name="hide_all_currencies" translatable="false">Hide all currencies</string>
<string name="toggle_app_theme" translatable="false">Toggle app theme - %s</string>