Updated on 2026-08-14

This commit is contained in:
Tangem 2026-04-09 16:33:29 +04:00
parent 2e431b9261
commit 9ef791c432
9 changed files with 152 additions and 269 deletions

View file

@ -2,71 +2,59 @@ package com.tangem.tap.features.details.ui.appsettings
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.extensions.wrappedList
import com.tangem.domain.apptheme.model.AppThemeMode
import com.tangem.tap.features.details.ui.appsettings.AppSettingsScreenState.Dialog
import com.tangem.core.ui.message.DialogMessage
import com.tangem.core.ui.message.EventMessageAction
import com.tangem.wallet.R
import kotlinx.collections.immutable.toImmutableList
internal class AppSettingsDialogsFactory {
fun createThemeModeSelectorDialog(
selectedModeIndex: Int,
onSelect: (AppThemeMode) -> Unit,
onDismiss: () -> Unit,
): Dialog.Selector {
val modes = AppThemeMode.available
return Dialog.Selector(
title = resourceReference(R.string.app_settings_theme_selector_title),
selectedItemIndex = selectedModeIndex,
items = modes.map { mode ->
resourceReference(
id = when (mode) {
AppThemeMode.FORCE_DARK -> R.string.app_settings_theme_mode_dark
AppThemeMode.FORCE_LIGHT -> R.string.app_settings_theme_mode_light
AppThemeMode.FOLLOW_SYSTEM -> R.string.app_settings_theme_mode_system
},
)
}.toImmutableList(),
onSelect = { index ->
val mode = AppThemeMode.available[index]
onSelect(mode)
},
onDismiss = onDismiss,
)
}
fun createDisableBiometricAuthenticationAlert(onDisable: () -> Unit, onDismiss: () -> Unit): Dialog.Alert {
return Dialog.Alert(
fun createDisableBiometricAuthenticationAlert(onDisable: () -> Unit): DialogMessage {
return DialogMessage(
title = resourceReference(R.string.common_attention),
description = resourceReference(
message = resourceReference(
R.string.app_settings_off_biometrics_alert_message,
wrappedList(resourceReference(R.string.common_biometrics)),
),
confirmText = resourceReference(R.string.common_disable),
onConfirm = onDisable,
onDismiss = onDismiss,
isDismissable = false,
firstActionBuilder = {
EventMessageAction(
title = resourceReference(R.string.common_disable),
isWarning = true,
onClick = onDisable,
)
},
secondActionBuilder = { cancelAction() },
)
}
fun createEnableRequireAccessCodeAlert(onEnable: () -> Unit, onDismiss: () -> Unit): Dialog.Alert {
return Dialog.Alert(
fun createEnableRequireAccessCodeAlert(onEnable: () -> Unit): DialogMessage {
return DialogMessage(
title = resourceReference(R.string.common_attention),
description = resourceReference(R.string.app_settings_on_require_access_code_alert_message),
confirmText = resourceReference(R.string.common_enable),
onConfirm = { onEnable() },
onDismiss = onDismiss,
message = resourceReference(R.string.app_settings_on_require_access_code_alert_message),
isDismissable = false,
firstActionBuilder = {
EventMessageAction(
title = resourceReference(R.string.common_enable),
onClick = onEnable,
)
},
secondActionBuilder = { cancelAction() },
)
}
fun createDisableRequireAccessCodeAlert(onDisable: () -> Unit, onDismiss: () -> Unit): Dialog.Alert {
return Dialog.Alert(
fun createDisableRequireAccessCodeAlert(onDisable: () -> Unit): DialogMessage {
return DialogMessage(
title = resourceReference(R.string.common_attention),
description = resourceReference(R.string.app_settings_off_require_access_code_alert_message),
confirmText = resourceReference(R.string.common_disable),
onConfirm = { onDisable() },
onDismiss = onDismiss,
message = resourceReference(R.string.app_settings_off_require_access_code_alert_message),
isDismissable = false,
firstActionBuilder = {
EventMessageAction(
title = resourceReference(R.string.common_disable),
isWarning = true,
onClick = onDisable,
)
},
secondActionBuilder = { cancelAction() },
)
}
}

View file

@ -8,8 +8,6 @@ import androidx.compose.foundation.layout.systemBars
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.rememberUpdatedState
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.tooling.preview.Preview
@ -42,13 +40,6 @@ internal fun AppSettingsScreen(state: AppSettingsScreenState, onBackClick: () ->
@Composable
private fun AppSettings(state: AppSettingsScreenState.Content) {
val dialog by rememberUpdatedState(newValue = state.dialog)
when (val safeDialog = dialog) {
is AppSettingsScreenState.Dialog.Alert -> SettingsAlertDialog(dialog = safeDialog)
is AppSettingsScreenState.Dialog.Selector -> SettingsSelectorDialog(dialog = safeDialog)
null -> Unit
}
val bottomBarHeight = with(LocalDensity.current) { WindowInsets.systemBars.getBottom(this).toDp() }
LazyColumn(
@ -102,12 +93,7 @@ private class AppSettingsScreenStateProvider : CollectionPreviewParameterProvide
itemsFactory.createSelectThemeModeButton(AppThemeMode.DEFAULT, {}),
)
add(
AppSettingsScreenState.Content(
items = items,
dialog = null,
),
)
add(AppSettingsScreenState.Content(items = items))
},
)
// endregion Preview

View file

@ -10,10 +10,7 @@ internal sealed class AppSettingsScreenState {
object Loading : AppSettingsScreenState()
data class Content(
val items: ImmutableList<Item>,
val dialog: Dialog?,
) : AppSettingsScreenState()
data class Content(val items: ImmutableList<Item>) : AppSettingsScreenState()
@Immutable
sealed class Item {
@ -46,26 +43,4 @@ internal sealed class AppSettingsScreenState {
val onClick: () -> Unit,
) : Item()
}
@Immutable
sealed class Dialog {
abstract val onDismiss: () -> Unit
data class Alert(
val title: TextReference,
val description: TextReference,
val confirmText: TextReference,
val onConfirm: () -> Unit,
override val onDismiss: () -> Unit,
) : Dialog()
data class Selector(
val title: TextReference,
val selectedItemIndex: Int,
val items: ImmutableList<TextReference>,
val onSelect: (Int) -> Unit,
override val onDismiss: () -> Unit,
) : Dialog()
}
}

View file

@ -4,42 +4,56 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.arkivanov.decompose.extensions.compose.subscribeAsState
import com.arkivanov.decompose.router.slot.childSlot
import com.arkivanov.essenty.lifecycle.doOnResume
import com.tangem.common.routing.AppRouter
import com.tangem.core.decompose.context.AppComponentContext
import com.tangem.core.decompose.model.getOrCreateModel
import com.tangem.tap.common.extensions.dispatchNavigationAction
import com.tangem.tap.features.details.ui.appsettings.api.AppSettingsComponent
import com.tangem.tap.features.details.ui.appsettings.model.AppSettingsDialogConfig
import com.tangem.tap.features.details.ui.appsettings.model.AppSettingsModel
import com.tangem.tap.store
import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
@Suppress("UnusedPrivateMember")
internal class DefaultAppSettingsComponent @AssistedInject constructor(
@Assisted appComponentContext: AppComponentContext,
@Assisted params: Unit,
@Suppress("UnusedPrivateMember") @Assisted params: Unit,
) : AppSettingsComponent, AppComponentContext by appComponentContext {
private val model: AppSettingsModel = getOrCreateModel()
init {
private val dialogSlot = childSlot(
source = model.dialogNavigation,
serializer = AppSettingsDialogConfig.serializer(),
handleBackButton = true,
childFactory = { config, _ -> config },
)
init {
doOnResume { model.onResume() }
}
@Composable
override fun Content(modifier: Modifier) {
val state by model.uiState.collectAsStateWithLifecycle()
val dialog by dialogSlot.subscribeAsState()
AppSettingsScreen(
modifier = modifier,
state = state,
onBackClick = {
store.dispatchNavigationAction(AppRouter::pop)
},
onBackClick = model::onBackClick,
)
dialog.child?.instance?.let { config ->
when (config) {
is AppSettingsDialogConfig.ThemeModeSelector -> SettingsSelectorDialog(
config = config,
onSelect = model::onThemeModeSelected,
onDismiss = model::dismissDialog,
)
}
}
}
@AssistedFactory

View file

@ -0,0 +1,38 @@
package com.tangem.tap.features.details.ui.appsettings
import androidx.compose.runtime.Composable
import com.tangem.core.ui.components.DialogButtonUM
import com.tangem.core.ui.components.SelectorDialog
import com.tangem.core.ui.extensions.stringResourceSafe
import com.tangem.domain.apptheme.model.AppThemeMode
import com.tangem.tap.features.details.ui.appsettings.model.AppSettingsDialogConfig
import com.tangem.wallet.R
import kotlinx.collections.immutable.toImmutableList
@Composable
internal fun SettingsSelectorDialog(
config: AppSettingsDialogConfig.ThemeModeSelector,
onSelect: (Int) -> Unit,
onDismiss: () -> Unit,
) {
val modes = AppThemeMode.available
SelectorDialog(
title = stringResourceSafe(R.string.app_settings_theme_selector_title),
selectedItemIndex = config.selectedModeIndex,
items = modes.map { mode ->
stringResourceSafe(
id = when (mode) {
AppThemeMode.FORCE_DARK -> R.string.app_settings_theme_mode_dark
AppThemeMode.FORCE_LIGHT -> R.string.app_settings_theme_mode_light
AppThemeMode.FOLLOW_SYSTEM -> R.string.app_settings_theme_mode_system
},
)
}.toImmutableList(),
confirmButton = DialogButtonUM(
title = stringResourceSafe(R.string.common_cancel),
onClick = onDismiss,
),
onSelect = onSelect,
onDismissDialog = onDismiss,
)
}

View file

@ -1,54 +0,0 @@
package com.tangem.tap.features.details.ui.appsettings.components
import android.content.res.Configuration
import androidx.compose.runtime.Composable
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider
import com.tangem.core.ui.components.BasicDialog
import com.tangem.core.ui.components.DialogButtonUM
import com.tangem.core.ui.extensions.resolveReference
import com.tangem.core.ui.extensions.stringResourceSafe
import com.tangem.core.ui.res.TangemThemePreview
import com.tangem.tap.features.details.ui.appsettings.AppSettingsDialogsFactory
import com.tangem.tap.features.details.ui.appsettings.AppSettingsScreenState.Dialog
import com.tangem.wallet.R
@Composable
internal fun SettingsAlertDialog(dialog: Dialog.Alert) {
BasicDialog(
title = dialog.title.resolveReference(),
message = dialog.description.resolveReference(),
isDismissable = false,
confirmButton = DialogButtonUM(
title = dialog.confirmText.resolveReference(),
isWarning = true,
onClick = dialog.onConfirm,
),
dismissButton = DialogButtonUM(
title = stringResourceSafe(id = R.string.common_cancel),
onClick = dialog.onDismiss,
),
onDismissDialog = dialog.onDismiss,
)
}
// region Preview
@Preview(showBackground = true, widthDp = 360)
@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES)
@Composable
private fun AlertDialogPreview(@PreviewParameter(AlertDialogProvider::class) dialog: Dialog.Alert) {
TangemThemePreview {
SettingsAlertDialog(dialog = dialog)
}
}
private class AlertDialogProvider : CollectionPreviewParameterProvider<Dialog>(
collection = buildList {
val dialogsFactory = AppSettingsDialogsFactory()
add(dialogsFactory.createThemeModeSelectorDialog(selectedModeIndex = 0, onSelect = {}, onDismiss = {}))
add(dialogsFactory.createDisableBiometricAuthenticationAlert(onDisable = {}, onDismiss = {}))
},
)
// endregion Preview

View file

@ -1,52 +0,0 @@
package com.tangem.tap.features.details.ui.appsettings.components
import android.content.res.Configuration
import androidx.compose.runtime.Composable
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider
import com.tangem.core.ui.components.DialogButtonUM
import com.tangem.core.ui.components.SelectorDialog
import com.tangem.core.ui.extensions.resolveReference
import com.tangem.core.ui.extensions.stringResourceSafe
import com.tangem.core.ui.res.TangemThemePreview
import com.tangem.tap.features.details.ui.appsettings.AppSettingsDialogsFactory
import com.tangem.tap.features.details.ui.appsettings.AppSettingsScreenState.Dialog
import com.tangem.wallet.R
import kotlinx.collections.immutable.toImmutableList
@Composable
internal fun SettingsSelectorDialog(dialog: Dialog.Selector) {
SelectorDialog(
title = dialog.title.resolveReference(),
selectedItemIndex = dialog.selectedItemIndex,
items = dialog.items.map { it.resolveReference() }.toImmutableList(),
confirmButton = DialogButtonUM(
title = stringResourceSafe(R.string.common_cancel),
onClick = dialog.onDismiss,
),
onSelect = dialog.onSelect,
onDismissDialog = dialog.onDismiss,
)
}
// region Preview
@Preview(showBackground = true, widthDp = 360)
@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES)
@Composable
private fun SettingsSelectorDialogPreview(@PreviewParameter(DialogProvider::class) param: Dialog.Selector) {
TangemThemePreview {
SettingsSelectorDialog(param)
}
}
private class DialogProvider : CollectionPreviewParameterProvider<Dialog.Selector>(
collection = listOf(
AppSettingsDialogsFactory().createThemeModeSelectorDialog(
selectedModeIndex = 0,
onSelect = {},
onDismiss = {},
),
),
)
// endregion Preview

View file

@ -0,0 +1,10 @@
package com.tangem.tap.features.details.ui.appsettings.model
import kotlinx.serialization.Serializable
@Serializable
internal sealed interface AppSettingsDialogConfig {
@Serializable
data class ThemeModeSelector(val selectedModeIndex: Int) : AppSettingsDialogConfig
}

View file

@ -1,6 +1,9 @@
package com.tangem.tap.features.details.ui.appsettings.model
import androidx.compose.runtime.Stable
import com.arkivanov.decompose.router.slot.SlotNavigation
import com.arkivanov.decompose.router.slot.activate
import com.arkivanov.decompose.router.slot.dismiss
import com.tangem.common.doOnFailure
import com.tangem.common.doOnSuccess
import com.tangem.common.routing.AppRoute
@ -67,6 +70,8 @@ internal class AppSettingsModel @Inject constructor(
private val itemsFactory = AppSettingsItemsFactory()
private val dialogsFactory = AppSettingsDialogsFactory()
val dialogNavigation: SlotNavigation<AppSettingsDialogConfig> = SlotNavigation()
private val localState = MutableStateFlow(LocalState())
private val biometricsStatusJobHolder = JobHolder()
@ -94,10 +99,7 @@ internal class AppSettingsModel @Inject constructor(
uiState.update { prevState ->
when (prevState) {
is AppSettingsScreenState.Content -> prevState.copy(items = items)
is AppSettingsScreenState.Loading -> AppSettingsScreenState.Content(
items = items,
dialog = null,
)
is AppSettingsScreenState.Loading -> AppSettingsScreenState.Content(items = items)
}
}
}
@ -188,28 +190,31 @@ internal class AppSettingsModel @Inject constructor(
settingsManager.openBiometricSettings()
}
fun onBackClick() {
router.pop()
}
private fun showAppCurrencySelector() {
router.push(AppRoute.AppCurrencySelector)
}
private fun showThemeModeSelector(selectedMode: AppThemeMode) {
updateContentState {
copy(
dialog = dialogsFactory.createThemeModeSelectorDialog(
selectedModeIndex = selectedMode.ordinal,
onSelect = { mode ->
analyticsEventHandler.send(
event = Settings.AppSettings.ThemeSwitched(
theme = AnalyticsParam.AppTheme.fromAppThemeMode(mode),
),
)
changeAppThemeMode(mode)
dismissDialog()
},
onDismiss = ::dismissDialog,
),
)
}
dialogNavigation.activate(AppSettingsDialogConfig.ThemeModeSelector(selectedMode.ordinal))
}
fun onThemeModeSelected(index: Int) {
val mode = AppThemeMode.available[index]
analyticsEventHandler.send(
event = Settings.AppSettings.ThemeSwitched(
theme = AnalyticsParam.AppTheme.fromAppThemeMode(mode),
),
)
changeAppThemeMode(mode)
dialogNavigation.dismiss()
}
fun dismissDialog() {
dialogNavigation.dismiss()
}
private fun onBiometricAuthenticationToggled(isChecked: Boolean) {
@ -219,17 +224,11 @@ internal class AppSettingsModel @Inject constructor(
if (isChecked) {
toggleBiometricsAuthentication(enable = true)
} else {
updateContentState {
copy(
dialog = dialogsFactory.createDisableBiometricAuthenticationAlert(
onDisable = {
toggleBiometricsAuthentication(enable = false)
dismissDialog()
},
onDismiss = ::dismissDialog,
),
)
}
uiMessageSender.send(
dialogsFactory.createDisableBiometricAuthenticationAlert(
onDisable = { toggleBiometricsAuthentication(enable = false) },
),
)
}
}
@ -241,25 +240,17 @@ internal class AppSettingsModel @Inject constructor(
// TODO : Uncomment and implement analytics event when ready
// val param = AnalyticsParam.OnOffState(isChecked)
// analyticsEventHandler.send(Settings.AppSettings.RequireAccessCodeChanged(param))
updateContentState {
copy(
dialog = if (isChecked) {
dialogsFactory.createEnableRequireAccessCodeAlert(
onEnable = {
toggleRequireAccessCode(enable = true)
dismissDialog()
},
onDismiss = ::dismissDialog,
)
} else {
dialogsFactory.createDisableRequireAccessCodeAlert(
onDisable = {
toggleRequireAccessCode(enable = false)
dismissDialog()
},
onDismiss = ::dismissDialog,
)
},
if (isChecked) {
uiMessageSender.send(
dialogsFactory.createEnableRequireAccessCodeAlert(
onEnable = { toggleRequireAccessCode(enable = true) },
),
)
} else {
uiMessageSender.send(
dialogsFactory.createDisableRequireAccessCodeAlert(
onDisable = { toggleRequireAccessCode(enable = false) },
),
)
}
}
@ -371,10 +362,6 @@ internal class AppSettingsModel @Inject constructor(
}
}
private fun dismissDialog() {
updateContentState { copy(dialog = null) }
}
private fun bootstrapLocalState() = modelScope.launch {
localState.update { state ->
state.copy(
@ -394,15 +381,6 @@ internal class AppSettingsModel @Inject constructor(
.launchIn(modelScope)
}
private fun updateContentState(block: AppSettingsScreenState.Content.() -> AppSettingsScreenState.Content) {
uiState.update { prevState ->
when (prevState) {
is AppSettingsScreenState.Content -> block(prevState)
is AppSettingsScreenState.Loading -> prevState
}
}
}
private data class LocalState(
val hasSecuredWallets: Boolean = false,
val isEnrollBiometricsNeeded: Boolean = false,