Updated on 2026-08-14

This commit is contained in:
Tangem 2023-09-07 10:51:42 +03:00
parent f289bbab97
commit 93b0b05e69
25 changed files with 494 additions and 132 deletions

View file

@ -23,6 +23,7 @@ import com.tangem.datasource.config.FeaturesLocalLoader
import com.tangem.datasource.config.models.Config
import com.tangem.datasource.connection.NetworkConnectionManager
import com.tangem.domain.appcurrency.repository.AppCurrencyRepository
import com.tangem.domain.apptheme.repository.AppThemeModeRepository
import com.tangem.domain.card.ScanCardProcessor
import com.tangem.domain.common.LogConfig
import com.tangem.domain.tokens.repository.CurrenciesRepository
@ -173,6 +174,9 @@ class TapApplication : Application(), ImageLoaderFactory {
@Inject
lateinit var currenciesRepository: CurrenciesRepository
@Inject
lateinit var appThemeModeRepository: AppThemeModeRepository
override fun onCreate() {
super.onCreate()
@ -195,6 +199,7 @@ class TapApplication : Application(), ImageLoaderFactory {
walletManagersFacade = walletManagersFacade,
appStateHolder = appStateHolder,
currenciesRepository = currenciesRepository,
appThemeModeRepository = appThemeModeRepository,
),
),
)

View file

@ -6,6 +6,7 @@ import com.tangem.common.CompletionResult
import com.tangem.common.core.TangemError
import com.tangem.datasource.config.ConfigManager
import com.tangem.datasource.config.models.ChatConfig
import com.tangem.domain.apptheme.model.AppThemeMode
import com.tangem.domain.models.scan.ScanResponse
import com.tangem.domain.wallets.legacy.UserWalletsListManager
import com.tangem.tap.common.analytics.topup.TopUpController
@ -103,4 +104,5 @@ sealed class GlobalAction : Action {
}
data class UpdateUserWalletsListManager(val manager: UserWalletsListManager) : GlobalAction()
data class ChangeAppThemeMode(val appThemeMode: AppThemeMode) : GlobalAction()
}

View file

@ -91,6 +91,9 @@ fun globalReducer(action: Action, state: AppState, appStateHolder: AppStateHolde
userWalletsListManager = action.manager,
)
}
is GlobalAction.ChangeAppThemeMode -> globalState.copy(
appThemeMode = action.appThemeMode,
)
else -> globalState
}
}

View file

@ -1,6 +1,7 @@
package com.tangem.tap.common.redux.global
import com.tangem.datasource.config.ConfigManager
import com.tangem.domain.apptheme.model.AppThemeMode
import com.tangem.domain.models.scan.ScanResponse
import com.tangem.domain.wallets.legacy.UserWalletsListManager
import com.tangem.tap.common.analytics.topup.TopUpController
@ -29,6 +30,7 @@ data class GlobalState(
val userCountryCode: String? = null,
val userWalletsListManager: UserWalletsListManager? = null,
val topUpController: TopUpController? = null,
val appThemeMode: AppThemeMode = AppThemeMode.DEFAULT,
) : StateType
typealias CryptoCurrencyName = String

View file

@ -1,5 +1,6 @@
package com.tangem.tap.features.details.redux
import com.tangem.domain.apptheme.model.AppThemeMode
import com.tangem.domain.common.CardTypesResolver
import com.tangem.domain.models.scan.CardDTO
import com.tangem.domain.models.scan.ScanResponse
@ -68,6 +69,10 @@ sealed class DetailsAction : Action {
data class BiometricsStatusChanged(
val needEnrollBiometrics: Boolean,
) : AppSettings()
data class ChangeAppThemeMode(
val appThemeMode: AppThemeMode,
) : AppSettings()
}
data class ChangeAppCurrency(val fiatCurrency: FiatCurrency) : DetailsAction()

View file

@ -9,6 +9,7 @@ import com.tangem.common.flatMap
import com.tangem.core.analytics.Analytics
import com.tangem.core.navigation.AppScreen
import com.tangem.core.navigation.NavigationAction
import com.tangem.domain.apptheme.model.AppThemeMode
import com.tangem.domain.common.TapWorkarounds.isTangemTwins
import com.tangem.domain.common.util.cardTypesResolver
import com.tangem.domain.models.scan.ScanResponse
@ -221,6 +222,9 @@ class DetailsMiddleware {
is DetailsAction.AppSettings.EnrollBiometrics -> {
enrollBiometrics()
}
is DetailsAction.AppSettings.ChangeAppThemeMode -> {
changeAppThemeMode(action.appThemeMode)
}
is DetailsAction.AppSettings.SwitchPrivacySetting.Success,
is DetailsAction.AppSettings.SwitchPrivacySetting.Failure,
is DetailsAction.AppSettings.BiometricsStatusChanged,
@ -252,6 +256,14 @@ class DetailsMiddleware {
store.dispatchOnMain(NavigationAction.OpenBiometricsSettings)
}
private fun changeAppThemeMode(appThemeMode: AppThemeMode) {
val repository = store.state.daggerGraphState.get(DaggerGraphState::appThemeModeRepository)
scope.launch {
repository.changeAppThemeMode(appThemeMode)
}
}
private fun toggleSaveWallets(state: DetailsState, enable: Boolean) = scope.launch {
// Nothing to change
if (preferencesStorage.shouldSaveUserWallets == enable) {

View file

@ -58,6 +58,7 @@ private fun handlePrepareScreen(action: DetailsAction.PrepareScreen): DetailsSta
saveWallets = preferencesStorage.shouldSaveUserWallets,
saveAccessCodes = preferencesStorage.shouldSaveAccessCodes,
selectedFiatCurrency = store.state.globalState.appCurrency,
selectedThemeMode = store.state.globalState.appThemeMode,
),
)
}
@ -196,6 +197,11 @@ private fun handlePrivacyAction(action: DetailsAction.AppSettings, state: Detail
needEnrollBiometrics = action.needEnrollBiometrics,
),
)
is DetailsAction.AppSettings.ChangeAppThemeMode -> state.copy(
appSettingsState = state.appSettingsState.copy(
selectedThemeMode = action.appThemeMode,
),
)
is DetailsAction.AppSettings.EnrollBiometrics,
is DetailsAction.AppSettings.CheckBiometricsStatus,
-> state

View file

@ -1,5 +1,6 @@
package com.tangem.tap.features.details.redux
import com.tangem.domain.apptheme.model.AppThemeMode
import com.tangem.domain.models.scan.CardDTO
import com.tangem.domain.models.scan.ScanResponse
import com.tangem.tap.common.entities.Button
@ -55,6 +56,7 @@ data class AppSettingsState(
val needEnrollBiometrics: Boolean = false,
val isInProgress: Boolean = false,
val selectedFiatCurrency: FiatCurrency = FiatCurrency.Default,
val selectedThemeMode: AppThemeMode = AppThemeMode.DEFAULT,
)
enum class SecurityOption { LongTap, PassCode, AccessCode }

View file

@ -1,28 +0,0 @@
package com.tangem.tap.features.details.ui.appsettings
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.tap.features.details.ui.appsettings.AppSettingsScreenState.Alert
import com.tangem.wallet.R
internal class AppSettingsAlertsFactory {
fun createDeleteSavedWalletsAlert(onDelete: () -> Unit, onDismiss: () -> Unit): Alert {
return Alert(
title = resourceReference(R.string.common_attention),
description = resourceReference(R.string.app_settings_off_saved_wallet_alert_message),
confirmText = resourceReference(R.string.common_delete),
onConfirm = onDelete,
onDismiss = onDismiss,
)
}
fun createDeleteSavedAccessCodesAlert(onDelete: () -> Unit, onDismiss: () -> Unit): Alert {
return Alert(
title = resourceReference(R.string.common_attention),
description = resourceReference(R.string.app_settings_off_saved_access_code_alert_message),
confirmText = resourceReference(R.string.common_delete),
onConfirm = onDelete,
onDismiss = onDismiss,
)
}
}

View file

@ -0,0 +1,58 @@
package com.tangem.tap.features.details.ui.appsettings
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.domain.apptheme.model.AppThemeMode
import com.tangem.tap.features.details.ui.appsettings.AppSettingsScreenState.Dialog
import com.tangem.wallet.R
import kotlinx.collections.immutable.toImmutableList
internal class AppSettingsDialogsFactory {
fun createDeleteSavedWalletsAlert(onDelete: () -> Unit, onDismiss: () -> Unit): Dialog.Alert {
return Dialog.Alert(
title = resourceReference(R.string.common_attention),
description = resourceReference(R.string.app_settings_off_saved_wallet_alert_message),
confirmText = resourceReference(R.string.common_delete),
onConfirm = onDelete,
onDismiss = onDismiss,
)
}
fun createDeleteSavedAccessCodesAlert(onDelete: () -> Unit, onDismiss: () -> Unit): Dialog.Alert {
return Dialog.Alert(
title = resourceReference(R.string.common_attention),
description = resourceReference(R.string.app_settings_off_saved_access_code_alert_message),
confirmText = resourceReference(R.string.common_delete),
onConfirm = onDelete,
onDismiss = onDismiss,
)
}
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,
)
}
}

View file

@ -2,6 +2,7 @@ package com.tangem.tap.features.details.ui.appsettings
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.extensions.stringReference
import com.tangem.domain.apptheme.model.AppThemeMode
import com.tangem.tap.features.details.ui.appsettings.AppSettingsScreenState.Item
import com.tangem.wallet.R
@ -56,4 +57,20 @@ internal class AppSettingsItemsFactory {
onClick = onClick,
)
}
fun createSelectThemeModeButton(currentThemeMode: AppThemeMode, onClick: () -> Unit): Item.Button {
return Item.Button(
id = "select_theme_mode_button",
title = resourceReference(R.string.app_settings_theme_selector_title),
description = resourceReference(
id = when (currentThemeMode) {
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
},
),
isEnabled = true,
onClick = onClick,
)
}
}

View file

@ -11,11 +11,9 @@ 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.res.TangemTheme
import com.tangem.domain.apptheme.model.AppThemeMode
import com.tangem.tap.features.details.ui.appsettings.AppSettingsScreenState.Item
import com.tangem.tap.features.details.ui.appsettings.components.ButtonItem
import com.tangem.tap.features.details.ui.appsettings.components.CardItem
import com.tangem.tap.features.details.ui.appsettings.components.SettingsAlertDialog
import com.tangem.tap.features.details.ui.appsettings.components.SwitchItem
import com.tangem.tap.features.details.ui.appsettings.components.*
import com.tangem.tap.features.details.ui.common.SettingsScreensScaffold
import com.tangem.wallet.R
import kotlinx.collections.immutable.persistentListOf
@ -37,9 +35,11 @@ internal fun AppSettingsScreen(state: AppSettingsScreenState, onBackClick: () ->
@Composable
private fun AppSettings(state: AppSettingsScreenState.Content) {
val alert by rememberUpdatedState(newValue = state.alert)
alert?.let { safeAlert ->
SettingsAlertDialog(alert = safeAlert)
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
}
LazyColumn {
@ -48,15 +48,15 @@ private fun AppSettings(state: AppSettingsScreenState.Content) {
key = Item::id,
) { item ->
when (item) {
is Item.Card -> CardItem(
is Item.Card -> SettingsCardItem(
modifier = Modifier.padding(horizontal = TangemTheme.dimens.spacing16),
item = item,
)
is Item.Button -> ButtonItem(
is Item.Button -> SettingsButtonItem(
modifier = Modifier.padding(vertical = TangemTheme.dimens.spacing8),
item = item,
)
is Item.Switch -> SwitchItem(
is Item.Switch -> SettingsSwitchItem(
modifier = Modifier.padding(
vertical = TangemTheme.dimens.spacing16,
horizontal = TangemTheme.dimens.spacing20,
@ -92,27 +92,17 @@ private fun AppSettingsScreenPreview_Dark(
private class AppSettingsScreenStateProvider : CollectionPreviewParameterProvider<AppSettingsScreenState>(
collection = buildList {
val itemsFactory = AppSettingsItemsFactory()
val dialogsFactory = AppSettingsAlertsFactory()
val items = persistentListOf(
itemsFactory.createEnrollBiometricsCard {},
itemsFactory.createSelectAppCurrencyButton(currentAppCurrencyName = "US Dollar") {},
itemsFactory.createSaveWalletsSwitch(isChecked = true, isEnabled = true, { _ -> }),
itemsFactory.createSaveAccessCodeSwitch(isChecked = false, isEnabled = true) { _ -> },
itemsFactory.createSelectThemeModeButton(AppThemeMode.DEFAULT, {}),
)
AppSettingsScreenState.Content(
items = items,
alert = null,
).let(::add)
AppSettingsScreenState.Content(
items = items,
alert = dialogsFactory.createDeleteSavedWalletsAlert({}, {}),
).let(::add)
AppSettingsScreenState.Content(
items = items,
alert = dialogsFactory.createDeleteSavedAccessCodesAlert({}, {}),
dialog = null,
).let(::add)
},
)

View file

@ -12,7 +12,7 @@ internal sealed class AppSettingsScreenState {
data class Content(
val items: ImmutableList<Item>,
val alert: Alert?,
val dialog: Dialog?,
) : AppSettingsScreenState()
@Immutable
@ -46,11 +46,25 @@ internal sealed class AppSettingsScreenState {
) : Item()
}
data class Alert(
val title: TextReference,
val description: TextReference,
val confirmText: TextReference,
val onConfirm: () -> Unit,
val onDismiss: () -> Unit,
)
@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

@ -3,6 +3,7 @@ package com.tangem.tap.features.details.ui.appsettings
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import com.tangem.domain.apptheme.model.AppThemeMode
import com.tangem.tap.common.extensions.dispatchOnMain
import com.tangem.tap.common.redux.AppState
import com.tangem.tap.features.details.redux.AppSetting
@ -17,7 +18,7 @@ import org.rekotlin.Store
internal class AppSettingsViewModel(private val store: Store<AppState>) {
private val itemsFactory = AppSettingsItemsFactory()
private val alertsFactory = AppSettingsAlertsFactory()
private val dialogsFactory = AppSettingsDialogsFactory()
var uiState: AppSettingsScreenState by mutableStateOf(AppSettingsScreenState.Loading)
private set
@ -25,7 +26,7 @@ internal class AppSettingsViewModel(private val store: Store<AppState>) {
fun updateState(state: DetailsState) {
uiState = AppSettingsScreenState.Content(
items = buildItems(state.appSettingsState),
alert = (uiState as? AppSettingsScreenState.Content)?.alert,
dialog = (uiState as? AppSettingsScreenState.Content)?.dialog,
)
}
@ -63,6 +64,10 @@ internal class AppSettingsViewModel(private val store: Store<AppState>) {
onCheckedChange = ::onSaveAccessCodesToggled,
).let(::add)
}
itemsFactory.createSelectThemeModeButton(state.selectedThemeMode) {
showThemeModeSelector(state.selectedThemeMode)
}.let(::add)
}
return items.toImmutableList()
@ -76,13 +81,28 @@ internal class AppSettingsViewModel(private val store: Store<AppState>) {
store.dispatchOnMain(WalletAction.AppCurrencyAction.ChooseAppCurrency)
}
private fun showThemeModeSelector(selectedMode: AppThemeMode) {
updateContentState {
copy(
dialog = dialogsFactory.createThemeModeSelectorDialog(
selectedModeIndex = selectedMode.ordinal,
onSelect = { mode ->
store.dispatchOnMain(DetailsAction.AppSettings.ChangeAppThemeMode(mode))
dismissDialog()
},
onDismiss = ::dismissDialog,
),
)
}
}
private fun onSaveWalletsToggled(isChecked: Boolean) {
if (isChecked) {
onSettingsToggled(AppSetting.SaveWallets, enable = true)
} else {
updateContentState {
copy(
alert = alertsFactory.createDeleteSavedWalletsAlert(
dialog = dialogsFactory.createDeleteSavedWalletsAlert(
onDelete = {
onSettingsToggled(AppSetting.SaveWallets, enable = false)
dismissDialog()
@ -100,7 +120,7 @@ internal class AppSettingsViewModel(private val store: Store<AppState>) {
} else {
updateContentState {
copy(
alert = alertsFactory.createDeleteSavedAccessCodesAlert(
dialog = dialogsFactory.createDeleteSavedAccessCodesAlert(
onDelete = {
onSettingsToggled(AppSetting.SaveAccessCode, enable = false)
dismissDialog()
@ -117,7 +137,7 @@ internal class AppSettingsViewModel(private val store: Store<AppState>) {
}
private fun dismissDialog() {
updateContentState { copy(alert = null) }
updateContentState { copy(dialog = null) }
}
private fun updateContentState(block: AppSettingsScreenState.Content.() -> AppSettingsScreenState.Content) {

View file

@ -9,52 +9,52 @@ import com.tangem.core.ui.components.BasicDialog
import com.tangem.core.ui.components.DialogButton
import com.tangem.core.ui.extensions.resolveReference
import com.tangem.core.ui.res.TangemTheme
import com.tangem.tap.features.details.ui.appsettings.AppSettingsAlertsFactory
import com.tangem.tap.features.details.ui.appsettings.AppSettingsScreenState.Alert
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(alert: Alert) {
internal fun SettingsAlertDialog(dialog: Dialog.Alert) {
BasicDialog(
title = alert.title.resolveReference(),
message = alert.description.resolveReference(),
title = dialog.title.resolveReference(),
message = dialog.description.resolveReference(),
isDismissable = false,
confirmButton = DialogButton(
title = alert.confirmText.resolveReference(),
title = dialog.confirmText.resolveReference(),
warning = true,
onClick = alert.onConfirm,
onClick = dialog.onConfirm,
),
dismissButton = DialogButton(
title = stringResource(id = R.string.common_cancel),
onClick = alert.onDismiss,
onClick = dialog.onDismiss,
),
onDismissDialog = alert.onDismiss,
onDismissDialog = dialog.onDismiss,
)
}
// region Preview
@Preview(showBackground = true, widthDp = 360)
@Composable
private fun AlertDialogPreview_Light(@PreviewParameter(AlertDialogProvider::class) dialog: Alert) {
private fun AlertDialogPreview_Light(@PreviewParameter(AlertDialogProvider::class) dialog: Dialog.Alert) {
TangemTheme {
SettingsAlertDialog(alert = dialog)
SettingsAlertDialog(dialog = dialog)
}
}
@Preview(showBackground = true, widthDp = 360)
@Composable
private fun AlertDialogPreview_Dark(@PreviewParameter(AlertDialogProvider::class) dialog: Alert) {
private fun AlertDialogPreview_Dark(@PreviewParameter(AlertDialogProvider::class) dialog: Dialog.Alert) {
TangemTheme(isDark = true) {
SettingsAlertDialog(alert = dialog)
SettingsAlertDialog(dialog = dialog)
}
}
private class AlertDialogProvider : CollectionPreviewParameterProvider<Alert>(
private class AlertDialogProvider : CollectionPreviewParameterProvider<Dialog.Alert>(
collection = buildList {
val alertsFactory = AppSettingsAlertsFactory()
val dialogsFactory = AppSettingsDialogsFactory()
alertsFactory.createDeleteSavedAccessCodesAlert({}, {}).let(::add)
alertsFactory.createDeleteSavedWalletsAlert({}, {}).let(::add)
dialogsFactory.createDeleteSavedAccessCodesAlert({}, {}).let(::add)
dialogsFactory.createDeleteSavedWalletsAlert({}, {}).let(::add)
},
)
// endregion Preview

View file

@ -20,7 +20,7 @@ import com.tangem.tap.features.details.ui.appsettings.AppSettingsScreenState.Ite
@OptIn(ExperimentalMaterialApi::class)
@Composable
internal fun ButtonItem(item: Item.Button, modifier: Modifier = Modifier) {
internal fun SettingsButtonItem(item: Item.Button, modifier: Modifier = Modifier) {
Surface(
modifier = modifier.fillMaxWidth(),
color = TangemTheme.colors.background.secondary,
@ -55,7 +55,7 @@ internal fun ButtonItem(item: Item.Button, modifier: Modifier = Modifier) {
@Composable
private fun ButtonItemPreview_Light(@PreviewParameter(ButtonItemProvider::class) item: Item.Button) {
TangemTheme {
ButtonItem(item = item)
SettingsButtonItem(item = item)
}
}
@ -63,7 +63,7 @@ private fun ButtonItemPreview_Light(@PreviewParameter(ButtonItemProvider::class)
@Composable
private fun ButtonItemPreview_Dark(@PreviewParameter(ButtonItemProvider::class) item: Item.Button) {
TangemTheme(isDark = true) {
ButtonItem(item = item)
SettingsButtonItem(item = item)
}
}

View file

@ -22,7 +22,7 @@ import com.tangem.tap.features.details.ui.appsettings.AppSettingsScreenState.Ite
@OptIn(ExperimentalMaterialApi::class)
@Composable
internal fun CardItem(item: Item.Card, modifier: Modifier = Modifier) {
internal fun SettingsCardItem(item: Item.Card, modifier: Modifier = Modifier) {
Surface(
modifier = modifier.fillMaxWidth(),
color = TangemTheme.colors.button.disabled,
@ -62,7 +62,7 @@ internal fun CardItem(item: Item.Card, modifier: Modifier = Modifier) {
@Composable
private fun CardItemPreview_Light(@PreviewParameter(CardItemProvider::class) item: Item.Card) {
TangemTheme {
CardItem(item = item)
SettingsCardItem(item = item)
}
}
@ -70,7 +70,7 @@ private fun CardItemPreview_Light(@PreviewParameter(CardItemProvider::class) ite
@Composable
private fun CardItemPreview_Dark(@PreviewParameter(CardItemProvider::class) item: Item.Card) {
TangemTheme(isDark = true) {
CardItem(item = item)
SettingsCardItem(item = item)
}
}

View file

@ -0,0 +1,58 @@
package com.tangem.tap.features.details.ui.appsettings.components
import androidx.compose.runtime.Composable
import androidx.compose.ui.res.stringResource
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.DialogButton
import com.tangem.core.ui.components.SelectorDialog
import com.tangem.core.ui.extensions.resolveReference
import com.tangem.core.ui.res.TangemTheme
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 = DialogButton(
title = stringResource(R.string.common_cancel),
onClick = dialog.onDismiss,
),
onSelect = dialog.onSelect,
onDismissDialog = dialog.onDismiss,
)
}
// region Preview
@Preview(showBackground = true, widthDp = 360)
@Composable
private fun SettingsSelectorDialogPreview_Light(@PreviewParameter(DialogProvider::class) param: Dialog.Selector) {
TangemTheme(isDark = false) {
SettingsSelectorDialog(param)
}
}
@Preview(showBackground = true, widthDp = 360)
@Composable
private fun SettingsSelectorDialogPreview_Dark(@PreviewParameter(DialogProvider::class) param: Dialog.Selector) {
TangemTheme(isDark = true) {
SettingsSelectorDialog(param)
}
}
private class DialogProvider : CollectionPreviewParameterProvider<Dialog.Selector>(
collection = listOf(
AppSettingsDialogsFactory().createThemeModeSelectorDialog(
selectedModeIndex = 0,
onSelect = {},
onDismiss = {},
),
),
)
// endregion Preview

View file

@ -21,7 +21,7 @@ import com.tangem.tap.features.details.ui.appsettings.AppSettingsScreenState.Ite
import com.tangem.tap.features.details.ui.common.TangemSwitch
@Composable
internal fun SwitchItem(item: Item.Switch, modifier: Modifier = Modifier) {
internal fun SettingsSwitchItem(item: Item.Switch, modifier: Modifier = Modifier) {
val titleTextColor by rememberUpdatedState(
newValue = if (item.isEnabled) {
TangemTheme.colors.text.primary1
@ -72,7 +72,7 @@ internal fun SwitchItem(item: Item.Switch, modifier: Modifier = Modifier) {
@Composable
private fun SwitchItemPreview_Light(@PreviewParameter(SwitchItemProvider::class) item: Item.Switch) {
TangemTheme {
SwitchItem(item = item)
SettingsSwitchItem(item = item)
}
}
@ -80,7 +80,7 @@ private fun SwitchItemPreview_Light(@PreviewParameter(SwitchItemProvider::class)
@Composable
private fun SwitchItemPreview_Dark(@PreviewParameter(SwitchItemProvider::class) item: Item.Switch) {
TangemTheme(isDark = true) {
SwitchItem(item = item)
SettingsSwitchItem(item = item)
}
}

View file

@ -15,7 +15,7 @@ object ResetBackupCardDialog {
setPositiveButton(R.string.common_cancel) { _, _ ->
/* no-op */
}
setNegativeButton(R.string.common_reset) { _, _ ->
setNegativeButton(R.string.card_settings_action_sheet_reset) { _, _ ->
store.dispatch(BackupAction.ResetBackupCard(cardId))
}
setOnDismissListener {

View file

@ -3,6 +3,7 @@ package com.tangem.tap.proxy.redux
import com.tangem.datasource.asset.AssetReader
import com.tangem.datasource.connection.NetworkConnectionManager
import com.tangem.domain.appcurrency.repository.AppCurrencyRepository
import com.tangem.domain.apptheme.repository.AppThemeModeRepository
import com.tangem.domain.card.ScanCardProcessor
import com.tangem.domain.card.ScanCardUseCase
import com.tangem.domain.card.repository.CardSdkConfigRepository
@ -38,6 +39,7 @@ data class DaggerGraphState(
val appCurrencyRepository: AppCurrencyRepository? = null,
val walletManagersFacade: WalletManagersFacade? = null,
val appStateHolder: AppStateHolder? = null,
val appThemeModeRepository: AppThemeModeRepository? = null,
// FIXME: It is used only for TokensList screen. Remove after refactoring of TokensList
val currenciesRepository: CurrenciesRepository? = null,

View file

@ -28,11 +28,16 @@
<string name="app_settings_saved_access_codes_footer">Подключите функцию хранения кодов доступа от карт на телефоне в зашифрованном виде, и при работе с картой вместо кода доступа будет запрашиваться биометрическая аутентификация.</string>
<string name="app_settings_saved_wallet">Cохранение кошелька</string>
<string name="app_settings_saved_wallet_footer">Подключите функцию привязки карты в приложении, а также возможность биометрической аутентификации. Подпись транзакции все так же потребует карту.</string>
<string name="app_settings_theme_mode_dark">Тёмная</string>
<string name="app_settings_theme_mode_light">Светлая</string>
<string name="app_settings_theme_mode_system">Как в системе</string>
<string name="app_settings_theme_selector_title">Тема</string>
<string name="app_settings_title">Настройки приложения</string>
<string name="biometric_lockout_permanent_warning_description">Пожалуйста, отсканируйте карту</string>
<string name="biometric_lockout_warning_description">Пожалуйста, попробуйте снова через 30 секунд или отсканируйте карту</string>
<string name="biometric_lockout_warning_title">Слишком много попыток</string>
<string name="biometric_unavailable_warning">Вы отключили биометрическую аутентификацию на вашем телефоне и не сможете сохранять кошельки в приложении. Для сохранения кошельков, пожалуйста, включите функцию биометрической аутентификации в настройках телефона.</string>
<string name="button_start_backup_process">Начать резервное копирование</string>
<plurals name="card_label_card_count">
<item quantity="one">%d карта</item>
<item quantity="few">%d карты</item>
@ -43,6 +48,7 @@
<string name="card_settings_access_code_recovery_enabled_description">Использовать эту карту для сброса кода доступа на других картах в этом кошельке</string>
<string name="card_settings_access_code_recovery_footer">Отключить возможность сброса кода доступа на этой карте или других картах этого кошелька</string>
<string name="card_settings_access_code_recovery_title">Восстановление кода доступа</string>
<string name="card_settings_action_sheet_reset">Сбросить</string>
<string name="card_settings_action_sheet_title">Вы уверены, что хотите это сделать?</string>
<string name="card_settings_change_access_code">Смена кода доступа</string>
<string name="card_settings_change_access_code_footer">Код доступа будет изменен только на данной карте</string>
@ -96,7 +102,6 @@
<string name="common_reject">Отклонить</string>
<string name="common_reload">Перезагрузить</string>
<string name="common_rename">Переименовать</string>
<string name="common_reset">Сбросить</string>
<string name="common_save_changes">Сохранить изменения</string>
<string name="common_search">Искать</string>
<string name="common_search_tokens">Поиск токенов</string>
@ -196,7 +201,7 @@
</plurals>
<string name="main_manage_tokens">Управление токенами</string>
<string name="main_no_backup_warning_subtitle">Чтобы защитить свои активы, мы советуем вам выполнить эту процедуру</string>
<string name="main_no_backup_warning_title">Бэкап кошелька не был произведен</string>
<string name="main_no_backup_warning_title">Резервное копирование не выполнено</string>
<string name="main_page_balance">Баланс</string>
<string name="main_processing_full_amount">В сумме учтены не все монеты</string>
<string name="main_promotion_credited">1INCH токены будут зачислены на адрес вашего кошелька в сети %s в течение 48 часов</string>
@ -210,6 +215,7 @@
<item quantity="other">Вам надо сгенерировать адреса для %d новых сетей, используя вашу карту</item>
</plurals>
<string name="main_warning_missing_derivation_title">Некоторые адреса отсутствуют</string>
<string name="notification_title_not_enough_funds">Невозможно покрыть %1$s комиссию</string>
<string name="onboarding_access_code_feature_1_description">Вам необходимо установить единый код доступа для защиты всех ваших карт</string>
<string name="onboarding_access_code_feature_1_title">Защита</string>
<string name="onboarding_access_code_feature_2_description">Позже вы сможете установить индивидуальный код доступа для каждой карты</string>
@ -407,7 +413,7 @@
<string name="story_meet_pay">Расплачивайтесь</string>
<string name="story_meet_send">Отправляйте</string>
<string name="story_meet_store">Храните</string>
<string name="story_meet_title">Встречайте\nTangem</string>
<string name="story_meet_title">Встречайте Tangem</string>
<string name="story_web3_description">Обменивайте, покупайте NFT, получайте займы и делайте вклады в более чем 100 различных децентрализованных сервисах</string>
<string name="story_web3_title">Поддержка DeFi</string>
<string name="swapping_approve_information_text">Подтверждения считаются отраслевым стандартом для всех децентрализованных бирж и защищают ваш кошелек от доступа со стороны смарт-контракта без вашего разрешения. По замыслу смарт-контракты не могут получить доступ к вашим токенам, если вы не одобрите доступ со своей стороны. «Разблокируя» свои токены, вы даете смарт-контракту 1inch разрешение тратить ваши активы. Майнеры сети получают компенсацию за газ (оплачиваемый вами) за запись этого действия в блокчейне. Как только разрешение будет предоставлено, вы сможете обменять свой токен.</string>

View file

@ -28,11 +28,16 @@
<string name="app_settings_saved_access_codes_footer">Biometric authentication will be requested instead of the access code for interactions with your card.</string>
<string name="app_settings_saved_wallet">Keep the wallet in the app</string>
<string name="app_settings_saved_wallet_footer">Enable to link all the wallets to Tangem app. Biometric authentication will be required for unlocking the app. Transaction signing requires tapping your Tangem card.</string>
<string name="app_settings_theme_mode_dark">Dark</string>
<string name="app_settings_theme_mode_light">Light</string>
<string name="app_settings_theme_mode_system">System default</string>
<string name="app_settings_theme_selector_title">Theme</string>
<string name="app_settings_title">App Settings</string>
<string name="biometric_lockout_permanent_warning_description">Please scan the card</string>
<string name="biometric_lockout_warning_description">Please try again in 30 seconds or scan the card</string>
<string name="biometric_lockout_warning_title">Too many attempts</string>
<string name="biometric_unavailable_warning">You have disabled biometric authentication on your phone and will not be able to save wallets in the app. To save wallets, please enable the biometric authentication function in your phone settings.</string>
<string name="button_start_backup_process">Start backup process</string>
<plurals name="card_label_card_count">
<item quantity="one">%d card</item>
<item quantity="other">%d cards</item>
@ -41,6 +46,7 @@
<string name="card_settings_access_code_recovery_enabled_description">Allows you to use this card to reset access code on other cards in this wallet</string>
<string name="card_settings_access_code_recovery_footer">Disable the ability to reset the access code on this card or other cards in this wallet</string>
<string name="card_settings_access_code_recovery_title">Access code recovery</string>
<string name="card_settings_action_sheet_reset">Reset</string>
<string name="card_settings_action_sheet_title">Are you sure you want to do this?</string>
<string name="card_settings_change_access_code">Change Access Code</string>
<string name="card_settings_change_access_code_footer">Access code will be changed on this card only</string>
@ -95,7 +101,6 @@
<string name="common_reject">Reject</string>
<string name="common_reload">Reload</string>
<string name="common_rename">Rename</string>
<string name="common_reset">Reset</string>
<string name="common_save_changes">Save changes</string>
<string name="common_search">Search</string>
<string name="common_search_tokens">Search tokens</string>
@ -194,7 +199,7 @@
</plurals>
<string name="main_manage_tokens">Manage tokens</string>
<string name="main_no_backup_warning_subtitle">To protect your assets, we advise you to carry out this procedure</string>
<string name="main_no_backup_warning_title">Your wallet has not been backed up</string>
<string name="main_no_backup_warning_title">Your wallet hasn\'t been backed up</string>
<string name="main_page_balance">Total balance</string>
<string name="main_processing_full_amount">The amount does not include some of your funds</string>
<string name="main_promotion_credited">1INCH tokens will be credited to your %s wallet address within 48 hours</string>
@ -206,6 +211,7 @@
<item quantity="other">You need to generate addresses for %d new networks using your card</item>
</plurals>
<string name="main_warning_missing_derivation_title">Some addresses are missing</string>
<string name="notification_title_not_enough_funds">Unable to cover %1$s fee</string>
<string name="onboarding_access_code_feature_1_description">You have to set up a single access code to protect all your wallets</string>
<string name="onboarding_access_code_feature_1_title">Protect</string>
<string name="onboarding_access_code_feature_2_description">You can set up an individual access code on each card later</string>
@ -233,7 +239,7 @@
<string name="onboarding_button_skip_backup">Skip for later</string>
<string name="onboarding_button_what_does_it_mean">How does it work?</string>
<string name="onboarding_create_wallet_body">Let\'s generate all the keys on your card and create a secure wallet</string>
<string name="onboarding_create_wallet_button_create_wallet">Create a wallet</string>
<string name="onboarding_create_wallet_button_create_wallet">Create wallet</string>
<string name="onboarding_create_wallet_header">Create a wallet</string>
<string name="onboarding_create_wallet_options_button_options">Other options</string>
<string name="onboarding_create_wallet_options_message">Your keys will be securely generated inside the card. There is no seed phrase, which means nobody can export or steal it.</string>
@ -400,7 +406,7 @@
<string name="story_meet_pay">Pay</string>
<string name="story_meet_send">Send</string>
<string name="story_meet_store">Store</string>
<string name="story_meet_title">Meet\nTangem</string>
<string name="story_meet_title">Meet Tangem</string>
<string name="story_web3_description">Exchange, buy NFT\'s, make loans and deposits in more than 100 different decentralized services</string>
<string name="story_web3_title">DeFi Compatible</string>
<string name="swapping_approve_information_text">Approvals are considered an industry standard across all decentralized exchanges and protect your wallet from being accessed by a smart contract without your permission. By design, smart contracts can\'t access your tokens unless you approve access from your end. By \"unlocking\" your tokens, you are give permission to the 1inch smart contract to spend your assets. The miners of the network are compensated with a gas fee (paid by you) to record this action on the blockchain. Once permission has been granted you will be able to swap your token.</string>

View file

@ -1,44 +1,33 @@
package com.tangem.core.ui.components
import androidx.compose.foundation.LocalIndication
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.itemsIndexed
import androidx.compose.material.RadioButton
import androidx.compose.material.RadioButtonDefaults
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.Immutable
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.input.TextFieldValue
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider
import androidx.compose.ui.window.Dialog
import androidx.compose.ui.window.DialogProperties
import com.tangem.core.ui.R
import com.tangem.core.ui.components.SelctorDialogParamsProvider.SelectorDialogParams
import com.tangem.core.ui.res.TangemTheme
/**
* Dialog button params
*
* @param title Button text. If not provided default values will be used
* @param warning If true then button text will be in theme warning color
* @param enabled If false button will be disabled
* @param onClick Button click callback
*/
data class DialogButton(
val title: String? = null,
val warning: Boolean = false,
val enabled: Boolean = true,
val onClick: () -> Unit,
)
/**
* Additional params for dialog text field
*/
data class AdditionalTextInputDialogParams(
val label: String? = null,
val placeholder: String? = null,
val caption: String? = null,
val enabled: Boolean = true,
val isError: Boolean = false,
)
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.persistentListOf
import kotlinx.collections.immutable.toImmutableList
/**
* Simple alert dialog with a message and 'OK' button
@ -129,6 +118,54 @@ fun TextInputDialog(
)
}
@Composable
fun SelectorDialog(
selectedItemIndex: Int,
items: ImmutableList<String>,
confirmButton: DialogButton,
onSelect: (index: Int) -> Unit,
onDismissDialog: () -> Unit,
title: String? = null,
isDismissable: Boolean = true,
) {
TangemDialog(
type = DialogType.Selector(selectedItemIndex, items, onSelect),
confirmButton = confirmButton,
title = title,
onDismissDialog = onDismissDialog,
properties = DialogProperties(
dismissOnBackPress = isDismissable,
dismissOnClickOutside = isDismissable,
),
)
}
/**
* Dialog button params
*
* @param title Button text. If not provided default values will be used
* @param warning If true then button text will be in theme warning color
* @param enabled If false button will be disabled
* @param onClick Button click callback
*/
data class DialogButton(
val title: String? = null,
val warning: Boolean = false,
val enabled: Boolean = true,
val onClick: () -> Unit,
)
/**
* Additional params for dialog text field
*/
data class AdditionalTextInputDialogParams(
val label: String? = null,
val placeholder: String? = null,
val caption: String? = null,
val enabled: Boolean = true,
val isError: Boolean = false,
)
// region Defaults
@Composable
private fun TangemDialog(
@ -146,15 +183,18 @@ private fun TangemDialog(
shape = TangemTheme.shapes.roundedCornersLarge,
color = TangemTheme.colors.background.plain,
)
.padding(all = TangemTheme.dimens.spacing24),
.padding(vertical = TangemTheme.dimens.spacing24),
) {
if (title != null) {
Text(
modifier = Modifier.fillMaxWidth(),
modifier = Modifier
.padding(horizontal = TangemTheme.dimens.spacing24)
.fillMaxWidth(),
text = title,
style = when (type) {
is DialogType.Message -> TangemTheme.typography.h2
is DialogType.TextInput -> TangemTheme.typography.h3
is DialogType.Selector -> TangemTheme.typography.h2
},
color = TangemTheme.colors.text.primary1,
)
@ -162,7 +202,13 @@ private fun TangemDialog(
}
DialogContent(type = type)
SpacerH24()
DialogButtons(confirmButton = confirmButton, dismissButton = dismissButton)
DialogButtons(
modifier = Modifier
.padding(horizontal = TangemTheme.dimens.spacing24)
.fillMaxWidth(),
confirmButton = confirmButton,
dismissButton = dismissButton,
)
}
}
}
@ -177,7 +223,9 @@ private fun DialogContent(type: DialogType, modifier: Modifier = Modifier) {
when (type) {
is DialogType.Message -> {
Text(
modifier = Modifier.fillMaxWidth(),
modifier = Modifier
.padding(horizontal = TangemTheme.dimens.spacing24)
.fillMaxWidth(),
text = type.message,
style = TangemTheme.typography.body2,
color = TangemTheme.colors.text.secondary,
@ -185,7 +233,9 @@ private fun DialogContent(type: DialogType, modifier: Modifier = Modifier) {
}
is DialogType.TextInput -> {
OutlineTextField(
modifier = Modifier.fillMaxWidth(),
modifier = Modifier
.padding(horizontal = TangemTheme.dimens.spacing24)
.fillMaxWidth(),
value = type.value,
label = type.params.label,
placeholder = type.params.placeholder,
@ -197,6 +247,14 @@ private fun DialogContent(type: DialogType, modifier: Modifier = Modifier) {
},
)
}
is DialogType.Selector -> {
SelectorDialogContent(
modifier = Modifier.fillMaxWidth(),
selectedItemIndex = type.selectedItemIndex,
items = type.items,
onSelect = type.onSelect,
)
}
}
}
}
@ -204,7 +262,7 @@ private fun DialogContent(type: DialogType, modifier: Modifier = Modifier) {
@Composable
private fun DialogButtons(confirmButton: DialogButton, dismissButton: DialogButton?, modifier: Modifier = Modifier) {
Row(
modifier = modifier.fillMaxWidth(),
modifier = modifier,
horizontalArrangement = Arrangement.spacedBy(
space = TangemTheme.dimens.spacing4,
alignment = Alignment.End,
@ -252,14 +310,72 @@ private fun DialogButton(
}
}
private sealed interface DialogType {
data class Message(val message: String) : DialogType
@Composable
private fun SelectorDialogContent(
selectedItemIndex: Int,
items: ImmutableList<String>,
onSelect: (index: Int) -> Unit,
modifier: Modifier = Modifier,
) {
LazyColumn(modifier = modifier) {
itemsIndexed(items = items) { index, itemText ->
val onClick = remember(index) {
{ onSelect(index) }
}
val interactionSource = remember { MutableInteractionSource() }
Row(
modifier = Modifier
.clickable(
interactionSource = interactionSource,
indication = LocalIndication.current,
onClick = onClick,
)
.padding(
vertical = TangemTheme.dimens.spacing16,
horizontal = TangemTheme.dimens.spacing18,
)
.fillMaxWidth(),
horizontalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing16),
verticalAlignment = Alignment.CenterVertically,
) {
RadioButton(
modifier = Modifier.size(TangemTheme.dimens.size24),
selected = index == selectedItemIndex,
onClick = onClick,
colors = RadioButtonDefaults.colors(
selectedColor = TangemTheme.colors.icon.accent,
unselectedColor = TangemTheme.colors.icon.secondary,
),
interactionSource = interactionSource,
)
Text(
text = itemText,
style = TangemTheme.typography.body1,
color = TangemTheme.colors.text.primary1,
)
}
}
}
}
@Immutable
private sealed class DialogType {
data class Message(val message: String) : DialogType()
data class TextInput(
val value: TextFieldValue,
val onValueChange: (TextFieldValue) -> Unit,
val params: AdditionalTextInputDialogParams = AdditionalTextInputDialogParams(),
) : DialogType
) : DialogType()
data class Selector(
val selectedItemIndex: Int,
val items: ImmutableList<String>,
val onSelect: (index: Int) -> Unit,
) : DialogType()
}
// endregion Defaults
@ -381,4 +497,65 @@ private fun TextInputDialogPreview_Dark() {
TextInputDialogSample()
}
}
@Preview(showBackground = true, widthDp = 360)
@Composable
private fun SelectorDialogPreview_Light(
@PreviewParameter(SelctorDialogParamsProvider::class) param: SelectorDialogParams,
) {
TangemTheme(isDark = false) {
SelectorDialog(
title = param.title,
items = param.items,
selectedItemIndex = param.selectedItemIndex,
confirmButton = DialogButton(title = "Cancel", onClick = {}),
onSelect = {},
onDismissDialog = {},
)
}
}
@Preview(showBackground = true, widthDp = 360)
@Composable
private fun SelectorDialogPreview_Dark(
@PreviewParameter(SelctorDialogParamsProvider::class) param: SelectorDialogParams,
) {
TangemTheme(isDark = true) {
SelectorDialog(
title = param.title,
items = param.items,
selectedItemIndex = param.selectedItemIndex,
confirmButton = DialogButton(title = "Cancel", onClick = {}),
onSelect = {},
onDismissDialog = {},
)
}
}
private class SelctorDialogParamsProvider : CollectionPreviewParameterProvider<SelectorDialogParams>(
collection = listOf(
SelectorDialogParams(
title = "Theme",
selectedItemIndex = 0,
persistentListOf("Light", "Dark", "Follow system"),
),
SelectorDialogParams(
title = null,
selectedItemIndex = 2,
persistentListOf("Light", "Dark", "Follow system"),
),
SelectorDialogParams(
title = "Count",
selectedItemIndex = 8,
List(size = 10) { it.toString() }.toImmutableList(),
),
),
) {
data class SelectorDialogParams(
val title: String?,
val selectedItemIndex: Int,
val items: ImmutableList<String>,
)
}
// endregion Preview

View file

@ -26,5 +26,10 @@ enum class AppThemeMode {
* The default [AppThemeMode].
*/
val DEFAULT: AppThemeMode = FORCE_LIGHT
/**
* List of available [AppThemeMode]s.
* */
val available: List<AppThemeMode> = values().toList()
}
}