diff --git a/apps/android/.idea/codeStyles/codeStyleConfig.xml b/apps/android/.idea/codeStyles/codeStyleConfig.xml
index 79ee123c2b..6e6eec1148 100644
--- a/apps/android/.idea/codeStyles/codeStyleConfig.xml
+++ b/apps/android/.idea/codeStyles/codeStyleConfig.xml
@@ -1,5 +1,6 @@
+
\ No newline at end of file
diff --git a/apps/android/app/src/main/java/chat/simplex/app/MainActivity.kt b/apps/android/app/src/main/java/chat/simplex/app/MainActivity.kt
index 4646474f0a..f8a21c2cd3 100644
--- a/apps/android/app/src/main/java/chat/simplex/app/MainActivity.kt
+++ b/apps/android/app/src/main/java/chat/simplex/app/MainActivity.kt
@@ -4,6 +4,7 @@ import android.app.Application
import android.content.*
import android.net.Uri
import android.os.Bundle
+import android.os.SystemClock.elapsedRealtime
import android.util.Log
import androidx.activity.compose.setContent
import androidx.activity.viewModels
@@ -37,12 +38,12 @@ class MainActivity: FragmentActivity(), LifecycleEventObserver {
private val vm by viewModels()
private val chatController by lazy { (application as SimplexApp).chatController }
private val userAuthorized = mutableStateOf(null)
- private val lastLA = mutableStateOf(null)
+ private val enteredBackground = mutableStateOf(null)
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
ProcessLifecycleOwner.get().lifecycle.addObserver(this)
-// testJson()
+ // testJson()
val m = vm.chatModel
processNotificationIntent(intent, m)
setContent {
@@ -67,38 +68,37 @@ class MainActivity: FragmentActivity(), LifecycleEventObserver {
override fun onStateChanged(source: LifecycleOwner, event: Lifecycle.Event) {
withApi {
when (event) {
+ Lifecycle.Event.ON_STOP -> {
+ enteredBackground.value = elapsedRealtime()
+ }
Lifecycle.Event.ON_START -> {
// perform local authentication if needed
val m = vm.chatModel
- val lastLAVal = lastLA.value
- if (
- m.controller.appPrefs.performLA.get()
- && (lastLAVal == null || (System.nanoTime() - lastLAVal >= 30 * 1e+9))
- ) {
- userAuthorized.value = false
- authenticate(
- generalGetString(R.string.auth_access_chats),
- generalGetString(R.string.auth_log_in_using_credential),
- this@MainActivity,
- completed = { laResult ->
- when (laResult) {
- LAResult.Success -> {
- userAuthorized.value = true
- lastLA.value = System.nanoTime()
- }
- is LAResult.Error -> laErrorToast(applicationContext, laResult.errString)
- LAResult.Failed -> laFailedToast(applicationContext)
- LAResult.Unavailable -> {
- userAuthorized.value = true
- m.performLA.value = false
- m.controller.appPrefs.performLA.set(false)
- laUnavailableTurningOffAlert()
+ val enteredBackgroundVal = enteredBackground.value
+ if (!m.controller.appPrefs.performLA.get()) {
+ userAuthorized.value = true
+ } else {
+ if (enteredBackgroundVal == null || elapsedRealtime() - enteredBackgroundVal >= 30 * 1e+3) {
+ userAuthorized.value = false
+ authenticate(
+ generalGetString(R.string.auth_unlock),
+ generalGetString(R.string.auth_log_in_using_credential),
+ this@MainActivity,
+ completed = { laResult ->
+ when (laResult) {
+ LAResult.Success -> userAuthorized.value = true
+ is LAResult.Error -> laErrorToast(applicationContext, laResult.errString)
+ LAResult.Failed -> laFailedToast(applicationContext)
+ LAResult.Unavailable -> {
+ userAuthorized.value = true
+ m.performLA.value = false
+ m.controller.appPrefs.performLA.set(false)
+ laUnavailableTurningOffAlert()
+ }
}
}
- }
- )
- } else {
- userAuthorized.value = true
+ )
+ }
}
}
}
@@ -137,8 +137,6 @@ class MainActivity: FragmentActivity(), LifecycleEventObserver {
LAResult.Success -> {
m.performLA.value = true
prefPerformLA.set(true)
- userAuthorized.value = true
- lastLA.value = System.nanoTime()
laTurnedOnAlert()
}
is LAResult.Error -> {
@@ -206,7 +204,7 @@ fun MainPage(
showLANotice: () -> Unit
) {
// this with LaunchedEffect(userAuthorized.value) fixes bottom sheet visibly collapsing after authentication
- var chatsAccessAuthorized by remember { mutableStateOf(false) }
+ var chatsAccessAuthorized by remember { mutableStateOf(false) }
LaunchedEffect(userAuthorized.value) {
delay(500L)
chatsAccessAuthorized = userAuthorized.value == true
diff --git a/apps/android/app/src/main/res/values-ru/strings.xml b/apps/android/app/src/main/res/values-ru/strings.xml
index f8b1242652..6382394249 100644
--- a/apps/android/app/src/main/res/values-ru/strings.xml
+++ b/apps/android/app/src/main/res/values-ru/strings.xml
@@ -72,7 +72,7 @@
Блокировка SimpleX включена
Вы будете аутентифицированы при запуске и возобновлении приложения, которое было 30 секунд в фоновом режиме.
- Разблокировать SimpleX
+ Разблокировать
Пройдите аутентификацию
Включить блокировку SimpleX
Отключить блокировку SimpleX
diff --git a/apps/android/app/src/main/res/values/strings.xml b/apps/android/app/src/main/res/values/strings.xml
index cb1d5cc80e..cad76c4ab8 100644
--- a/apps/android/app/src/main/res/values/strings.xml
+++ b/apps/android/app/src/main/res/values/strings.xml
@@ -72,7 +72,7 @@
SimpleX Lock turned on
You will be required to authenticate when you start or resume the app after 30 seconds in background.
- Access chats
+ Unlock
Log in using your credential
Enable SimpleX Lock
Disable SimpleX Lock
diff --git a/apps/ios/Shared/ContentView.swift b/apps/ios/Shared/ContentView.swift
index 158392ec23..408edf0911 100644
--- a/apps/ios/Shared/ContentView.swift
+++ b/apps/ios/Shared/ContentView.swift
@@ -11,29 +11,29 @@ struct ContentView: View {
@EnvironmentObject var chatModel: ChatModel
@ObservedObject var alertManager = AlertManager.shared
@ObservedObject var callController = CallController.shared
- @State private var showNotificationAlert = false
@Binding var userAuthorized: Bool?
var body: some View {
ZStack {
- if let step = chatModel.onboardingStage {
- if userAuthorized == true,
- case .onboardingComplete = step,
- let user = chatModel.currentUser {
- ZStack(alignment: .top) {
- ChatListView(user: user)
- .onAppear {
- NtfManager.shared.requestAuthorization(onDeny: {
- alertManager.showAlert(notificationAlert())
- })
+ if userAuthorized == true {
+ if let step = chatModel.onboardingStage {
+ if case .onboardingComplete = step,
+ let user = chatModel.currentUser {
+ ZStack(alignment: .top) {
+ ChatListView(user: user)
+ .onAppear {
+ NtfManager.shared.requestAuthorization(onDeny: {
+ alertManager.showAlert(notificationAlert())
+ })
+ }
+ if chatModel.showCallView, let call = chatModel.activeCall {
+ ActiveCallView(call: call)
+ }
+ IncomingCallView()
}
- if chatModel.showCallView, let call = chatModel.activeCall {
- ActiveCallView(call: call)
- }
- IncomingCallView()
+ } else {
+ OnboardingView(onboarding: step)
}
- } else {
- OnboardingView(onboarding: step)
}
}
}
@@ -99,11 +99,15 @@ final class AlertManager: ObservableObject {
}
func showAlertMsg(title: LocalizedStringKey, message: LocalizedStringKey? = nil) {
- if let message = message {
- showAlert(Alert(title: Text(title), message: Text(message)))
- } else {
- showAlert(Alert(title: Text(title)))
- }
+ showAlert(mkAlert(title: title, message: message))
+ }
+}
+
+func mkAlert(title: LocalizedStringKey, message: LocalizedStringKey? = nil) -> Alert {
+ if let message = message {
+ return Alert(title: Text(title), message: Text(message))
+ } else {
+ return Alert(title: Text(title))
}
}
diff --git a/apps/ios/Shared/Model/ChatModel.swift b/apps/ios/Shared/Model/ChatModel.swift
index 4d4ae6ad91..0337b2be80 100644
--- a/apps/ios/Shared/Model/ChatModel.swift
+++ b/apps/ios/Shared/Model/ChatModel.swift
@@ -14,6 +14,7 @@ import WebKit
final class ChatModel: ObservableObject {
@Published var onboardingStage: OnboardingStage?
@Published var currentUser: User?
+ @Published var performLA: Bool = false
// list of chat "previews"
@Published var chats: [Chat] = []
// current chat
diff --git a/apps/ios/Shared/SimpleXApp.swift b/apps/ios/Shared/SimpleXApp.swift
index 0575fd587a..291befb628 100644
--- a/apps/ios/Shared/SimpleXApp.swift
+++ b/apps/ios/Shared/SimpleXApp.swift
@@ -14,10 +14,12 @@ let logger = Logger()
struct SimpleXApp: App {
@UIApplicationDelegateAdaptor(AppDelegate.self) var appDelegate
@StateObject private var chatModel = ChatModel.shared
+ @ObservedObject var alertManager = AlertManager.shared
@Environment(\.scenePhase) var scenePhase
+ @AppStorage(DEFAULT_PERFORM_LA) private var performLA = false
@State private var userAuthorized: Bool? = nil
- @State private var doAuthenticate: Bool? = nil
- @State private var lastLA: Double? = nil
+ @State private var doAuthenticate: Bool = true
+ @State private var enteredBackground: Double? = nil
init() {
hs_init(0, nil)
@@ -35,8 +37,8 @@ struct SimpleXApp: App {
chatModel.appOpenUrl = url
}
.onAppear() {
+ chatModel.performLA = performLA
initializeChat()
- doAuthenticate = true
}
.onChange(of: scenePhase) { phase in
logger.debug("scenePhase \(String(describing: scenePhase))")
@@ -45,10 +47,11 @@ struct SimpleXApp: App {
case .background:
BGManager.shared.schedule()
doAuthenticate = true
+ enteredBackground = ProcessInfo.processInfo.systemUptime
case .inactive:
- authenticateUser()
+ authenticateOnPhaseChange()
case .active:
- authenticateUser()
+ authenticateOnPhaseChange()
default:
break
}
@@ -56,34 +59,37 @@ struct SimpleXApp: App {
}
}
- private func authenticateUser() {
- if doAuthenticate == true,
- authenticationExpired() {
+ private func authenticateOnPhaseChange() {
+ if doAuthenticate {
doAuthenticate = false
- userAuthorized = false
- authenticate() { laResult in
- switch (laResult) {
- case .success:
- userAuthorized = true
- lastLA = ProcessInfo.processInfo.systemUptime
- case .failed:
- laFailedAlert()
- case .unavailable:
- userAuthorized = true
- laUnavailableAlert()
+ if !performLA {
+ userAuthorized = true
+ } else {
+ if authenticationExpired() {
+ userAuthorized = false
+ authenticate(reason: "Unlock") { laResult in
+ switch (laResult) {
+ case .success:
+ userAuthorized = true
+ case .failed:
+ AlertManager.shared.showAlert(laFailedAlert())
+ case .unavailable:
+ userAuthorized = true
+ performLA = false
+ chatModel.performLA = false
+ AlertManager.shared.showAlert(laUnavailableTurningOffAlert())
+ }
+ }
}
}
}
}
private func authenticationExpired() -> Bool {
- if (lastLA == nil) {
- return true
- }
- else if let lastLA = lastLA, ProcessInfo.processInfo.systemUptime - lastLA >= 30 {
- return true
+ if let enteredBackground = enteredBackground {
+ return ProcessInfo.processInfo.systemUptime - enteredBackground >= 30
} else {
- return false
+ return true
}
}
}
diff --git a/apps/ios/Shared/Views/Helpers/LocalAuthenticationUtils.swift b/apps/ios/Shared/Views/Helpers/LocalAuthenticationUtils.swift
index 2cc1885640..4776f660c3 100644
--- a/apps/ios/Shared/Views/Helpers/LocalAuthenticationUtils.swift
+++ b/apps/ios/Shared/Views/Helpers/LocalAuthenticationUtils.swift
@@ -15,11 +15,10 @@ enum LAResult {
case unavailable(authError: String?)
}
-func authenticate(completed: @escaping (LAResult) -> Void) {
+func authenticate(reason: String, completed: @escaping (LAResult) -> Void) {
let laContext = LAContext()
var authAvailabilityError: NSError?
if laContext.canEvaluatePolicy(.deviceOwnerAuthentication, error: &authAvailabilityError) {
- let reason = "Access chats"
laContext.evaluatePolicy(.deviceOwnerAuthentication, localizedReason: reason) { success, authError in
DispatchQueue.main.async {
if success {
@@ -36,16 +35,30 @@ func authenticate(completed: @escaping (LAResult) -> Void) {
}
}
-func laFailedAlert() {
- AlertManager.shared.showAlertMsg(
+func laTurnedOnAlert() -> Alert {
+ mkAlert(
+ title: "SimpleX Lock turned on",
+ message: "You will be required to authenticate when you start or resume the app after 30 seconds in background."
+ )
+}
+
+func laFailedAlert() -> Alert {
+ mkAlert(
title: "Authentication failed",
message: "You could not be verified; please try again."
)
}
-func laUnavailableAlert() {
- AlertManager.shared.showAlertMsg(
+func laUnavailableInstructionAlert() -> Alert {
+ mkAlert(
title: "Authentication unavailable",
- message: "Your device is not configured for authentication."
+ message: "Device authentication is not enabled. You can turn on SimpleX Lock via Settings, once you enable device authentication."
+ )
+}
+
+func laUnavailableTurningOffAlert() -> Alert {
+ mkAlert(
+ title: "Authentication unavailable",
+ message: "Device authentication is disabled. Turning off SimpleX Lock."
)
}
diff --git a/apps/ios/Shared/Views/UserSettings/SettingsView.swift b/apps/ios/Shared/Views/UserSettings/SettingsView.swift
index 214e9182f0..38d506b352 100644
--- a/apps/ios/Shared/Views/UserSettings/SettingsView.swift
+++ b/apps/ios/Shared/Views/UserSettings/SettingsView.swift
@@ -14,11 +14,13 @@ let appVersion = Bundle.main.object(forInfoDictionaryKey: "CFBundleShortVersionS
let appBuild = Bundle.main.object(forInfoDictionaryKey: "CFBundleVersion") as? String
+let DEFAULT_PERFORM_LA = "performLocalAuthentication"
let DEFAULT_USE_NOTIFICATIONS = "useNotifications"
let DEFAULT_PENDING_CONNECTIONS = "pendingConnections"
let DEFAULT_WEBRTC_POLICY_RELAY = "webrtcPolicyRelay"
let appDefaults: [String:Any] = [
+ DEFAULT_PERFORM_LA: false,
DEFAULT_USE_NOTIFICATIONS: false,
DEFAULT_PENDING_CONNECTIONS: true,
DEFAULT_WEBRTC_POLICY_RELAY: true
@@ -30,10 +32,22 @@ struct SettingsView: View {
@Environment(\.colorScheme) var colorScheme
@EnvironmentObject var chatModel: ChatModel
@Binding var showSettings: Bool
+ @AppStorage(DEFAULT_PERFORM_LA) private var prefPerformLA = false
+ @State private var performLAToggleReset = false
@AppStorage(DEFAULT_USE_NOTIFICATIONS) private var useNotifications = false
@AppStorage(DEFAULT_PENDING_CONNECTIONS) private var pendingConnections = true
@State var showNotificationsAlert: Bool = false
@State var whichNotificationsAlert = NotificationAlert.enable
+ @State var alert: SettingsViewAlert? = nil
+
+ enum SettingsViewAlert: Identifiable {
+ case laTurnedOnAlert
+ case laFailedAlert
+ case laUnavailableInstructionAlert
+ case laUnavailableTurningOffAlert
+
+ var id: SettingsViewAlert { get { self } }
+ }
var body: some View {
let user: User = chatModel.currentUser!
@@ -57,6 +71,9 @@ struct SettingsView: View {
}
Section("Settings") {
+ settingsRow("lock") {
+ Toggle("SimpleX Lock", isOn: $chatModel.performLA)
+ }
settingsRow("link") {
Toggle("Show pending connections", isOn: $pendingConnections)
}
@@ -133,6 +150,68 @@ struct SettingsView: View {
}
}
.navigationTitle("Your settings")
+ .onChange(of: chatModel.performLA) { performLAToggle in
+ if performLAToggleReset {
+ performLAToggleReset = false
+ } else {
+ if performLAToggle {
+ enableLA()
+ } else {
+ disableLA()
+ }
+ }
+ }
+ .alert(item: $alert) { alertItem in
+ switch alertItem {
+ case .laTurnedOnAlert: return laTurnedOnAlert()
+ case .laFailedAlert: return laFailedAlert()
+ case .laUnavailableInstructionAlert: return laUnavailableInstructionAlert()
+ case .laUnavailableTurningOffAlert: return laUnavailableTurningOffAlert()
+ }
+ }
+ }
+ }
+
+ private func enableLA() {
+ authenticate(reason: "Enable SimpleX Lock") { laResult in
+ switch laResult {
+ case .success:
+ prefPerformLA = true
+ alert = .laTurnedOnAlert
+ case .failed:
+ prefPerformLA = false
+ withAnimation() {
+ chatModel.performLA = false
+ }
+ performLAToggleReset = true
+ alert = .laFailedAlert
+ case .unavailable:
+ prefPerformLA = false
+ withAnimation() {
+ chatModel.performLA = false
+ }
+ performLAToggleReset = true
+ alert = .laUnavailableInstructionAlert
+ }
+ }
+ }
+
+ private func disableLA() {
+ authenticate(reason: "Disable SimpleX Lock") { laResult in
+ switch (laResult) {
+ case .success:
+ prefPerformLA = false
+ case .failed:
+ prefPerformLA = true
+ withAnimation() {
+ chatModel.performLA = true
+ }
+ performLAToggleReset = true
+ alert = .laFailedAlert
+ case .unavailable:
+ prefPerformLA = false
+ alert = .laUnavailableTurningOffAlert
+ }
}
}