desktop: setup passphrase during onboarding (#2987)
* desktop: setup passphrase during onboarding * updated logic * removed unused code * button and starting chat action * better * removed debug code * fallback * focusing and moving focus on desktop text fields * different logic * removed unused variable * divided logic in two functions * enabled keyboard enter * rollback when db deleted by hand on desktop * update texts, font size * stopping chat before other actions --------- Co-authored-by: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com>
This commit is contained in:
parent
8aed568199
commit
aff71c58d7
@ -71,7 +71,7 @@ class SimplexApp: Application(), LifecycleEventObserver {
|
||||
}
|
||||
Lifecycle.Event.ON_RESUME -> {
|
||||
isAppOnForeground = true
|
||||
if (chatModel.onboardingStage.value == OnboardingStage.OnboardingComplete) {
|
||||
if (chatModel.controller.appPrefs.onboardingStage.get() == OnboardingStage.OnboardingComplete) {
|
||||
SimplexService.showBackgroundServiceNoticeIfNeeded()
|
||||
}
|
||||
/**
|
||||
@ -80,7 +80,7 @@ class SimplexApp: Application(), LifecycleEventObserver {
|
||||
* It can happen when app was started and a user enables battery optimization while app in background
|
||||
* */
|
||||
if (chatModel.chatRunning.value != false &&
|
||||
chatModel.onboardingStage.value == OnboardingStage.OnboardingComplete &&
|
||||
chatModel.controller.appPrefs.onboardingStage.get() == OnboardingStage.OnboardingComplete &&
|
||||
appPrefs.notificationsMode.get() == NotificationsMode.SERVICE
|
||||
) {
|
||||
SimplexService.start()
|
||||
@ -191,7 +191,7 @@ class SimplexApp: Application(), LifecycleEventObserver {
|
||||
|
||||
override fun androidChatInitializedAndStarted() {
|
||||
// Prevents from showing "Enable notifications" alert when onboarding wasn't complete yet
|
||||
if (chatModel.onboardingStage.value == OnboardingStage.OnboardingComplete) {
|
||||
if (chatModel.controller.appPrefs.onboardingStage.get() == OnboardingStage.OnboardingComplete) {
|
||||
SimplexService.showBackgroundServiceNoticeIfNeeded()
|
||||
if (appPrefs.notificationsMode.get() == NotificationsMode.SERVICE)
|
||||
withBGApi {
|
||||
|
@ -0,0 +1,106 @@
|
||||
package chat.simplex.common.views.database
|
||||
|
||||
import SectionItemView
|
||||
import SectionTextFooter
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.material.*
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.MutableState
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import androidx.compose.ui.unit.dp
|
||||
import chat.simplex.common.ui.theme.SimplexGreen
|
||||
import chat.simplex.common.views.helpers.*
|
||||
import chat.simplex.res.MR
|
||||
import dev.icerock.moko.resources.compose.painterResource
|
||||
import dev.icerock.moko.resources.compose.stringResource
|
||||
|
||||
@Composable
|
||||
actual fun SavePassphraseSetting(
|
||||
useKeychain: Boolean,
|
||||
initialRandomDBPassphrase: Boolean,
|
||||
storedKey: Boolean,
|
||||
progressIndicator: Boolean,
|
||||
minHeight: Dp,
|
||||
onCheckedChange: (Boolean) -> Unit,
|
||||
) {
|
||||
SectionItemView(minHeight = minHeight) {
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
Icon(
|
||||
if (storedKey) painterResource(MR.images.ic_vpn_key_filled) else painterResource(MR.images.ic_vpn_key_off_filled),
|
||||
stringResource(MR.strings.save_passphrase_in_keychain),
|
||||
tint = if (storedKey) SimplexGreen else MaterialTheme.colors.secondary
|
||||
)
|
||||
Spacer(Modifier.padding(horizontal = 4.dp))
|
||||
Text(
|
||||
stringResource(MR.strings.save_passphrase_in_keychain),
|
||||
Modifier.padding(end = 24.dp),
|
||||
color = Color.Unspecified
|
||||
)
|
||||
Spacer(Modifier.fillMaxWidth().weight(1f))
|
||||
DefaultSwitch(
|
||||
checked = useKeychain,
|
||||
onCheckedChange = onCheckedChange,
|
||||
enabled = !initialRandomDBPassphrase && !progressIndicator
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
actual fun DatabaseEncryptionFooter(
|
||||
useKeychain: MutableState<Boolean>,
|
||||
chatDbEncrypted: Boolean?,
|
||||
storedKey: MutableState<Boolean>,
|
||||
initialRandomDBPassphrase: MutableState<Boolean>,
|
||||
) {
|
||||
if (chatDbEncrypted == false) {
|
||||
SectionTextFooter(generalGetString(MR.strings.database_is_not_encrypted))
|
||||
} else if (useKeychain.value) {
|
||||
if (storedKey.value) {
|
||||
SectionTextFooter(generalGetString(MR.strings.keychain_is_storing_securely))
|
||||
if (initialRandomDBPassphrase.value) {
|
||||
SectionTextFooter(generalGetString(MR.strings.encrypted_with_random_passphrase))
|
||||
} else {
|
||||
SectionTextFooter(annotatedStringResource(MR.strings.impossible_to_recover_passphrase))
|
||||
}
|
||||
} else {
|
||||
SectionTextFooter(generalGetString(MR.strings.keychain_allows_to_receive_ntfs))
|
||||
}
|
||||
} else {
|
||||
SectionTextFooter(generalGetString(MR.strings.you_have_to_enter_passphrase_every_time))
|
||||
SectionTextFooter(annotatedStringResource(MR.strings.impossible_to_recover_passphrase))
|
||||
}
|
||||
}
|
||||
|
||||
actual fun encryptDatabaseSavedAlert(onConfirm: () -> Unit) {
|
||||
AlertManager.shared.showAlertDialog(
|
||||
title = generalGetString(MR.strings.encrypt_database_question),
|
||||
text = generalGetString(MR.strings.database_will_be_encrypted_and_passphrase_stored) + "\n" + storeSecurelySaved(),
|
||||
confirmText = generalGetString(MR.strings.encrypt_database),
|
||||
onConfirm = onConfirm,
|
||||
destructive = true,
|
||||
)
|
||||
}
|
||||
|
||||
actual fun changeDatabaseKeySavedAlert(onConfirm: () -> Unit) {
|
||||
AlertManager.shared.showAlertDialog(
|
||||
title = generalGetString(MR.strings.change_database_passphrase_question),
|
||||
text = generalGetString(MR.strings.database_encryption_will_be_updated) + "\n" + storeSecurelySaved(),
|
||||
confirmText = generalGetString(MR.strings.update_database),
|
||||
onConfirm = onConfirm,
|
||||
destructive = false,
|
||||
)
|
||||
}
|
||||
|
||||
actual fun removePassphraseAlert(onConfirm: () -> Unit) {
|
||||
AlertManager.shared.showAlertDialog(
|
||||
title = generalGetString(MR.strings.remove_passphrase_from_keychain),
|
||||
text = generalGetString(MR.strings.notifications_will_be_hidden) + "\n" + storeSecurelyDanger(),
|
||||
confirmText = generalGetString(MR.strings.remove_passphrase),
|
||||
onConfirm = onConfirm,
|
||||
destructive = true,
|
||||
)
|
||||
}
|
@ -32,8 +32,7 @@ import chat.simplex.res.MR
|
||||
import dev.icerock.moko.resources.compose.painterResource
|
||||
import dev.icerock.moko.resources.compose.stringResource
|
||||
import kotlinx.coroutines.*
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||
import kotlinx.coroutines.flow.*
|
||||
|
||||
data class SettingsViewState(
|
||||
val userPickerState: MutableStateFlow<AnimatedViewState>,
|
||||
@ -64,7 +63,7 @@ fun MainScreen() {
|
||||
if (
|
||||
!chatModel.controller.appPrefs.laNoticeShown.get()
|
||||
&& showAdvertiseLAAlert
|
||||
&& chatModel.onboardingStage.value == OnboardingStage.OnboardingComplete
|
||||
&& chatModel.controller.appPrefs.onboardingStage.get() == OnboardingStage.OnboardingComplete
|
||||
&& chatModel.chats.isNotEmpty()
|
||||
&& chatModel.activeCallInvitation.value == null
|
||||
) {
|
||||
@ -102,7 +101,10 @@ fun MainScreen() {
|
||||
}
|
||||
|
||||
Box {
|
||||
val onboarding = chatModel.onboardingStage.value
|
||||
var onboarding by remember { mutableStateOf(chatModel.controller.appPrefs.onboardingStage.get()) }
|
||||
LaunchedEffect(Unit) {
|
||||
snapshotFlow { chatModel.controller.appPrefs.onboardingStage.state.value }.distinctUntilChanged().collect { onboarding = it }
|
||||
}
|
||||
val userCreated = chatModel.userCreated.value
|
||||
var showInitializationView by remember { mutableStateOf(false) }
|
||||
when {
|
||||
@ -112,7 +114,7 @@ fun MainScreen() {
|
||||
DatabaseErrorView(chatModel.chatDbStatus, chatModel.controller.appPrefs)
|
||||
}
|
||||
}
|
||||
onboarding == null || userCreated == null -> SplashView()
|
||||
remember { chatModel.chatDbEncrypted }.value == null || userCreated == null -> SplashView()
|
||||
onboarding == OnboardingStage.OnboardingComplete && userCreated -> {
|
||||
Box {
|
||||
showAdvertiseLAAlert = true
|
||||
@ -134,6 +136,7 @@ fun MainScreen() {
|
||||
}
|
||||
}
|
||||
onboarding == OnboardingStage.Step2_CreateProfile -> CreateProfile(chatModel) {}
|
||||
onboarding == OnboardingStage.Step2_5_SetupDatabasePassphrase -> SetupDatabasePassphrase(chatModel)
|
||||
onboarding == OnboardingStage.Step3_CreateSimpleXAddress -> CreateSimpleXAddress(chatModel)
|
||||
onboarding == OnboardingStage.Step4_SetNotificationsMode -> SetNotificationsMode(chatModel)
|
||||
}
|
||||
|
@ -38,7 +38,6 @@ import kotlin.time.*
|
||||
@Stable
|
||||
object ChatModel {
|
||||
val controller: ChatController = ChatController
|
||||
val onboardingStage = mutableStateOf<OnboardingStage?>(null)
|
||||
val setDeliveryReceipts = mutableStateOf(false)
|
||||
val currentUser = mutableStateOf<User?>(null)
|
||||
val users = mutableStateListOf<UserInfo>()
|
||||
|
@ -50,17 +50,16 @@ suspend fun initChatController(useKey: String? = null, confirmMigrations: Migrat
|
||||
if (user == null) {
|
||||
chatModel.controller.appPrefs.onboardingStage.set(OnboardingStage.Step1_SimpleXInfo)
|
||||
chatModel.controller.appPrefs.privacyDeliveryReceiptsSet.set(true)
|
||||
chatModel.onboardingStage.value = OnboardingStage.Step1_SimpleXInfo
|
||||
chatModel.currentUser.value = null
|
||||
chatModel.users.clear()
|
||||
} else {
|
||||
val savedOnboardingStage = appPreferences.onboardingStage.get()
|
||||
chatModel.onboardingStage.value = if (listOf(OnboardingStage.Step1_SimpleXInfo, OnboardingStage.Step2_CreateProfile).contains(savedOnboardingStage) && chatModel.users.size == 1) {
|
||||
appPreferences.onboardingStage.set(if (listOf(OnboardingStage.Step1_SimpleXInfo, OnboardingStage.Step2_CreateProfile).contains(savedOnboardingStage) && chatModel.users.size == 1) {
|
||||
OnboardingStage.Step3_CreateSimpleXAddress
|
||||
} else {
|
||||
savedOnboardingStage
|
||||
}
|
||||
if (chatModel.onboardingStage.value == OnboardingStage.OnboardingComplete && !chatModel.controller.appPrefs.privacyDeliveryReceiptsSet.get()) {
|
||||
})
|
||||
if (appPreferences.onboardingStage.get() == OnboardingStage.OnboardingComplete && !chatModel.controller.appPrefs.privacyDeliveryReceiptsSet.get()) {
|
||||
chatModel.setDeliveryReceipts.value = true
|
||||
}
|
||||
chatController.startChat(user)
|
||||
|
@ -100,7 +100,7 @@ abstract class NtfManager {
|
||||
if (chatModel.chatRunning.value == null) {
|
||||
val step = 50L
|
||||
for (i in 0..(timeout / step)) {
|
||||
if (chatModel.chatRunning.value == true || chatModel.onboardingStage.value == OnboardingStage.Step1_SimpleXInfo) {
|
||||
if (chatModel.chatRunning.value == true || chatModel.controller.appPrefs.onboardingStage.get() == OnboardingStage.Step1_SimpleXInfo) {
|
||||
break
|
||||
}
|
||||
delay(step)
|
||||
|
@ -24,6 +24,7 @@ import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import chat.simplex.common.model.ChatModel
|
||||
import chat.simplex.common.model.Profile
|
||||
import chat.simplex.common.platform.appPlatform
|
||||
import chat.simplex.common.platform.navigationBarsWithImePadding
|
||||
import chat.simplex.common.ui.theme.*
|
||||
import chat.simplex.common.views.helpers.*
|
||||
@ -88,14 +89,20 @@ fun CreateProfilePanel(chatModel: ChatModel, close: () -> Unit) {
|
||||
icon = painterResource(MR.images.ic_arrow_back_ios_new),
|
||||
textDecoration = TextDecoration.None,
|
||||
fontWeight = FontWeight.Medium
|
||||
) { chatModel.onboardingStage.value = OnboardingStage.Step1_SimpleXInfo }
|
||||
) { chatModel.controller.appPrefs.onboardingStage.set(OnboardingStage.Step1_SimpleXInfo) }
|
||||
}
|
||||
Spacer(Modifier.fillMaxWidth().weight(1f))
|
||||
val enabled = displayName.value.isNotEmpty() && isValidDisplayName(displayName.value)
|
||||
val createModifier: Modifier
|
||||
val createColor: Color
|
||||
if (enabled) {
|
||||
createModifier = Modifier.clickable { createProfile(chatModel, displayName.value, fullName.value, close) }.padding(8.dp)
|
||||
createModifier = Modifier.clickable {
|
||||
if (chatModel.controller.appPrefs.onboardingStage.get() == OnboardingStage.OnboardingComplete) {
|
||||
createProfileInProfiles(chatModel, displayName.value, fullName.value, close)
|
||||
} else {
|
||||
createProfileOnboarding(chatModel, displayName.value, fullName.value, close)
|
||||
}
|
||||
}.padding(8.dp)
|
||||
createColor = MaterialTheme.colors.primary
|
||||
} else {
|
||||
createModifier = Modifier.padding(8.dp)
|
||||
@ -116,7 +123,7 @@ fun CreateProfilePanel(chatModel: ChatModel, close: () -> Unit) {
|
||||
}
|
||||
}
|
||||
|
||||
fun createProfile(chatModel: ChatModel, displayName: String, fullName: String, close: () -> Unit) {
|
||||
fun createProfileInProfiles(chatModel: ChatModel, displayName: String, fullName: String, close: () -> Unit) {
|
||||
withApi {
|
||||
val user = chatModel.controller.apiCreateActiveUser(
|
||||
Profile(displayName, fullName, null)
|
||||
@ -125,16 +132,32 @@ fun createProfile(chatModel: ChatModel, displayName: String, fullName: String, c
|
||||
if (chatModel.users.isEmpty()) {
|
||||
chatModel.controller.startChat(user)
|
||||
chatModel.controller.appPrefs.onboardingStage.set(OnboardingStage.Step3_CreateSimpleXAddress)
|
||||
chatModel.onboardingStage.value = OnboardingStage.Step3_CreateSimpleXAddress
|
||||
} else {
|
||||
val users = chatModel.controller.listUsers()
|
||||
chatModel.users.clear()
|
||||
chatModel.users.addAll(users)
|
||||
chatModel.controller.getUserChatData()
|
||||
close()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun createProfileOnboarding(chatModel: ChatModel, displayName: String, fullName: String, close: () -> Unit) {
|
||||
withApi {
|
||||
chatModel.controller.apiCreateActiveUser(
|
||||
Profile(displayName, fullName, null)
|
||||
) ?: return@withApi
|
||||
val onboardingStage = chatModel.controller.appPrefs.onboardingStage
|
||||
if (chatModel.users.isEmpty()) {
|
||||
onboardingStage.set(if (appPlatform.isDesktop && chatModel.controller.appPrefs.initialRandomDBPassphrase.get()) {
|
||||
OnboardingStage.Step2_5_SetupDatabasePassphrase
|
||||
} else {
|
||||
OnboardingStage.Step3_CreateSimpleXAddress
|
||||
})
|
||||
} else {
|
||||
// the next two lines are only needed for failure case when because of the database error the app gets stuck on on-boarding screen,
|
||||
// this will get it unstuck.
|
||||
chatModel.controller.appPrefs.onboardingStage.set(OnboardingStage.OnboardingComplete)
|
||||
chatModel.onboardingStage.value = OnboardingStage.OnboardingComplete
|
||||
onboardingStage.set(OnboardingStage.OnboardingComplete)
|
||||
close()
|
||||
}
|
||||
}
|
||||
|
@ -30,6 +30,7 @@ import chat.simplex.common.model.*
|
||||
import chat.simplex.common.ui.theme.*
|
||||
import chat.simplex.common.views.helpers.*
|
||||
import chat.simplex.common.model.*
|
||||
import chat.simplex.common.platform.appPlatform
|
||||
import chat.simplex.res.MR
|
||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||
import kotlinx.datetime.Clock
|
||||
@ -61,46 +62,8 @@ fun DatabaseEncryptionView(m: ChatModel) {
|
||||
initialRandomDBPassphrase,
|
||||
progressIndicator,
|
||||
onConfirmEncrypt = {
|
||||
progressIndicator.value = true
|
||||
withApi {
|
||||
try {
|
||||
prefs.encryptionStartedAt.set(Clock.System.now())
|
||||
val error = m.controller.apiStorageEncryption(currentKey.value, newKey.value)
|
||||
prefs.encryptionStartedAt.set(null)
|
||||
val sqliteError = ((error?.chatError as? ChatError.ChatErrorDatabase)?.databaseError as? DatabaseError.ErrorExport)?.sqliteError
|
||||
when {
|
||||
sqliteError is SQLiteError.ErrorNotADatabase -> {
|
||||
operationEnded(m, progressIndicator) {
|
||||
AlertManager.shared.showAlertMsg(
|
||||
generalGetString(MR.strings.wrong_passphrase_title),
|
||||
generalGetString(MR.strings.enter_correct_current_passphrase)
|
||||
)
|
||||
}
|
||||
}
|
||||
error != null -> {
|
||||
operationEnded(m, progressIndicator) {
|
||||
AlertManager.shared.showAlertMsg(generalGetString(MR.strings.error_encrypting_database),
|
||||
"failed to set storage encryption: ${error.responseType} ${error.details}"
|
||||
)
|
||||
}
|
||||
}
|
||||
else -> {
|
||||
prefs.initialRandomDBPassphrase.set(false)
|
||||
initialRandomDBPassphrase.value = false
|
||||
if (useKeychain.value) {
|
||||
DatabaseUtils.ksDatabasePassword.set(newKey.value)
|
||||
}
|
||||
resetFormAfterEncryption(m, initialRandomDBPassphrase, currentKey, newKey, confirmNewKey, storedKey, useKeychain.value)
|
||||
operationEnded(m, progressIndicator) {
|
||||
AlertManager.shared.showAlertMsg(generalGetString(MR.strings.database_encrypted))
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
operationEnded(m, progressIndicator) {
|
||||
AlertManager.shared.showAlertMsg(generalGetString(MR.strings.error_encrypting_database), e.stackTraceToString())
|
||||
}
|
||||
}
|
||||
encryptDatabase(currentKey, newKey, confirmNewKey, initialRandomDBPassphrase, useKeychain, storedKey, progressIndicator)
|
||||
}
|
||||
}
|
||||
)
|
||||
@ -143,17 +106,11 @@ fun DatabaseEncryptionLayout(
|
||||
if (checked) {
|
||||
setUseKeychain(true, useKeychain, prefs)
|
||||
} else if (storedKey.value) {
|
||||
AlertManager.shared.showAlertDialog(
|
||||
title = generalGetString(MR.strings.remove_passphrase_from_keychain),
|
||||
text = generalGetString(MR.strings.notifications_will_be_hidden) + "\n" + storeSecurelyDanger(),
|
||||
confirmText = generalGetString(MR.strings.remove_passphrase),
|
||||
onConfirm = {
|
||||
DatabaseUtils.ksDatabasePassword.remove()
|
||||
setUseKeychain(false, useKeychain, prefs)
|
||||
storedKey.value = false
|
||||
},
|
||||
destructive = true,
|
||||
)
|
||||
removePassphraseAlert {
|
||||
DatabaseUtils.ksDatabasePassword.remove()
|
||||
setUseKeychain(false, useKeychain, prefs)
|
||||
storedKey.value = false
|
||||
}
|
||||
} else {
|
||||
setUseKeychain(false, useKeychain, prefs)
|
||||
}
|
||||
@ -217,37 +174,13 @@ fun DatabaseEncryptionLayout(
|
||||
}
|
||||
|
||||
Column {
|
||||
if (chatDbEncrypted == false) {
|
||||
SectionTextFooter(generalGetString(MR.strings.database_is_not_encrypted))
|
||||
} else if (useKeychain.value) {
|
||||
if (storedKey.value) {
|
||||
SectionTextFooter(generalGetString(MR.strings.keychain_is_storing_securely))
|
||||
if (initialRandomDBPassphrase.value) {
|
||||
SectionTextFooter(generalGetString(MR.strings.encrypted_with_random_passphrase))
|
||||
} else {
|
||||
SectionTextFooter(annotatedStringResource(MR.strings.impossible_to_recover_passphrase))
|
||||
}
|
||||
} else {
|
||||
SectionTextFooter(generalGetString(MR.strings.keychain_allows_to_receive_ntfs))
|
||||
}
|
||||
} else {
|
||||
SectionTextFooter(generalGetString(MR.strings.you_have_to_enter_passphrase_every_time))
|
||||
SectionTextFooter(annotatedStringResource(MR.strings.impossible_to_recover_passphrase))
|
||||
}
|
||||
DatabaseEncryptionFooter(useKeychain, chatDbEncrypted, storedKey, initialRandomDBPassphrase)
|
||||
}
|
||||
SectionBottomSpacer()
|
||||
}
|
||||
}
|
||||
|
||||
fun encryptDatabaseSavedAlert(onConfirm: () -> Unit) {
|
||||
AlertManager.shared.showAlertDialog(
|
||||
title = generalGetString(MR.strings.encrypt_database_question),
|
||||
text = generalGetString(MR.strings.database_will_be_encrypted_and_passphrase_stored) + "\n" + storeSecurelySaved(),
|
||||
confirmText = generalGetString(MR.strings.encrypt_database),
|
||||
onConfirm = onConfirm,
|
||||
destructive = true,
|
||||
)
|
||||
}
|
||||
expect fun encryptDatabaseSavedAlert(onConfirm: () -> Unit)
|
||||
|
||||
fun encryptDatabaseAlert(onConfirm: () -> Unit) {
|
||||
AlertManager.shared.showAlertDialog(
|
||||
@ -259,15 +192,7 @@ fun encryptDatabaseAlert(onConfirm: () -> Unit) {
|
||||
)
|
||||
}
|
||||
|
||||
fun changeDatabaseKeySavedAlert(onConfirm: () -> Unit) {
|
||||
AlertManager.shared.showAlertDialog(
|
||||
title = generalGetString(MR.strings.change_database_passphrase_question),
|
||||
text = generalGetString(MR.strings.database_encryption_will_be_updated) + "\n" + storeSecurelySaved(),
|
||||
confirmText = generalGetString(MR.strings.update_database),
|
||||
onConfirm = onConfirm,
|
||||
destructive = false,
|
||||
)
|
||||
}
|
||||
expect fun changeDatabaseKeySavedAlert(onConfirm: () -> Unit)
|
||||
|
||||
fun changeDatabaseKeyAlert(onConfirm: () -> Unit) {
|
||||
AlertManager.shared.showAlertDialog(
|
||||
@ -279,37 +204,25 @@ fun changeDatabaseKeyAlert(onConfirm: () -> Unit) {
|
||||
)
|
||||
}
|
||||
|
||||
expect fun removePassphraseAlert(onConfirm: () -> Unit)
|
||||
|
||||
@Composable
|
||||
fun SavePassphraseSetting(
|
||||
expect fun SavePassphraseSetting(
|
||||
useKeychain: Boolean,
|
||||
initialRandomDBPassphrase: Boolean,
|
||||
storedKey: Boolean,
|
||||
progressIndicator: Boolean,
|
||||
minHeight: Dp = TextFieldDefaults.MinHeight,
|
||||
onCheckedChange: (Boolean) -> Unit,
|
||||
) {
|
||||
SectionItemView(minHeight = minHeight) {
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
Icon(
|
||||
if (storedKey) painterResource(MR.images.ic_vpn_key_filled) else painterResource(MR.images.ic_vpn_key_off_filled),
|
||||
stringResource(MR.strings.save_passphrase_in_keychain),
|
||||
tint = if (storedKey) SimplexGreen else MaterialTheme.colors.secondary
|
||||
)
|
||||
Spacer(Modifier.padding(horizontal = 4.dp))
|
||||
Text(
|
||||
stringResource(MR.strings.save_passphrase_in_keychain),
|
||||
Modifier.padding(end = 24.dp),
|
||||
color = Color.Unspecified
|
||||
)
|
||||
Spacer(Modifier.fillMaxWidth().weight(1f))
|
||||
DefaultSwitch(
|
||||
checked = useKeychain,
|
||||
onCheckedChange = onCheckedChange,
|
||||
enabled = !initialRandomDBPassphrase && !progressIndicator
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
@Composable
|
||||
expect fun DatabaseEncryptionFooter(
|
||||
useKeychain: MutableState<Boolean>,
|
||||
chatDbEncrypted: Boolean?,
|
||||
storedKey: MutableState<Boolean>,
|
||||
initialRandomDBPassphrase: MutableState<Boolean>,
|
||||
)
|
||||
|
||||
fun resetFormAfterEncryption(
|
||||
m: ChatModel,
|
||||
@ -443,6 +356,62 @@ fun PassphraseField(
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun encryptDatabase(
|
||||
currentKey: MutableState<String>,
|
||||
newKey: MutableState<String>,
|
||||
confirmNewKey: MutableState<String>,
|
||||
initialRandomDBPassphrase: MutableState<Boolean>,
|
||||
useKeychain: MutableState<Boolean>,
|
||||
storedKey: MutableState<Boolean>,
|
||||
progressIndicator: MutableState<Boolean>
|
||||
): Boolean {
|
||||
val m = ChatModel
|
||||
val prefs = ChatController.appPrefs
|
||||
progressIndicator.value = true
|
||||
return try {
|
||||
prefs.encryptionStartedAt.set(Clock.System.now())
|
||||
val error = m.controller.apiStorageEncryption(currentKey.value, newKey.value)
|
||||
prefs.encryptionStartedAt.set(null)
|
||||
val sqliteError = ((error?.chatError as? ChatError.ChatErrorDatabase)?.databaseError as? DatabaseError.ErrorExport)?.sqliteError
|
||||
when {
|
||||
sqliteError is SQLiteError.ErrorNotADatabase -> {
|
||||
operationEnded(m, progressIndicator) {
|
||||
AlertManager.shared.showAlertMsg(
|
||||
generalGetString(MR.strings.wrong_passphrase_title),
|
||||
generalGetString(MR.strings.enter_correct_current_passphrase)
|
||||
)
|
||||
}
|
||||
false
|
||||
}
|
||||
error != null -> {
|
||||
operationEnded(m, progressIndicator) {
|
||||
AlertManager.shared.showAlertMsg(generalGetString(MR.strings.error_encrypting_database),
|
||||
"failed to set storage encryption: ${error.responseType} ${error.details}"
|
||||
)
|
||||
}
|
||||
false
|
||||
}
|
||||
else -> {
|
||||
prefs.initialRandomDBPassphrase.set(false)
|
||||
initialRandomDBPassphrase.value = false
|
||||
if (useKeychain.value) {
|
||||
DatabaseUtils.ksDatabasePassword.set(newKey.value)
|
||||
}
|
||||
resetFormAfterEncryption(m, initialRandomDBPassphrase, currentKey, newKey, confirmNewKey, storedKey, useKeychain.value)
|
||||
operationEnded(m, progressIndicator) {
|
||||
AlertManager.shared.showAlertMsg(generalGetString(MR.strings.database_encrypted))
|
||||
}
|
||||
true
|
||||
}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
operationEnded(m, progressIndicator) {
|
||||
AlertManager.shared.showAlertMsg(generalGetString(MR.strings.error_encrypting_database), e.stackTraceToString())
|
||||
}
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
// based on https://generatepasswords.org/how-to-calculate-entropy/
|
||||
private fun passphraseEntropy(s: String): Double {
|
||||
var hasDigits = false
|
||||
|
@ -12,6 +12,9 @@ import androidx.compose.material.*
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.focus.FocusRequester
|
||||
import androidx.compose.ui.focus.focusRequester
|
||||
import androidx.compose.ui.input.key.*
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.dp
|
||||
import chat.simplex.common.model.AppPreferences
|
||||
@ -252,6 +255,11 @@ private fun mtrErrorDescription(err: MTRError): String =
|
||||
|
||||
@Composable
|
||||
private fun DatabaseKeyField(text: MutableState<String>, enabled: Boolean, onClick: (() -> Unit)? = null) {
|
||||
val focusRequester = remember { FocusRequester() }
|
||||
LaunchedEffect(Unit) {
|
||||
delay(100L)
|
||||
focusRequester.requestFocus()
|
||||
}
|
||||
PassphraseField(
|
||||
text,
|
||||
generalGetString(MR.strings.enter_passphrase),
|
||||
@ -259,7 +267,15 @@ private fun DatabaseKeyField(text: MutableState<String>, enabled: Boolean, onCli
|
||||
keyboardActions = KeyboardActions(onDone = if (enabled) {
|
||||
{ onClick?.invoke() }
|
||||
} else null
|
||||
)
|
||||
),
|
||||
modifier = Modifier.focusRequester(focusRequester).onPreviewKeyEvent {
|
||||
if (onClick != null && it.key == Key.Enter && it.type == KeyEventType.KeyUp) {
|
||||
onClick()
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -73,6 +73,7 @@ fun DatabaseView(
|
||||
m.chatDbChanged.value,
|
||||
useKeychain.value,
|
||||
m.chatDbEncrypted.value,
|
||||
m.controller.appPrefs.storeDBPassphrase.state.value,
|
||||
m.controller.appPrefs.initialRandomDBPassphrase,
|
||||
importArchiveLauncher,
|
||||
chatArchiveName,
|
||||
@ -122,6 +123,7 @@ fun DatabaseLayout(
|
||||
chatDbChanged: Boolean,
|
||||
useKeyChain: Boolean,
|
||||
chatDbEncrypted: Boolean?,
|
||||
passphraseSaved: Boolean,
|
||||
initialRandomDBPassphrase: SharedPreference<Boolean>,
|
||||
importArchiveLauncher: FileChooserLauncher,
|
||||
chatArchiveName: MutableState<String?>,
|
||||
@ -182,7 +184,7 @@ fun DatabaseLayout(
|
||||
else painterResource(MR.images.ic_lock),
|
||||
stringResource(MR.strings.database_passphrase),
|
||||
click = showSettingsModal() { DatabaseEncryptionView(it) },
|
||||
iconColor = if (unencrypted) WarningOrange else MaterialTheme.colors.secondary,
|
||||
iconColor = if (unencrypted || (appPlatform.isDesktop && passphraseSaved)) WarningOrange else MaterialTheme.colors.secondary,
|
||||
disabled = operationsDisabled
|
||||
)
|
||||
SettingsActionItem(
|
||||
@ -657,6 +659,7 @@ fun PreviewDatabaseLayout() {
|
||||
chatDbChanged = false,
|
||||
useKeyChain = false,
|
||||
chatDbEncrypted = false,
|
||||
passphraseSaved = false,
|
||||
initialRandomDBPassphrase = SharedPreference({ true }, {}),
|
||||
importArchiveLauncher = rememberFileChooserLauncher(true) {},
|
||||
chatArchiveName = remember { mutableStateOf("dummy_archive") },
|
||||
|
@ -101,6 +101,10 @@ class AlertManager {
|
||||
Modifier.fillMaxWidth().padding(horizontal = DEFAULT_PADDING).padding(bottom = DEFAULT_PADDING_HALF),
|
||||
horizontalArrangement = Arrangement.SpaceBetween
|
||||
) {
|
||||
val focusRequester = remember { FocusRequester() }
|
||||
LaunchedEffect(Unit) {
|
||||
focusRequester.requestFocus()
|
||||
}
|
||||
TextButton(onClick = {
|
||||
onDismiss?.invoke()
|
||||
hideAlert()
|
||||
@ -108,7 +112,7 @@ class AlertManager {
|
||||
TextButton(onClick = {
|
||||
onConfirm?.invoke()
|
||||
hideAlert()
|
||||
}) { Text(confirmText, color = if (destructive) MaterialTheme.colors.error else Color.Unspecified) }
|
||||
}, Modifier.focusRequester(focusRequester)) { Text(confirmText, color = if (destructive) MaterialTheme.colors.error else Color.Unspecified) }
|
||||
}
|
||||
},
|
||||
shape = RoundedCornerShape(corner = CornerSize(25.dp))
|
||||
|
@ -54,6 +54,12 @@ object DatabaseUtils {
|
||||
} else {
|
||||
dbKey = ksDatabasePassword.get() ?: ""
|
||||
}
|
||||
} else if (appPlatform.isDesktop && !hasDatabase(dataDir.absolutePath)) {
|
||||
// In case of database was deleted by hand
|
||||
dbKey = randomDatabasePassword()
|
||||
ksDatabasePassword.set(dbKey)
|
||||
appPreferences.initialRandomDBPassphrase.set(true)
|
||||
appPreferences.storeDBPassphrase.set(true)
|
||||
}
|
||||
return dbKey
|
||||
}
|
||||
|
@ -12,6 +12,7 @@ import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.painter.Painter
|
||||
import androidx.compose.ui.text.TextStyle
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.style.TextDecoration
|
||||
import androidx.compose.ui.unit.dp
|
||||
@ -66,11 +67,13 @@ fun SimpleButton(
|
||||
fun SimpleButtonIconEnded(
|
||||
text: String,
|
||||
icon: Painter,
|
||||
style: TextStyle = MaterialTheme.typography.caption,
|
||||
color: Color = MaterialTheme.colors.primary,
|
||||
disabled: Boolean = false,
|
||||
click: () -> Unit
|
||||
) {
|
||||
SimpleButtonFrame(click) {
|
||||
Text(text, style = MaterialTheme.typography.caption, color = color)
|
||||
SimpleButtonFrame(click, disabled = disabled) {
|
||||
Text(text, style = style, color = color)
|
||||
Icon(
|
||||
icon, text, tint = color,
|
||||
modifier = Modifier.padding(start = 8.dp)
|
||||
|
@ -66,7 +66,6 @@ private fun deleteStorageAndRestart(m: ChatModel, password: String, completed: (
|
||||
val createdUser = m.controller.apiCreateActiveUser(profile, pastTimestamp = true)
|
||||
m.currentUser.value = createdUser
|
||||
m.controller.appPrefs.onboardingStage.set(OnboardingStage.OnboardingComplete)
|
||||
m.onboardingStage.value = OnboardingStage.OnboardingComplete
|
||||
if (createdUser != null) {
|
||||
m.controller.startChat(createdUser)
|
||||
}
|
||||
|
@ -14,8 +14,7 @@ import dev.icerock.moko.resources.compose.painterResource
|
||||
import dev.icerock.moko.resources.compose.stringResource
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.unit.dp
|
||||
import chat.simplex.common.model.ChatModel
|
||||
import chat.simplex.common.model.UserContactLinkRec
|
||||
import chat.simplex.common.model.*
|
||||
import chat.simplex.common.platform.*
|
||||
import chat.simplex.common.ui.theme.*
|
||||
import chat.simplex.common.views.helpers.*
|
||||
@ -29,6 +28,10 @@ fun CreateSimpleXAddress(m: ChatModel) {
|
||||
val clipboard = LocalClipboardManager.current
|
||||
val uriHandler = LocalUriHandler.current
|
||||
|
||||
LaunchedEffect(Unit) {
|
||||
prepareChatBeforeAddressCreation()
|
||||
}
|
||||
|
||||
CreateSimpleXAddressLayout(
|
||||
userAddress.value,
|
||||
share = { address: String -> clipboard.shareText(address) },
|
||||
@ -63,7 +66,6 @@ fun CreateSimpleXAddress(m: ChatModel) {
|
||||
OnboardingStage.OnboardingComplete
|
||||
}
|
||||
m.controller.appPrefs.onboardingStage.set(next)
|
||||
m.onboardingStage.value = next
|
||||
},
|
||||
)
|
||||
|
||||
@ -172,3 +174,19 @@ private fun ProgressIndicator() {
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun prepareChatBeforeAddressCreation() {
|
||||
if (chatModel.users.isNotEmpty()) return
|
||||
withApi {
|
||||
val user = chatModel.controller.apiGetActiveUser() ?: return@withApi
|
||||
chatModel.currentUser.value = user
|
||||
if (chatModel.users.isEmpty()) {
|
||||
chatModel.controller.startChat(user)
|
||||
} else {
|
||||
val users = chatModel.controller.listUsers()
|
||||
chatModel.users.clear()
|
||||
chatModel.users.addAll(users)
|
||||
chatModel.controller.getUserChatData()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -13,8 +13,7 @@ import androidx.compose.ui.text.*
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import chat.simplex.common.model.ChatController
|
||||
import chat.simplex.common.model.User
|
||||
import chat.simplex.common.model.*
|
||||
import chat.simplex.common.ui.theme.*
|
||||
import chat.simplex.common.views.chat.item.MarkdownText
|
||||
import chat.simplex.common.views.helpers.*
|
||||
@ -22,7 +21,7 @@ import chat.simplex.res.MR
|
||||
import dev.icerock.moko.resources.StringResource
|
||||
|
||||
@Composable
|
||||
fun HowItWorks(user: User?, onboardingStage: MutableState<OnboardingStage?>? = null) {
|
||||
fun HowItWorks(user: User?, onboardingStage: SharedPreference<OnboardingStage>? = null) {
|
||||
Column(Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = DEFAULT_PADDING),
|
||||
|
@ -14,6 +14,7 @@ import kotlinx.coroutines.launch
|
||||
enum class OnboardingStage {
|
||||
Step1_SimpleXInfo,
|
||||
Step2_CreateProfile,
|
||||
Step2_5_SetupDatabasePassphrase,
|
||||
Step3_CreateSimpleXAddress,
|
||||
Step4_SetNotificationsMode,
|
||||
OnboardingComplete
|
||||
|
@ -41,7 +41,7 @@ fun SetNotificationsMode(m: ChatModel) {
|
||||
}
|
||||
Spacer(Modifier.fillMaxHeight().weight(1f))
|
||||
Box(Modifier.fillMaxWidth().padding(bottom = DEFAULT_PADDING_HALF), contentAlignment = Alignment.Center) {
|
||||
OnboardingActionButton(MR.strings.use_chat, OnboardingStage.OnboardingComplete, m.onboardingStage, false) {
|
||||
OnboardingActionButton(MR.strings.use_chat, OnboardingStage.OnboardingComplete, false) {
|
||||
changeNotificationsMode(currentMode.value, m)
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,233 @@
|
||||
package chat.simplex.common.views.onboarding
|
||||
|
||||
import SectionBottomSpacer
|
||||
import SectionItemView
|
||||
import SectionItemViewSpaceBetween
|
||||
import SectionTextFooter
|
||||
import SectionView
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.text.KeyboardActions
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material.*
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.runtime.saveable.rememberSaveable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.focus.*
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.input.key.*
|
||||
import androidx.compose.ui.platform.LocalFocusManager
|
||||
import androidx.compose.ui.text.input.ImeAction
|
||||
import dev.icerock.moko.resources.compose.painterResource
|
||||
import dev.icerock.moko.resources.compose.stringResource
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import androidx.compose.ui.unit.dp
|
||||
import chat.simplex.common.model.*
|
||||
import chat.simplex.common.platform.*
|
||||
import chat.simplex.common.ui.theme.*
|
||||
import chat.simplex.common.views.database.*
|
||||
import chat.simplex.common.views.helpers.*
|
||||
import chat.simplex.res.MR
|
||||
import kotlinx.coroutines.delay
|
||||
|
||||
@Composable
|
||||
fun SetupDatabasePassphrase(m: ChatModel) {
|
||||
val progressIndicator = remember { mutableStateOf(false) }
|
||||
val prefs = m.controller.appPrefs
|
||||
val saveInPreferences = remember { mutableStateOf(prefs.storeDBPassphrase.get()) }
|
||||
val initialRandomDBPassphrase = remember { mutableStateOf(prefs.initialRandomDBPassphrase.get()) }
|
||||
// Do not do rememberSaveable on current key to prevent saving it on disk in clear text
|
||||
val currentKey = remember { mutableStateOf(if (initialRandomDBPassphrase.value) DatabaseUtils.ksDatabasePassword.get() ?: "" else "") }
|
||||
val newKey = rememberSaveable { mutableStateOf("") }
|
||||
val confirmNewKey = rememberSaveable { mutableStateOf("") }
|
||||
fun nextStep() {
|
||||
m.controller.appPrefs.onboardingStage.set(OnboardingStage.Step3_CreateSimpleXAddress)
|
||||
}
|
||||
SetupDatabasePassphraseLayout(
|
||||
currentKey,
|
||||
newKey,
|
||||
confirmNewKey,
|
||||
progressIndicator,
|
||||
onConfirmEncrypt = {
|
||||
withApi {
|
||||
if (m.chatRunning.value == true) {
|
||||
// Stop chat if it's started before doing anything
|
||||
stopChatAsync(m)
|
||||
}
|
||||
prefs.storeDBPassphrase.set(false)
|
||||
|
||||
val newKeyValue = newKey.value
|
||||
val success = encryptDatabase(currentKey, newKey, confirmNewKey, mutableStateOf(true), saveInPreferences, mutableStateOf(true), progressIndicator)
|
||||
if (success) {
|
||||
startChat(newKeyValue)
|
||||
nextStep()
|
||||
} else {
|
||||
// Rollback in case of it is finished with error in order to allow to repeat the process again
|
||||
prefs.storeDBPassphrase.set(true)
|
||||
}
|
||||
}
|
||||
},
|
||||
nextStep = ::nextStep,
|
||||
)
|
||||
|
||||
if (progressIndicator.value) {
|
||||
ProgressIndicator()
|
||||
}
|
||||
|
||||
DisposableEffect(Unit) {
|
||||
onDispose {
|
||||
if (m.chatRunning.value != true) {
|
||||
withBGApi {
|
||||
val user = chatController.apiGetActiveUser()
|
||||
if (user != null) {
|
||||
m.controller.startChat(user)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun SetupDatabasePassphraseLayout(
|
||||
currentKey: MutableState<String>,
|
||||
newKey: MutableState<String>,
|
||||
confirmNewKey: MutableState<String>,
|
||||
progressIndicator: MutableState<Boolean>,
|
||||
onConfirmEncrypt: () -> Unit,
|
||||
nextStep: () -> Unit,
|
||||
) {
|
||||
Column(
|
||||
Modifier.fillMaxSize().verticalScroll(rememberScrollState()).padding(top = DEFAULT_PADDING),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
) {
|
||||
AppBarTitle(stringResource(MR.strings.setup_database_passphrase))
|
||||
|
||||
Spacer(Modifier.weight(1f))
|
||||
|
||||
Column(Modifier.width(600.dp)) {
|
||||
val focusRequester = remember { FocusRequester() }
|
||||
val focusManager = LocalFocusManager.current
|
||||
LaunchedEffect(Unit) {
|
||||
delay(100L)
|
||||
focusRequester.requestFocus()
|
||||
}
|
||||
PassphraseField(
|
||||
newKey,
|
||||
generalGetString(MR.strings.new_passphrase),
|
||||
modifier = Modifier
|
||||
.padding(horizontal = DEFAULT_PADDING)
|
||||
.focusRequester(focusRequester)
|
||||
.onPreviewKeyEvent {
|
||||
if (it.key == Key.Enter && it.type == KeyEventType.KeyUp) {
|
||||
focusManager.moveFocus(FocusDirection.Down)
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
},
|
||||
showStrength = true,
|
||||
isValid = ::validKey,
|
||||
keyboardActions = KeyboardActions(onNext = { defaultKeyboardAction(ImeAction.Next) }),
|
||||
)
|
||||
val onClickUpdate = {
|
||||
// Don't do things concurrently. Shouldn't be here concurrently, just in case
|
||||
if (!progressIndicator.value) {
|
||||
encryptDatabaseAlert(onConfirmEncrypt)
|
||||
}
|
||||
}
|
||||
val disabled = currentKey.value == newKey.value ||
|
||||
newKey.value != confirmNewKey.value ||
|
||||
newKey.value.isEmpty() ||
|
||||
!validKey(currentKey.value) ||
|
||||
!validKey(newKey.value) ||
|
||||
progressIndicator.value
|
||||
|
||||
PassphraseField(
|
||||
confirmNewKey,
|
||||
generalGetString(MR.strings.confirm_new_passphrase),
|
||||
modifier = Modifier
|
||||
.padding(horizontal = DEFAULT_PADDING)
|
||||
.onPreviewKeyEvent {
|
||||
if (!disabled && it.key == Key.Enter && it.type == KeyEventType.KeyUp) {
|
||||
onClickUpdate()
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
},
|
||||
isValid = { confirmNewKey.value == "" || newKey.value == confirmNewKey.value },
|
||||
keyboardActions = KeyboardActions(onDone = {
|
||||
if (!disabled) onClickUpdate()
|
||||
defaultKeyboardAction(ImeAction.Done)
|
||||
}),
|
||||
)
|
||||
|
||||
Box(Modifier.align(Alignment.CenterHorizontally).padding(vertical = DEFAULT_PADDING)) {
|
||||
SetPassphraseButton(disabled, onClickUpdate)
|
||||
}
|
||||
|
||||
Column {
|
||||
SectionTextFooter(generalGetString(MR.strings.you_have_to_enter_passphrase_every_time))
|
||||
SectionTextFooter(annotatedStringResource(MR.strings.impossible_to_recover_passphrase))
|
||||
}
|
||||
}
|
||||
|
||||
Spacer(Modifier.weight(1f))
|
||||
SkipButton(progressIndicator.value, nextStep)
|
||||
|
||||
SectionBottomSpacer()
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun SetPassphraseButton(disabled: Boolean, onClick: () -> Unit) {
|
||||
SimpleButtonIconEnded(
|
||||
stringResource(MR.strings.set_database_passphrase),
|
||||
painterResource(MR.images.ic_check),
|
||||
style = MaterialTheme.typography.h2,
|
||||
color = if (disabled) MaterialTheme.colors.secondary else MaterialTheme.colors.primary,
|
||||
disabled = disabled,
|
||||
click = onClick
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun SkipButton(disabled: Boolean, onClick: () -> Unit) {
|
||||
SimpleButtonIconEnded(stringResource(MR.strings.use_random_passphrase), painterResource(MR.images.ic_chevron_right), color =
|
||||
if (disabled) MaterialTheme.colors.secondary else WarningOrange, disabled = disabled, click = onClick)
|
||||
Text(
|
||||
stringResource(MR.strings.you_can_change_it_later),
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = DEFAULT_PADDING * 3),
|
||||
style = MaterialTheme.typography.subtitle1,
|
||||
color = MaterialTheme.colors.secondary,
|
||||
textAlign = TextAlign.Center,
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ProgressIndicator() {
|
||||
Box(
|
||||
Modifier.fillMaxSize(),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
CircularProgressIndicator(
|
||||
Modifier
|
||||
.padding(horizontal = 2.dp)
|
||||
.size(30.dp),
|
||||
color = MaterialTheme.colors.secondary,
|
||||
strokeWidth = 3.dp
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun startChat(key: String?) {
|
||||
val m = ChatModel
|
||||
initChatController(key)
|
||||
m.chatDbChanged.value = false
|
||||
m.chatRunning.value = true
|
||||
}
|
@ -6,7 +6,6 @@ import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material.*
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.MutableState
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.painter.Painter
|
||||
@ -25,7 +24,7 @@ import dev.icerock.moko.resources.StringResource
|
||||
fun SimpleXInfo(chatModel: ChatModel, onboarding: Boolean = true) {
|
||||
SimpleXInfoLayout(
|
||||
user = chatModel.currentUser.value,
|
||||
onboardingStage = if (onboarding) chatModel.onboardingStage else null,
|
||||
onboardingStage = if (onboarding) chatModel.controller.appPrefs.onboardingStage else null,
|
||||
showModal = { modalView -> { if (onboarding) ModalManager.fullscreen.showModal { modalView(chatModel) } else ModalManager.start.showModal { modalView(chatModel) } } },
|
||||
)
|
||||
}
|
||||
@ -33,7 +32,7 @@ fun SimpleXInfo(chatModel: ChatModel, onboarding: Boolean = true) {
|
||||
@Composable
|
||||
fun SimpleXInfoLayout(
|
||||
user: User?,
|
||||
onboardingStage: MutableState<OnboardingStage?>?,
|
||||
onboardingStage: SharedPreference<OnboardingStage>?,
|
||||
showModal: (@Composable (ChatModel) -> Unit) -> (() -> Unit),
|
||||
) {
|
||||
Column(
|
||||
@ -100,11 +99,11 @@ private fun InfoRow(icon: Painter, titleId: StringResource, textId: StringResour
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun OnboardingActionButton(user: User?, onboardingStage: MutableState<OnboardingStage?>, onclick: (() -> Unit)? = null) {
|
||||
fun OnboardingActionButton(user: User?, onboardingStage: SharedPreference<OnboardingStage>, onclick: (() -> Unit)? = null) {
|
||||
if (user == null) {
|
||||
OnboardingActionButton(MR.strings.create_your_profile, onboarding = OnboardingStage.Step2_CreateProfile, onboardingStage, true, onclick)
|
||||
OnboardingActionButton(MR.strings.create_your_profile, onboarding = OnboardingStage.Step2_CreateProfile, true, onclick)
|
||||
} else {
|
||||
OnboardingActionButton(MR.strings.make_private_connection, onboarding = OnboardingStage.OnboardingComplete, onboardingStage, true, onclick)
|
||||
OnboardingActionButton(MR.strings.make_private_connection, onboarding = OnboardingStage.OnboardingComplete, true, onclick)
|
||||
}
|
||||
}
|
||||
|
||||
@ -112,7 +111,6 @@ fun OnboardingActionButton(user: User?, onboardingStage: MutableState<Onboarding
|
||||
fun OnboardingActionButton(
|
||||
labelId: StringResource,
|
||||
onboarding: OnboardingStage?,
|
||||
onboardingStage: MutableState<OnboardingStage?>,
|
||||
border: Boolean,
|
||||
onclick: (() -> Unit)?
|
||||
) {
|
||||
@ -129,7 +127,6 @@ fun OnboardingActionButton(
|
||||
|
||||
SimpleButtonFrame(click = {
|
||||
onclick?.invoke()
|
||||
onboardingStage.value = onboarding
|
||||
if (onboarding != null) {
|
||||
ChatController.appPrefs.onboardingStage.set(onboarding)
|
||||
}
|
||||
|
@ -43,6 +43,7 @@ fun SettingsView(chatModel: ChatModel, setPerformLA: (Boolean) -> Unit, drawerSt
|
||||
profile = user.profile,
|
||||
stopped,
|
||||
chatModel.chatDbEncrypted.value == true,
|
||||
remember { chatModel.controller.appPrefs.storeDBPassphrase.state }.value,
|
||||
remember { chatModel.controller.appPrefs.notificationsMode.state },
|
||||
user.displayName,
|
||||
setPerformLA = setPerformLA,
|
||||
@ -115,6 +116,7 @@ fun SettingsLayout(
|
||||
profile: LocalProfile,
|
||||
stopped: Boolean,
|
||||
encrypted: Boolean,
|
||||
passphraseSaved: Boolean,
|
||||
notificationsMode: State<NotificationsMode>,
|
||||
userDisplayName: String,
|
||||
setPerformLA: (Boolean) -> Unit,
|
||||
@ -162,7 +164,7 @@ fun SettingsLayout(
|
||||
SettingsActionItem(painterResource(MR.images.ic_videocam), stringResource(MR.strings.settings_audio_video_calls), showSettingsModal { CallSettingsView(it, showModal) }, disabled = stopped, extraPadding = true)
|
||||
SettingsActionItem(painterResource(MR.images.ic_lock), stringResource(MR.strings.privacy_and_security), showSettingsModal { PrivacySettingsView(it, showSettingsModal, setPerformLA) }, disabled = stopped, extraPadding = true)
|
||||
SettingsActionItem(painterResource(MR.images.ic_light_mode), stringResource(MR.strings.appearance_settings), showSettingsModal { AppearanceView(it, showSettingsModal) }, extraPadding = true)
|
||||
DatabaseItem(encrypted, showSettingsModal { DatabaseView(it, showSettingsModal) }, stopped)
|
||||
DatabaseItem(encrypted, passphraseSaved, showSettingsModal { DatabaseView(it, showSettingsModal) }, stopped)
|
||||
}
|
||||
SectionDividerSpaced()
|
||||
|
||||
@ -207,7 +209,7 @@ expect fun SettingsSectionApp(
|
||||
withAuth: (title: String, desc: String, block: () -> Unit) -> Unit
|
||||
)
|
||||
|
||||
@Composable private fun DatabaseItem(encrypted: Boolean, openDatabaseView: () -> Unit, stopped: Boolean) {
|
||||
@Composable private fun DatabaseItem(encrypted: Boolean, saved: Boolean, openDatabaseView: () -> Unit, stopped: Boolean) {
|
||||
SectionItemViewWithIcon(openDatabaseView) {
|
||||
Row(
|
||||
Modifier.fillMaxWidth(),
|
||||
@ -217,7 +219,7 @@ expect fun SettingsSectionApp(
|
||||
Icon(
|
||||
painterResource(MR.images.ic_database),
|
||||
contentDescription = stringResource(MR.strings.database_passphrase_and_export),
|
||||
tint = if (encrypted) MaterialTheme.colors.secondary else WarningOrange,
|
||||
tint = if (encrypted && (appPlatform.isAndroid || !saved)) MaterialTheme.colors.secondary else WarningOrange,
|
||||
)
|
||||
TextIconSpaced(true)
|
||||
Text(stringResource(MR.strings.database_passphrase_and_export))
|
||||
@ -473,6 +475,7 @@ fun PreviewSettingsLayout() {
|
||||
profile = LocalProfile.sampleData,
|
||||
stopped = false,
|
||||
encrypted = false,
|
||||
passphraseSaved = false,
|
||||
notificationsMode = remember { mutableStateOf(NotificationsMode.OFF) },
|
||||
userDisplayName = "Alice",
|
||||
setPerformLA = { _ -> },
|
||||
|
@ -769,6 +769,11 @@
|
||||
<string name="onboarding_notifications_mode_periodic_desc"><![CDATA[<b>Good for battery</b>. Background service checks messages every 10 minutes. You may miss calls or urgent messages.]]></string>
|
||||
<string name="onboarding_notifications_mode_service_desc"><![CDATA[<b>Uses more battery</b>! Background service always runs – notifications are shown as soon as messages are available.]]></string>
|
||||
|
||||
<!-- SetupDatabasePassphrase.kt -->
|
||||
<string name="setup_database_passphrase">Setup database passphrase</string>
|
||||
<string name="you_can_change_it_later">Random passphrase is stored in settings as plaintext.\nYou can change it later.</string>
|
||||
<string name="use_random_passphrase">Use random passphrase</string>
|
||||
|
||||
<!-- MakeConnection -->
|
||||
<string name="paste_the_link_you_received">Paste received link</string>
|
||||
|
||||
@ -984,9 +989,11 @@
|
||||
|
||||
<!-- DatabaseEncryptionView.kt -->
|
||||
<string name="save_passphrase_in_keychain">Save passphrase in Keystore</string>
|
||||
<string name="save_passphrase_in_settings">Save passphrase in settings</string>
|
||||
<string name="database_encrypted">Database encrypted!</string>
|
||||
<string name="error_encrypting_database">Error encrypting database</string>
|
||||
<string name="remove_passphrase_from_keychain">Remove passphrase from Keystore?</string>
|
||||
<string name="remove_passphrase_from_settings">Remove passphrase from settings?</string>
|
||||
<string name="notifications_will_be_hidden">Notifications will be delivered only until the app stops!</string>
|
||||
<string name="remove_passphrase">Remove</string>
|
||||
<string name="encrypt_database">Encrypt</string>
|
||||
@ -995,18 +1002,23 @@
|
||||
<string name="new_passphrase">New passphrase…</string>
|
||||
<string name="confirm_new_passphrase">Confirm new passphrase…</string>
|
||||
<string name="update_database_passphrase">Update database passphrase</string>
|
||||
<string name="set_database_passphrase">Set database passphrase</string>
|
||||
<string name="enter_correct_current_passphrase">Please enter correct current passphrase.</string>
|
||||
<string name="database_is_not_encrypted">Your chat database is not encrypted - set passphrase to protect it.</string>
|
||||
<string name="keychain_is_storing_securely">Android Keystore is used to securely store passphrase - it allows notification service to work.</string>
|
||||
<string name="settings_is_storing_in_clear_text">The passphrase is stored in settings as plaintext.</string>
|
||||
<string name="encrypted_with_random_passphrase">Database is encrypted using a random passphrase, you can change it.</string>
|
||||
<string name="impossible_to_recover_passphrase"><![CDATA[<b>Please note</b>: you will NOT be able to recover or change passphrase if you lose it.]]></string>
|
||||
<string name="keychain_allows_to_receive_ntfs">Android Keystore will be used to securely store passphrase after you restart the app or change passphrase - it will allow receiving notifications.</string>
|
||||
<string name="passphrase_will_be_saved_in_settings">The passphrase will be stored in settings as plaintext after you change it or restart the app.</string>
|
||||
<string name="you_have_to_enter_passphrase_every_time">You have to enter passphrase every time the app starts - it is not stored on the device.</string>
|
||||
<string name="encrypt_database_question">Encrypt database?</string>
|
||||
<string name="change_database_passphrase_question">Change database passphrase?</string>
|
||||
<string name="database_will_be_encrypted">Database will be encrypted.</string>
|
||||
<string name="database_will_be_encrypted_and_passphrase_stored">Database will be encrypted and the passphrase stored in the Keystore.</string>
|
||||
<string name="database_will_be_encrypted_and_passphrase_stored_in_settings">Database will be encrypted and the passphrase stored in settings.</string>
|
||||
<string name="database_encryption_will_be_updated">Database encryption passphrase will be updated and stored in the Keystore.</string>
|
||||
<string name="database_encryption_will_be_updated_in_settings">Database encryption passphrase will be updated and stored in settings.</string>
|
||||
<string name="database_passphrase_will_be_updated">Database encryption passphrase will be updated.</string>
|
||||
<string name="store_passphrase_securely">Please store passphrase securely, you will NOT be able to change it if you lose it.</string>
|
||||
<string name="store_passphrase_securely_without_recover">Please store passphrase securely, you will NOT be able to access chat if you lose it.</string>
|
||||
|
@ -0,0 +1,106 @@
|
||||
package chat.simplex.common.views.database
|
||||
|
||||
import SectionItemView
|
||||
import SectionTextFooter
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.material.*
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.MutableState
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import androidx.compose.ui.unit.dp
|
||||
import chat.simplex.common.ui.theme.WarningOrange
|
||||
import chat.simplex.common.views.helpers.*
|
||||
import chat.simplex.res.MR
|
||||
import dev.icerock.moko.resources.compose.painterResource
|
||||
import dev.icerock.moko.resources.compose.stringResource
|
||||
|
||||
@Composable
|
||||
actual fun SavePassphraseSetting(
|
||||
useKeychain: Boolean,
|
||||
initialRandomDBPassphrase: Boolean,
|
||||
storedKey: Boolean,
|
||||
progressIndicator: Boolean,
|
||||
minHeight: Dp,
|
||||
onCheckedChange: (Boolean) -> Unit,
|
||||
) {
|
||||
SectionItemView(minHeight = minHeight) {
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
Icon(
|
||||
if (storedKey) painterResource(MR.images.ic_vpn_key_filled) else painterResource(MR.images.ic_vpn_key_off_filled),
|
||||
stringResource(MR.strings.save_passphrase_in_settings),
|
||||
tint = if (storedKey) WarningOrange else MaterialTheme.colors.secondary
|
||||
)
|
||||
Spacer(Modifier.padding(horizontal = 4.dp))
|
||||
Text(
|
||||
stringResource(MR.strings.save_passphrase_in_settings),
|
||||
Modifier.padding(end = 24.dp),
|
||||
color = Color.Unspecified
|
||||
)
|
||||
Spacer(Modifier.fillMaxWidth().weight(1f))
|
||||
DefaultSwitch(
|
||||
checked = useKeychain,
|
||||
onCheckedChange = onCheckedChange,
|
||||
enabled = !initialRandomDBPassphrase && !progressIndicator
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
actual fun DatabaseEncryptionFooter(
|
||||
useKeychain: MutableState<Boolean>,
|
||||
chatDbEncrypted: Boolean?,
|
||||
storedKey: MutableState<Boolean>,
|
||||
initialRandomDBPassphrase: MutableState<Boolean>,
|
||||
) {
|
||||
if (chatDbEncrypted == false) {
|
||||
SectionTextFooter(generalGetString(MR.strings.database_is_not_encrypted))
|
||||
} else if (useKeychain.value) {
|
||||
if (storedKey.value) {
|
||||
SectionTextFooter(generalGetString(MR.strings.settings_is_storing_in_clear_text))
|
||||
if (initialRandomDBPassphrase.value) {
|
||||
SectionTextFooter(generalGetString(MR.strings.encrypted_with_random_passphrase))
|
||||
} else {
|
||||
SectionTextFooter(annotatedStringResource(MR.strings.impossible_to_recover_passphrase))
|
||||
}
|
||||
} else {
|
||||
SectionTextFooter(generalGetString(MR.strings.passphrase_will_be_saved_in_settings))
|
||||
}
|
||||
} else {
|
||||
SectionTextFooter(generalGetString(MR.strings.you_have_to_enter_passphrase_every_time))
|
||||
SectionTextFooter(annotatedStringResource(MR.strings.impossible_to_recover_passphrase))
|
||||
}
|
||||
}
|
||||
|
||||
actual fun encryptDatabaseSavedAlert(onConfirm: () -> Unit) {
|
||||
AlertManager.shared.showAlertDialog(
|
||||
title = generalGetString(MR.strings.encrypt_database_question),
|
||||
text = generalGetString(MR.strings.database_will_be_encrypted_and_passphrase_stored_in_settings) + "\n" + storeSecurelySaved(),
|
||||
confirmText = generalGetString(MR.strings.encrypt_database),
|
||||
onConfirm = onConfirm,
|
||||
destructive = true,
|
||||
)
|
||||
}
|
||||
|
||||
actual fun changeDatabaseKeySavedAlert(onConfirm: () -> Unit) {
|
||||
AlertManager.shared.showAlertDialog(
|
||||
title = generalGetString(MR.strings.change_database_passphrase_question),
|
||||
text = generalGetString(MR.strings.database_encryption_will_be_updated_in_settings) + "\n" + storeSecurelySaved(),
|
||||
confirmText = generalGetString(MR.strings.update_database),
|
||||
onConfirm = onConfirm,
|
||||
destructive = false,
|
||||
)
|
||||
}
|
||||
|
||||
actual fun removePassphraseAlert(onConfirm: () -> Unit) {
|
||||
AlertManager.shared.showAlertDialog(
|
||||
title = generalGetString(MR.strings.remove_passphrase_from_settings),
|
||||
text = storeSecurelyDanger(),
|
||||
confirmText = generalGetString(MR.strings.remove_passphrase),
|
||||
onConfirm = onConfirm,
|
||||
destructive = true,
|
||||
)
|
||||
}
|
Loading…
Reference in New Issue
Block a user