Updated on 2026-08-14

This commit is contained in:
Tangem 2022-08-08 16:08:12 +03:00
commit 20b379cd1b
17 changed files with 440 additions and 51 deletions

View file

@ -6,8 +6,6 @@ import com.tangem.tap.common.redux.AppDialog
import com.tangem.tap.common.redux.global.GlobalState
import com.tangem.tap.common.ui.SimpleAlertDialog
import com.tangem.tap.common.ui.SimpleCancelableAlertDialog
import com.tangem.tap.features.details.redux.DetailsDialog
import com.tangem.tap.features.details.redux.PrivacySetting
import com.tangem.tap.features.details.redux.walletconnect.WalletConnectDialog
import com.tangem.tap.features.details.ui.walletconnect.dialogs.ApproveWcSessionDialog
import com.tangem.tap.features.details.ui.walletconnect.dialogs.BnbTransactionDialog
@ -150,18 +148,6 @@ class DialogManager : StoreSubscriber<GlobalState> {
)
is WalletDialog.RussianCardholdersWarningDialog ->
RussianCardholdersWarningBottomSheetDialog(context)
is DetailsDialog.ConfirmDisablingSaving -> {
val messageRes = when (state.dialog.setting) {
PrivacySetting.SAVE_CARDS -> R.string.app_settings_off_saved_wallet_alert_message
PrivacySetting.SAVE_ACCESS_CODE -> R.string.app_settings_off_saved_access_code_alert_message
}
SimpleCancelableAlertDialog.create(
titleRes = R.string.common_warning,
messageRes = messageRes,
context = context,
primaryButtonAction = state.dialog.onOk
)
}
else -> null
}
dialog?.show()

View file

@ -7,6 +7,7 @@ import androidx.fragment.app.FragmentManager
import com.tangem.common.extensions.VoidCallback
import com.tangem.tap.common.redux.navigation.AppScreen
import com.tangem.tap.common.redux.navigation.FragmentShareTransition
import com.tangem.tap.features.details.ui.appsettings.AppSettingsFragment
import com.tangem.tap.features.details.ui.cardsettings.CardSettingsFragment
import com.tangem.tap.features.details.ui.details.DetailsFragment
import com.tangem.tap.features.details.ui.resetcard.ResetCardFragment
@ -95,7 +96,7 @@ private fun fragmentFactory(screen: AppScreen): Fragment {
AppScreen.Details -> DetailsFragment()
AppScreen.DetailsSecurity -> SecurityModeFragment()
AppScreen.CardSettings -> CardSettingsFragment()
AppScreen.AppSettings -> CardSettingsFragment()
AppScreen.AppSettings -> AppSettingsFragment()
AppScreen.ResetToFactory -> ResetCardFragment()
AppScreen.Disclaimer -> DisclaimerFragment()
AppScreen.AddTokens -> AddTokensFragment()

View file

@ -44,10 +44,7 @@ sealed class DetailsAction : Action {
}
sealed class AppSettings : DetailsAction() {
data class SwitchPrivacySetting(val allow: Boolean, val setting: PrivacySetting) :
AppSettings()
data class ConfirmSwitchingSetting(val allow: Boolean, val setting: PrivacySetting) :
data class SwitchPrivacySetting(val enable: Boolean, val setting: PrivacySetting) :
AppSettings()
}

View file

@ -5,7 +5,6 @@ import com.tangem.common.core.TangemSdkError
import com.tangem.domain.common.isTangemTwins
import com.tangem.tap.common.analytics.Analytics
import com.tangem.tap.common.analytics.AnalyticsParam
import com.tangem.tap.common.extensions.dispatchDialogShow
import com.tangem.tap.common.extensions.dispatchNotification
import com.tangem.tap.common.extensions.dispatchOnMain
import com.tangem.tap.common.redux.AppState
@ -188,20 +187,8 @@ class DetailsMiddleware {
class ManagePrivacyMiddleware {
fun handle(action: DetailsAction.AppSettings) {
when (action) {
is DetailsAction.AppSettings.ConfirmSwitchingSetting -> {
// TODO()
}
is DetailsAction.AppSettings.SwitchPrivacySetting -> {
if (action.allow) {
store.dispatch(
DetailsAction.AppSettings.ConfirmSwitchingSetting(
action.allow,
action.setting,
),
)
} else {
store.dispatchDialogShow(DetailsDialog.ConfirmDisablingSaving(action.setting))
}
// TODO()
}
}
}

View file

@ -147,13 +147,12 @@ private fun handlePrivacyAction(
action: DetailsAction.AppSettings, state: DetailsState,
): DetailsState {
return when (action) {
is DetailsAction.AppSettings.ConfirmSwitchingSetting -> {
is DetailsAction.AppSettings.SwitchPrivacySetting -> {
when (action.setting) {
PrivacySetting.SAVE_CARDS -> state.copy(saveCards = action.allow)
PrivacySetting.SAVE_ACCESS_CODE -> state.copy(savePasswords = action.allow)
PrivacySetting.SaveWallets -> state.copy(saveWallets = action.enable)
PrivacySetting.SaveAccessCode -> state.copy(saveAccessCodes = action.enable)
}
}
is DetailsAction.AppSettings.SwitchPrivacySetting -> state
}
}

View file

@ -6,7 +6,6 @@ import com.tangem.common.card.Card
import com.tangem.domain.common.ScanResponse
import com.tangem.tap.common.entities.Button
import com.tangem.tap.common.entities.FiatCurrency
import com.tangem.tap.common.redux.StateDialog
import com.tangem.tap.features.onboarding.products.twins.redux.TwinCardsState
import com.tangem.tap.store
import org.rekotlin.StateType
@ -21,8 +20,8 @@ data class DetailsState(
val privacyPolicyUrl: String? = null,
val createBackupAllowed: Boolean = false,
val appCurrency: FiatCurrency = FiatCurrency.Default,
val saveCards: Boolean = true,
val savePasswords: Boolean = true
val saveWallets: Boolean = true,
val saveAccessCodes: Boolean = true,
) : StateType {
// if you do not delegate - the application crashes on startup,
@ -58,14 +57,6 @@ data class ManageSecurityState(
enum class SecurityOption { LongTap, PassCode, AccessCode }
sealed interface DetailsDialog : StateDialog {
data class ConfirmDisablingSaving(val setting: PrivacySetting) : DetailsDialog {
val onOk: () -> Unit =
{ store.dispatch(DetailsAction.AppSettings.ConfirmSwitchingSetting(false, setting)) }
}
}
enum class PrivacySetting {
SAVE_CARDS, SAVE_ACCESS_CODE
SaveWallets, SaveAccessCode
}

View file

@ -1,3 +1,71 @@
package com.tangem.tap.features.details.ui.appsettings
class AppSettingsFragment
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.mutableStateOf
import androidx.compose.ui.platform.ComposeView
import androidx.fragment.app.Fragment
import androidx.transition.TransitionInflater
import com.google.accompanist.appcompattheme.AppCompatTheme
import com.tangem.tap.common.redux.navigation.NavigationAction
import com.tangem.tap.features.details.redux.DetailsAction
import com.tangem.tap.features.details.redux.DetailsState
import com.tangem.tap.store
import com.tangem.wallet.R
import org.rekotlin.StoreSubscriber
class AppSettingsFragment : Fragment(), StoreSubscriber<DetailsState> {
private val viewModel = AppSettingsViewModel(store)
private var screenState: MutableState<AppSettingsScreenState> =
mutableStateOf(viewModel.updateState(store.state.detailsState))
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val inflater = TransitionInflater.from(requireContext())
enterTransition = inflater.inflateTransition(R.transition.slide_right)
exitTransition = inflater.inflateTransition(R.transition.fade)
}
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?,
): View {
return ComposeView(requireContext()).apply {
setContent {
isTransitionGroup = true
AppCompatTheme {
AppSettingsScreen(
state = screenState.value,
onBackPressed = {
store.dispatch(DetailsAction.ResetCardSettingsData)
store.dispatch(NavigationAction.PopBackTo())
},
)
}
}
}
}
override fun onStart() {
super.onStart()
store.subscribe(this) { state ->
state.skipRepeats { oldState, newState ->
oldState.detailsState == newState.detailsState
}.select { it.detailsState }
}
}
override fun onStop() {
super.onStop()
store.unsubscribe(this)
}
override fun newState(state: DetailsState) {
if (activity == null || view == null) return
screenState.value = viewModel.updateState(state)
}
}

View file

@ -1,9 +1,230 @@
package com.tangem.tap.features.details.ui.appsettings
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.shape.CornerSize
import androidx.compose.material.AlertDialog
import androidx.compose.material.MaterialTheme
import androidx.compose.material.Text
import androidx.compose.material.TextButton
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.colorResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import com.tangem.tap.common.compose.TangemTypography
import com.tangem.tap.features.details.redux.PrivacySetting
import com.tangem.tap.features.details.ui.common.SettingsScreensScaffold
import com.tangem.tap.features.details.ui.common.TangemSwitch
import com.tangem.wallet.R
@Composable
fun AppSettingsScreen(
state: AppSettingsScreenState,
onBackPressed: () -> Unit,
modifier: Modifier = Modifier,
) {
SettingsScreensScaffold(
content = {
AppSettings(state = state, modifier = modifier)
},
titleRes = R.string.app_settings_title,
onBackClick = onBackPressed,
)
}
@Composable
private fun AppSettings(
state: AppSettingsScreenState,
modifier: Modifier = Modifier,
) {
var dialogType by remember { mutableStateOf<PrivacySetting?>(null) }
val onDialogStateChange: (PrivacySetting?) -> Unit = { dialogType = it }
dialogType?.let {
SettingsAlertDialog(
element = it,
onDialogStateChange = onDialogStateChange,
onSettingToggled = state.onSettingToggled,
modifier = modifier,
)
}
Column(
modifier = modifier
.fillMaxSize(),
) {
AppSettingsElement(
state = state,
setting = PrivacySetting.SaveWallets,
onDialogStateChange = onDialogStateChange,
modifier = modifier,
)
Spacer(modifier = Modifier.size(32.dp))
AppSettingsElement(
state = state,
setting = PrivacySetting.SaveAccessCode,
onDialogStateChange = onDialogStateChange,
modifier = modifier,
)
}
}
@Composable
private fun AppSettingsElement(
state: AppSettingsScreenState,
setting: PrivacySetting,
onDialogStateChange: (PrivacySetting?) -> Unit,
modifier: Modifier = Modifier,
) {
val titleRes = when (setting) {
PrivacySetting.SaveWallets -> R.string.app_settings_saved_wallet
PrivacySetting.SaveAccessCode -> R.string.app_settings_saved_access_codes
}
val subtitleRes = when (setting) {
PrivacySetting.SaveWallets -> R.string.app_settings_saved_wallet_footer
PrivacySetting.SaveAccessCode -> R.string.app_settings_saved_access_codes_footer
}
val checked = state.settings[setting] ?: false
Row(
modifier = modifier
.fillMaxWidth()
.padding(start = 20.dp),
verticalAlignment = Alignment.CenterVertically,
) {
Column(
verticalArrangement = Arrangement.Center,
modifier = modifier
.weight(0.6f)
.padding(end = 4.dp),
) {
Text(
text = stringResource(id = titleRes),
style = TangemTypography.subtitle1,
color = colorResource(id = R.color.text_primary_1),
)
Spacer(modifier = Modifier.size(4.dp))
Text(
text = stringResource(id = subtitleRes),
style = TangemTypography.body2,
color = colorResource(id = R.color.text_secondary),
)
}
TangemSwitch(
checked = checked,
onCheckedChange = {
onCheckedChange(
element = setting,
enabled = it,
onSettingToggled = state.onSettingToggled,
onDialogStateChange = onDialogStateChange,
)
},
modifier = modifier
.padding(20.dp),
)
}
}
private fun onCheckedChange(
element: PrivacySetting, enabled: Boolean,
onSettingToggled: (PrivacySetting, Boolean) -> Unit,
onDialogStateChange: (PrivacySetting?) -> Unit,
) {
//Show warning if user wants to disable the switch
if (!enabled) {
onDialogStateChange(element)
} else {
onSettingToggled(element, enabled)
}
}
@Composable
private fun SettingsAlertDialog(
element: PrivacySetting,
onDialogStateChange: (PrivacySetting?) -> Unit,
onSettingToggled: (PrivacySetting, Boolean) -> Unit,
modifier: Modifier = Modifier,
) {
val text = when (element) {
PrivacySetting.SaveWallets -> R.string.app_settings_off_saved_wallet_alert_message
PrivacySetting.SaveAccessCode -> R.string.app_settings_off_saved_access_code_alert_message
}
AlertDialog(
onDismissRequest = { onDialogStateChange(null) },
confirmButton = {
TextButton(
onClick = {
onDialogStateChange(null)
},
) {
Text(
text = stringResource(id = R.string.common_cancel),
color = colorResource(id = R.color.text_secondary),
fontSize = 14.sp,
fontWeight = FontWeight.Medium,
)
}
},
dismissButton = {
TextButton(
onClick = {
onDialogStateChange(null)
onSettingToggled(element, false)
},
modifier = modifier.padding(bottom = 14.dp),
) {
Text(
text = stringResource(id = R.string.common_delete),
color = colorResource(id = R.color.text_warning),
fontSize = 14.sp,
fontWeight = FontWeight.Medium,
)
}
},
title = {
Text(
text = stringResource(id = R.string.common_attention),
color = colorResource(id = R.color.text_primary_1),
)
},
text = {
Text(
text = stringResource(id = text),
color = colorResource(id = R.color.text_secondary),
)
},
shape = MaterialTheme.shapes.medium.copy(all = CornerSize(size = 28.dp)),
modifier = modifier.padding(vertical = 34.dp, horizontal = 24.dp),
)
}
@Preview
@Composable
fun AppSettingsScreenPreview() {
AppSettingsScreen(
state = AppSettingsScreenState(
settings = mapOf(
PrivacySetting.SaveWallets to true,
PrivacySetting.SaveAccessCode to false,
),
onSettingToggled = { _, _ -> },
),
onBackPressed = { },
)
}

View file

@ -0,0 +1,10 @@
package com.tangem.tap.features.details.ui.appsettings
import com.tangem.tap.features.details.redux.PrivacySetting
data class AppSettingsScreenState(
val settings: Map<PrivacySetting, Boolean>,
val onSettingToggled: (PrivacySetting, Boolean) -> Unit,
)

View file

@ -0,0 +1,27 @@
package com.tangem.tap.features.details.ui.appsettings
import com.tangem.tap.common.redux.AppState
import com.tangem.tap.features.details.redux.DetailsAction
import com.tangem.tap.features.details.redux.DetailsState
import com.tangem.tap.features.details.redux.PrivacySetting
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import org.rekotlin.Store
class AppSettingsViewModel(private val store: Store<AppState>) {
private val _uiState = MutableStateFlow(updateState(store.state.detailsState))
val uiState: StateFlow<AppSettingsScreenState> = _uiState
fun updateState(state: DetailsState): AppSettingsScreenState {
return AppSettingsScreenState(
settings = mapOf(
PrivacySetting.SaveWallets to state.saveWallets,
PrivacySetting.SaveAccessCode to state.saveAccessCodes,
),
onSettingToggled = { privacySetting, enabled -> onSettingsToggled(privacySetting, enabled) },
)
}
private fun onSettingsToggled(setting: PrivacySetting, enable: Boolean) {
store.dispatch(DetailsAction.AppSettings.SwitchPrivacySetting(enable = enable, setting = setting))
}
}

View file

@ -0,0 +1,96 @@
package com.tangem.tap.features.details.ui.common
import androidx.compose.animation.animateColor
import androidx.compose.animation.core.FastOutLinearInEasing
import androidx.compose.animation.core.LinearOutSlowInEasing
import androidx.compose.animation.core.animateDp
import androidx.compose.animation.core.tween
import androidx.compose.animation.core.updateTransition
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.indication
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.BoxWithConstraints
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.offset
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.ripple.rememberRipple
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.colorResource
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import com.tangem.wallet.R
@Composable
fun TangemSwitch(
modifier: Modifier = Modifier,
enabledColor: Color = colorResource(id = R.color.control_checked),
disabledColor: Color = colorResource(id = R.color.icon_informative),
size: Dp = 48.dp,
checked: Boolean = false,
onCheckedChange: (Boolean) -> Unit,
) {
val transition = updateTransition(checked, label = "SwitchState")
val color by transition.animateColor(
transitionSpec = {
tween(durationMillis = 200, easing = FastOutLinearInEasing)
},
label = "",
) { enabled ->
if (enabled) enabledColor else disabledColor
}
val interactionSource = remember { MutableInteractionSource() }
Box(
modifier = modifier
.clickable(
interactionSource = interactionSource,
indication = null,
) {
onCheckedChange(!checked)
}
.indication(
interactionSource = interactionSource,
indication = rememberRipple(
bounded = false,
color = Color.Transparent,
),
),
) {
BoxWithConstraints(
modifier = modifier
.width(size)
.height(size / 2)
.indication(interactionSource, null)
.background(color = color, shape = RoundedCornerShape(100)),
contentAlignment = Alignment.CenterStart,
) {
val roundCardSize = this.maxWidth / 2
val xOffset by transition.animateDp(
transitionSpec = {
tween(durationMillis = 150, easing = LinearOutSlowInEasing)
},
label = "xOffset",
) { enabled ->
if (enabled) this.maxWidth - roundCardSize else 0.dp
}
Box(
modifier = Modifier
.size(this.maxWidth / 2)
.offset(x = xOffset, y = 0.dp)
.padding(3.dp)
.background(color = Color.White, shape = RoundedCornerShape(100)),
)
}
}
}

View file

@ -28,6 +28,7 @@ class DetailsViewModel(private val store: Store<AppState>) {
SettingsElement.PrivacyPolicy -> {
if (state.privacyPolicyUrl != null) it else null
}
SettingsElement.AppSettings -> null // TODO: until we implement settings from this screen
else -> it
}
}

View file

@ -6,6 +6,7 @@
<string name="common_done">Erledigt</string>
<string name="common_cancel">Abbrechen</string>
<string name="common_accept">Akzeptieren</string>
<string name="common_delete">Entfernen</string>
<string name="common_camera_denied_alert_title">Der Kamerazugriff wurde verweigert </string>
<string name="common_camera_denied_alert_message">Sie haben keinen Zugang zur Kamera erteilt, bitte passen Sie Ihre Datenschutzeinstellungen an</string>

View file

@ -7,6 +7,7 @@
<string name="common_done">Exécuté</string>
<string name="common_cancel">Annuler</string>
<string name="common_accept">J\'accepte</string>
<string name="common_delete">Supprimer</string>
<string name="common_camera_denied_alert_title">L\'accès à la caméra est refusé</string>
<string name="common_camera_denied_alert_message">Vous n\'avez pas octroyé l\'accès à votre caméra, veuillez modifier vos paramètres de confidentialité</string>

View file

@ -6,6 +6,7 @@
<string name="common_retry">Riprova</string>
<string name="common_done">Fatto</string>
<string name="common_cancel">Annulla</string>
<string name="common_delete">Rimuovere</string>
<string name="common_camera_denied_alert_title">Accesso alla videocamera negato</string>
<string name="common_camera_denied_alert_message">Non hai fornito l\'accesso alla tua videocamera, modifica le tue impostazioni sulla privacy</string>
<string name="common_camera_alert_button_settings">Impostazioni</string>

View file

@ -7,6 +7,7 @@
<string name="common_done">Готово</string>
<string name="common_cancel">Отмена</string>
<string name="common_accept">Принять</string>
<string name="common_delete">Удалить</string>
<string name="common_custom_string">%s</string>
<string name="common_camera_denied_alert_title">Доступ к камере запрещен</string>
<string name="common_camera_denied_alert_message">Вы не предоставили доступ к своей камере, измените настройки конфиденциальности</string>

View file

@ -8,6 +8,7 @@
<string name="common_done">Done</string>
<string name="common_cancel">Cancel</string>
<string name="common_accept">Accept</string>
<string name="common_delete">Delete</string>
<string name="common_custom_string" translatable="false">%s</string>
<string name="common_camera_denied_alert_title">Camera access denied</string>