diff --git a/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/views/helpers/LocalAuthentication.android.kt b/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/views/helpers/LocalAuthentication.android.kt index b238bdf7ca..07426c7fbf 100644 --- a/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/views/helpers/LocalAuthentication.android.kt +++ b/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/views/helpers/LocalAuthentication.android.kt @@ -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) } } } diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/App.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/App.kt index 94ca307529..3cba89922d 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/App.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/App.kt @@ -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) { diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/AppLock.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/AppLock.kt index d6214c252c..c93fabec8b 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/AppLock.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/AppLock.kt @@ -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) { 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() } diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/ChatModel.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/ChatModel.kt index e92b3d714a..5a1c46666d 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/ChatModel.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/ChatModel.kt @@ -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()) } diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/database/DatabaseView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/database/DatabaseView.kt index 333fda307a..b287847ace 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/database/DatabaseView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/database/DatabaseView.kt @@ -423,6 +423,7 @@ fun authStopChat(m: ChatModel, progressIndicator: MutableState? = 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 -> { diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/LocalAuthentication.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/LocalAuthentication.kt index 022ee37589..28f6320ee7 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/LocalAuthentication.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/LocalAuthentication.kt @@ -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))) diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/ModalView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/ModalView.kt index bfd61a2add..8da73ab3ca 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/ModalView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/ModalView.kt @@ -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] * */ diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/PrivacySettings.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/PrivacySettings.kt index 084dfc20d2..abf318390f 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/PrivacySettings.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/PrivacySettings.kt @@ -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, 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) { - 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, onCheckedChange: (Boolean) -> Unit) { +private fun EnableLock(performLA: State, onCheckedChange: (Boolean) -> Unit) { SectionItemView { Row(verticalAlignment = Alignment.CenterVertically) { Text( diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/SettingsView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/SettingsView.kt index b50e905f39..3e1522b288 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/SettingsView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/SettingsView.kt @@ -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) } diff --git a/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/DesktopApp.kt b/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/DesktopApp.kt index df8887c4e1..fc0e97b417 100644 --- a/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/DesktopApp.kt +++ b/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/DesktopApp.kt @@ -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) { 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() diff --git a/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/views/helpers/LocalAuthentication.desktop.kt b/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/views/helpers/LocalAuthentication.desktop.kt index a251b7dc20..e245efae03 100644 --- a/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/views/helpers/LocalAuthentication.desktop.kt +++ b/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/views/helpers/LocalAuthentication.desktop.kt @@ -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 -> {} } }