android, desktop: improvement to a lock UI (#4769)

* android, desktop: improvement to a lock UI

* oneTime passcode screen which allows to pass verification while in call

* change

* unused line

* don't ask to set up auth if already has

---------

Co-authored-by: Evgeny Poberezkin <evgeny@poberezkin.com>
This commit is contained in:
Stanislav Dmitrenko
2024-08-29 12:15:11 +00:00
committed by GitHub
parent 2fe3acf4df
commit 6edea46dad
11 changed files with 70 additions and 50 deletions
@@ -14,6 +14,7 @@ actual fun authenticate(
promptSubtitle: String,
selfDestruct: Boolean,
usingLAMode: LAMode,
oneTime: Boolean,
completed: (LAResult) -> Unit
) {
val activity = mainActivity.get() ?: return completed(LAResult.Error(""))
@@ -27,7 +28,7 @@ actual fun authenticate(
else -> completed(LAResult.Unavailable())
}
LAMode.PASSCODE -> {
authenticateWithPasscode(promptTitle, promptSubtitle, selfDestruct, completed)
authenticateWithPasscode(promptTitle, promptSubtitle, selfDestruct, oneTime, completed)
}
}
}
@@ -74,6 +74,7 @@ fun MainScreen() {
LaunchedEffect(showAdvertiseLAAlert) {
if (
!chatModel.controller.appPrefs.laNoticeShown.get()
&& !appPrefs.performLA.get()
&& showAdvertiseLAAlert
&& chatModel.controller.appPrefs.onboardingStage.get() == OnboardingStage.OnboardingComplete
&& chatModel.chats.size > 3
@@ -211,10 +212,8 @@ fun MainScreen() {
} else {
ActiveCallView()
}
} else {
// It's needed for privacy settings toggle, so it can be shown even if the app is passcode unlocked
ModalManager.fullscreen.showPasscodeInView()
}
ModalManager.fullscreen.showOneTimePasscodeInView()
AlertManager.privacySensitive.showInView()
if (onboarding == OnboardingStage.OnboardingComplete) {
LaunchedEffect(chatModel.currentUser.value, chatModel.appOpenUrl.value) {
@@ -5,6 +5,7 @@ import androidx.compose.material.*
import androidx.compose.runtime.mutableStateOf
import androidx.compose.ui.Modifier
import chat.simplex.common.model.*
import chat.simplex.common.model.ChatController.appPrefs
import chat.simplex.common.platform.*
import chat.simplex.common.views.helpers.*
import chat.simplex.common.views.localauth.SetAppPasscodeView
@@ -31,7 +32,7 @@ object AppLock {
fun showLANotice(laNoticeShown: SharedPreference<Boolean>) {
Log.d(TAG, "showLANotice")
if (!laNoticeShown.get()) {
if (!laNoticeShown.get() && !appPrefs.performLA.get()) {
laNoticeShown.set(true)
AlertManager.shared.showAlertDialog(
title = generalGetString(MR.strings.la_notice_title_simplex_lock),
@@ -57,6 +58,8 @@ object AppLock {
private fun showChooseLAMode() {
Log.d(TAG, "showLANotice")
if (appPrefs.performLA.get()) return
AlertManager.shared.showAlertDialogStacked(
title = generalGetString(MR.strings.la_lock_mode),
text = null,
@@ -80,21 +83,23 @@ object AppLock {
authenticate(
generalGetString(MR.strings.auth_enable_simplex_lock),
generalGetString(MR.strings.auth_confirm_credential),
oneTime = true,
completed = { laResult ->
when (laResult) {
LAResult.Success -> {
m.performLA.value = true
m.showAuthScreen.value = true
appPrefs.performLA.set(true)
laTurnedOnAlert()
}
is LAResult.Failed -> { /* Can be called multiple times on every failure */ }
is LAResult.Error -> {
m.performLA.value = false
appPrefs.performLA.set(false)
m.showAuthScreen.value = false
// Don't drop auth pref in case of state inconsistency (eg, you have set passcode but somehow bypassed toggle and turned it off and then on)
// appPrefs.performLA.set(false)
laFailedAlert()
}
is LAResult.Unavailable -> {
m.performLA.value = false
m.showAuthScreen.value = false
appPrefs.performLA.set(false)
m.showAdvertiseLAUnavailableAlert.value = true
}
@@ -104,19 +109,22 @@ object AppLock {
}
private fun setPasscode() {
if (appPrefs.performLA.get()) return
val appPrefs = ChatController.appPrefs
ModalManager.fullscreen.showCustomModal { close ->
Surface(Modifier.fillMaxSize(), color = MaterialTheme.colors.background, contentColor = LocalContentColor.current) {
SetAppPasscodeView(
submit = {
ChatModel.performLA.value = true
ChatModel.showAuthScreen.value = true
appPrefs.performLA.set(true)
appPrefs.laMode.set(LAMode.PASSCODE)
laTurnedOnAlert()
},
cancel = {
ChatModel.performLA.value = false
appPrefs.performLA.set(false)
ChatModel.showAuthScreen.value = false
// Don't drop auth pref in case of state inconsistency (eg, you have set passcode but somehow bypassed toggle and turned it off and then on)
// appPrefs.performLA.set(false)
laPasscodeNotSetAlert()
},
close = close
@@ -147,6 +155,7 @@ object AppLock {
else
generalGetString(MR.strings.auth_unlock),
selfDestruct = true,
oneTime = false,
completed = { laResult ->
when (laResult) {
LAResult.Success ->
@@ -160,7 +169,7 @@ object AppLock {
}
is LAResult.Unavailable -> {
userAuthorized.value = true
m.performLA.value = false
m.showAuthScreen.value = false
m.controller.appPrefs.performLA.set(false)
laUnavailableTurningOffAlert()
}
@@ -192,22 +201,23 @@ object AppLock {
generalGetString(MR.strings.auth_confirm_credential)
else
"",
oneTime = true,
completed = { laResult ->
val prefPerformLA = m.controller.appPrefs.performLA
when (laResult) {
LAResult.Success -> {
m.performLA.value = true
m.showAuthScreen.value = true
prefPerformLA.set(true)
laTurnedOnAlert()
}
is LAResult.Failed -> { /* Can be called multiple times on every failure */ }
is LAResult.Error -> {
m.performLA.value = false
m.showAuthScreen.value = false
prefPerformLA.set(false)
laFailedAlert()
}
is LAResult.Unavailable -> {
m.performLA.value = false
m.showAuthScreen.value = false
prefPerformLA.set(false)
laUnavailableInstructionAlert()
}
@@ -227,12 +237,13 @@ object AppLock {
generalGetString(MR.strings.auth_confirm_credential)
else
generalGetString(MR.strings.auth_disable_simplex_lock),
oneTime = true,
completed = { laResult ->
val prefPerformLA = m.controller.appPrefs.performLA
val selfDestructPref = m.controller.appPrefs.selfDestruct
when (laResult) {
LAResult.Success -> {
m.performLA.value = false
m.showAuthScreen.value = false
prefPerformLA.set(false)
DatabaseUtils.ksAppPassword.remove()
selfDestructPref.set(false)
@@ -240,12 +251,12 @@ object AppLock {
}
is LAResult.Failed -> { /* Can be called multiple times on every failure */ }
is LAResult.Error -> {
m.performLA.value = true
m.showAuthScreen.value = true
prefPerformLA.set(true)
laFailedAlert()
}
is LAResult.Unavailable -> {
m.performLA.value = false
m.showAuthScreen.value = false
prefPerformLA.set(false)
laUnavailableTurningOffAlert()
}
@@ -20,7 +20,6 @@ import dev.icerock.moko.resources.ImageResource
import dev.icerock.moko.resources.StringResource
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.*
import kotlinx.coroutines.flow.internal.ChannelFlow
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import kotlinx.datetime.*
@@ -98,7 +97,7 @@ object ChatModel {
}
)
}
val performLA by lazy { mutableStateOf(ChatController.appPrefs.performLA.get()) }
val showAuthScreen by lazy { mutableStateOf(ChatController.appPrefs.performLA.get()) }
val showAdvertiseLAUnavailableAlert = mutableStateOf(false)
val showChatPreviews by lazy { mutableStateOf(ChatController.appPrefs.privacyShowChatPreviews.get()) }
@@ -423,6 +423,7 @@ fun authStopChat(m: ChatModel, progressIndicator: MutableState<Boolean>? = null,
authenticate(
generalGetString(MR.strings.auth_stop_chat),
generalGetString(MR.strings.auth_log_in_using_credential),
oneTime = true,
completed = { laResult ->
when (laResult) {
LAResult.Success, is LAResult.Unavailable -> {
@@ -34,6 +34,7 @@ expect fun authenticate(
promptSubtitle: String,
selfDestruct: Boolean = false,
usingLAMode: LAMode = ChatModel.controller.appPrefs.laMode.get(),
oneTime: Boolean,
completed: (LAResult) -> Unit
)
@@ -41,10 +42,11 @@ fun authenticateWithPasscode(
promptTitle: String,
promptSubtitle: String,
selfDestruct: Boolean,
oneTime: Boolean,
completed: (LAResult) -> Unit
) {
val password = DatabaseUtils.ksAppPassword.get() ?: return completed(LAResult.Unavailable(generalGetString(MR.strings.la_no_app_password)))
ModalManager.fullscreen.showPasscodeCustomModal { close ->
ModalManager.fullscreen.showPasscodeCustomModal(oneTime) { close ->
BackHandler {
close()
completed(LAResult.Error(generalGetString(MR.strings.authentication_cancelled)))
@@ -69,6 +69,7 @@ class ModalManager(private val placement: ModalPlacement? = null) {
// Don't use mutableStateOf() here, because it produces this if showing from SimpleXAPI.startChat():
// java.lang.IllegalStateException: Reading a state that was created after the snapshot was taken or in a snapshot that has not yet been applied
private var passcodeView: MutableStateFlow<(@Composable (close: () -> Unit) -> Unit)?> = MutableStateFlow(null)
private var onTimePasscodeView: MutableStateFlow<(@Composable (close: () -> Unit) -> Unit)?> = MutableStateFlow(null)
fun showModal(settings: Boolean = false, showClose: Boolean = true, closeOnTop: Boolean = true, endButtons: @Composable RowScope.() -> Unit = {}, content: @Composable ModalData.() -> Unit) {
val data = ModalData()
@@ -105,9 +106,13 @@ class ModalManager(private val placement: ModalPlacement? = null) {
}
}
fun showPasscodeCustomModal(modal: @Composable (close: () -> Unit) -> Unit) {
Log.d(TAG, "ModalManager.showPasscodeCustomModal")
passcodeView.value = modal
fun showPasscodeCustomModal(oneTime: Boolean, modal: @Composable (close: () -> Unit) -> Unit) {
Log.d(TAG, "ModalManager.showPasscodeCustomModal, oneTime: $oneTime")
if (oneTime) {
onTimePasscodeView.value = modal
} else {
passcodeView.value = modal
}
}
fun hasModalsOpen() = modalCount.value > 0
@@ -179,6 +184,11 @@ class ModalManager(private val placement: ModalPlacement? = null) {
passcodeView.collectAsState().value?.invoke { passcodeView.value = null }
}
@Composable
fun showOneTimePasscodeInView() {
onTimePasscodeView.collectAsState().value?.invoke { onTimePasscodeView.value = null }
}
/**
* Allows to modify a list without getting [ConcurrentModificationException]
* */
@@ -1,12 +1,10 @@
package chat.simplex.common.views.usersettings
import SectionBottomSpacer
import SectionCustomFooter
import SectionDividerSpaced
import SectionItemView
import SectionTextFooter
import SectionView
import androidx.compose.foundation.*
import androidx.compose.foundation.layout.*
import androidx.compose.material.*
import androidx.compose.runtime.*
@@ -34,8 +32,6 @@ import chat.simplex.common.views.onboarding.ReadableText
import chat.simplex.common.model.ChatModel
import chat.simplex.common.model.ChatModel.withChats
import chat.simplex.common.platform.*
import kotlin.math.min
import kotlin.math.roundToInt
enum class LAMode {
SYSTEM,
@@ -374,7 +370,8 @@ fun SimplexLockView(
currentLAMode: SharedPreference<LAMode>,
setPerformLA: (Boolean) -> Unit
) {
val performLA = remember { chatModel.performLA }
val showAuthScreen = remember { chatModel.showAuthScreen }
val performLA = remember { appPrefs.performLA.state }
val laMode = remember { chatModel.controller.appPrefs.laMode.state }
val laLockDelay = remember { chatModel.controller.appPrefs.laLockDelay }
val showChangePasscode = remember { derivedStateOf { performLA.value && currentLAMode.state.value == LAMode.PASSCODE } }
@@ -382,13 +379,9 @@ fun SimplexLockView(
val selfDestructDisplayName = remember { mutableStateOf(chatModel.controller.appPrefs.selfDestructDisplayName.get() ?: "") }
val selfDestructDisplayNamePref = remember { chatModel.controller.appPrefs.selfDestructDisplayName }
fun resetLAEnabled(onOff: Boolean) {
chatModel.controller.appPrefs.performLA.set(onOff)
chatModel.performLA.value = onOff
}
fun disableUnavailableLA() {
resetLAEnabled(false)
chatModel.controller.appPrefs.performLA.set(false)
chatModel.showAuthScreen.value = false
currentLAMode.set(LAMode.default)
laUnavailableInstructionAlert()
}
@@ -405,7 +398,8 @@ fun SimplexLockView(
} else {
generalGetString(MR.strings.chat_lock)
},
generalGetString(MR.strings.change_lock_mode)
generalGetString(MR.strings.change_lock_mode),
oneTime = true,
) { laResult ->
when (laResult) {
is LAResult.Error -> {
@@ -415,7 +409,7 @@ fun SimplexLockView(
LAResult.Success -> {
when (toLAMode) {
LAMode.SYSTEM -> {
authenticate(generalGetString(MR.strings.auth_enable_simplex_lock), promptSubtitle = "", usingLAMode = toLAMode) { laResult ->
authenticate(generalGetString(MR.strings.auth_enable_simplex_lock), promptSubtitle = "", usingLAMode = toLAMode, oneTime = true) { laResult ->
when (laResult) {
LAResult.Success -> {
currentLAMode.set(toLAMode)
@@ -451,7 +445,7 @@ fun SimplexLockView(
}
fun toggleSelfDestruct(selfDestruct: SharedPreference<Boolean>) {
authenticate(generalGetString(MR.strings.la_current_app_passcode), generalGetString(MR.strings.change_self_destruct_mode)) { laResult ->
authenticate(generalGetString(MR.strings.la_current_app_passcode), generalGetString(MR.strings.change_self_destruct_mode), oneTime = true) { laResult ->
when (laResult) {
is LAResult.Error -> laFailedAlert()
is LAResult.Failed -> { /* Can be called multiple times on every failure */ }
@@ -470,7 +464,7 @@ fun SimplexLockView(
}
fun changeLAPassword() {
authenticate(generalGetString(MR.strings.la_current_app_passcode), generalGetString(MR.strings.la_change_app_passcode)) { laResult ->
authenticate(generalGetString(MR.strings.la_current_app_passcode), generalGetString(MR.strings.la_change_app_passcode), oneTime = true) { laResult ->
when (laResult) {
LAResult.Success -> {
ModalManager.fullscreen.showCustomModal { close ->
@@ -494,7 +488,7 @@ fun SimplexLockView(
}
fun changeSelfDestructPassword() {
authenticate(generalGetString(MR.strings.la_current_app_passcode), generalGetString(MR.strings.change_self_destruct_passcode)) { laResult ->
authenticate(generalGetString(MR.strings.la_current_app_passcode), generalGetString(MR.strings.change_self_destruct_passcode), oneTime = true) { laResult ->
when (laResult) {
LAResult.Success -> {
ModalManager.fullscreen.showCustomModal { close ->
@@ -525,8 +519,8 @@ fun SimplexLockView(
) {
AppBarTitle(stringResource(MR.strings.chat_lock))
SectionView {
EnableLock(performLA) { performLAToggle ->
performLA.value = performLAToggle
EnableLock(remember { appPrefs.performLA.state }) { performLAToggle ->
showAuthScreen.value = performLAToggle
chatModel.controller.appPrefs.laNoticeShown.set(true)
if (performLAToggle) {
when (currentLAMode.state.value) {
@@ -543,7 +537,9 @@ fun SimplexLockView(
passcodeAlert(generalGetString(MR.strings.passcode_set))
},
cancel = {
resetLAEnabled(false)
chatModel.showAuthScreen.value = false
// Don't drop auth pref in case of state inconsistency (eg, you have set passcode but somehow bypassed toggle and turned it off and then on)
// chatModel.controller.appPrefs.performLA.set(false)
},
close = close
)
@@ -660,7 +656,7 @@ private fun EnableSelfDestruct(
}
@Composable
private fun EnableLock(performLA: MutableState<Boolean>, onCheckedChange: (Boolean) -> Unit) {
private fun EnableLock(performLA: State<Boolean>, onCheckedChange: (Boolean) -> Unit) {
SectionItemView {
Row(verticalAlignment = Alignment.CenterVertically) {
Text(
@@ -22,6 +22,7 @@ import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.*
import chat.simplex.common.model.*
import chat.simplex.common.model.ChatController.appPrefs
import chat.simplex.common.platform.*
import chat.simplex.common.ui.theme.*
import chat.simplex.common.views.CreateProfile
@@ -234,7 +235,7 @@ fun ChatLockItem(
showSettingsModal: (@Composable (ChatModel) -> Unit) -> (() -> Unit),
setPerformLA: (Boolean) -> Unit
) {
val performLA = remember { ChatModel.performLA }
val performLA = remember { appPrefs.performLA.state }
val currentLAMode = remember { ChatModel.controller.appPrefs.laMode }
SettingsActionItemWithContent(
click = showSettingsModal { SimplexLockView(ChatModel, currentLAMode, setPerformLA) },
@@ -505,6 +506,7 @@ private fun runAuth(title: String, desc: String, onFinish: (success: Boolean) ->
authenticate(
title,
desc,
oneTime = true,
completed = { laResult ->
onFinish(laResult == LAResult.Success || laResult is LAResult.Unavailable)
}
@@ -14,7 +14,6 @@ import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.unit.dp
import androidx.compose.ui.window.*
import chat.simplex.common.model.*
import chat.simplex.common.model.ChatController.appPrefs
import chat.simplex.common.platform.*
import chat.simplex.common.ui.theme.DEFAULT_START_MODAL_WIDTH
import chat.simplex.common.ui.theme.SimpleXTheme
@@ -27,7 +26,6 @@ import kotlinx.coroutines.*
import java.awt.event.WindowEvent
import java.awt.event.WindowFocusListener
import java.io.File
import kotlin.math.sqrt
import kotlin.system.exitProcess
val simplexWindowState = SimplexWindowState()
@@ -172,7 +170,7 @@ private fun ApplicationScope.AppWindow(closedByError: MutableState<Boolean>) {
var windowFocused by remember { simplexWindowState.windowFocused }
LaunchedEffect(windowFocused) {
val delay = ChatController.appPrefs.laLockDelay.get()
if (!windowFocused && ChatModel.performLA.value && delay > 0) {
if (!windowFocused && ChatModel.showAuthScreen.value && delay > 0) {
delay(delay * 1000L)
// Trigger auth state check when delay ends (and if it ends)
AppLock.recheckAuthState()
@@ -7,10 +7,11 @@ actual fun authenticate(
promptSubtitle: String,
selfDestruct: Boolean,
usingLAMode: LAMode,
oneTime: Boolean,
completed: (LAResult) -> Unit
) {
when (usingLAMode) {
LAMode.PASSCODE -> authenticateWithPasscode(promptTitle, promptSubtitle, selfDestruct, completed)
LAMode.PASSCODE -> authenticateWithPasscode(promptTitle, promptSubtitle, selfDestruct, oneTime, completed)
else -> {}
}
}