From c3c712aa02d29fb73ac5ecc10fc97616599f9f6e Mon Sep 17 00:00:00 2001 From: JRoberts <8711996+jr-simplex@users.noreply.github.com> Date: Sat, 28 May 2022 22:09:46 +0400 Subject: [PATCH] ios: show local authentication notice; ios & android: retry authentication button (#706) * advertisement * refactor * advertisement state machine * simplify * ios: retry * remove log * android: retry * Update apps/ios/Shared/ContentView.swift Co-authored-by: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> * Update apps/ios/Shared/Views/UserSettings/SettingsView.swift Co-authored-by: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> Co-authored-by: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> --- .../java/chat/simplex/app/MainActivity.kt | 235 +++++++++++------- .../app/src/main/res/values-ru/strings.xml | 1 + .../app/src/main/res/values/strings.xml | 1 + apps/ios/Shared/ContentView.swift | 80 +++++- apps/ios/Shared/Model/ChatModel.swift | 1 - apps/ios/Shared/SimpleXApp.swift | 47 +--- .../Views/UserSettings/SettingsButton.swift | 3 +- .../Views/UserSettings/SettingsView.swift | 19 +- 8 files changed, 248 insertions(+), 139 deletions(-) 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 f8a21c2cd3..a67173534e 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 @@ -12,14 +12,19 @@ import androidx.compose.foundation.background import androidx.compose.foundation.layout.* import androidx.compose.material.MaterialTheme import androidx.compose.material.Surface +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.outlined.Replay import androidx.compose.runtime.* +import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp import androidx.fragment.app.FragmentActivity import androidx.lifecycle.* import androidx.work.* import chat.simplex.app.model.ChatModel import chat.simplex.app.model.NtfManager +import chat.simplex.app.ui.theme.SimpleButton import chat.simplex.app.ui.theme.SimpleXTheme import chat.simplex.app.views.SplashView import chat.simplex.app.views.call.ActiveCallView @@ -39,6 +44,7 @@ class MainActivity: FragmentActivity(), LifecycleEventObserver { private val chatController by lazy { (application as SimplexApp).chatController } private val userAuthorized = mutableStateOf(null) private val enteredBackground = mutableStateOf(null) + private val laFailed = mutableStateOf(false) override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) @@ -53,7 +59,14 @@ class MainActivity: FragmentActivity(), LifecycleEventObserver { .background(MaterialTheme.colors.background) .fillMaxSize() ) { - MainPage(m, userAuthorized, ::setPerformLA, showLANotice = { m.controller.showLANotice(this) }) + MainPage( + m, + userAuthorized, + laFailed, + ::runAuthenticate, + ::setPerformLA, + showLANotice = { m.controller.showLANotice(this) } + ) } } } @@ -72,39 +85,50 @@ class MainActivity: FragmentActivity(), LifecycleEventObserver { enteredBackground.value = elapsedRealtime() } Lifecycle.Event.ON_START -> { - // perform local authentication if needed - val m = vm.chatModel 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() - } - } - } - ) - } + if (enteredBackgroundVal == null || elapsedRealtime() - enteredBackgroundVal >= 30 * 1e+3) { + runAuthenticate() } } } } } + private fun runAuthenticate() { + val m = vm.chatModel + if (!m.controller.appPrefs.performLA.get()) { + userAuthorized.value = true + } else { + 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 -> { + laFailed.value = true + laErrorToast(applicationContext, laResult.errString) + } + LAResult.Failed -> { + laFailed.value = true + laFailedToast(applicationContext) + } + LAResult.Unavailable -> { + userAuthorized.value = true + m.performLA.value = false + m.controller.appPrefs.performLA.set(false) + laUnavailableTurningOffAlert() + } + } + } + ) + } + } + private fun schedulePeriodicServiceRestartWorker() { val workerVersion = chatController.appPrefs.autoRestartWorkerVersion.get() val workPolicy = if (workerVersion == SimplexService.SERVICE_START_WORKER_VERSION) { @@ -124,70 +148,79 @@ class MainActivity: FragmentActivity(), LifecycleEventObserver { } private fun setPerformLA(on: Boolean) { - val m = vm.chatModel + vm.chatModel.controller.appPrefs.laNoticeShown.set(true) if (on) { - m.controller.appPrefs.laNoticeShown.set(true) - authenticate( - generalGetString(R.string.auth_enable), - generalGetString(R.string.auth_confirm_credential), - this@MainActivity, - completed = { laResult -> - val prefPerformLA = m.controller.appPrefs.performLA - when (laResult) { - LAResult.Success -> { - m.performLA.value = true - prefPerformLA.set(true) - laTurnedOnAlert() - } - is LAResult.Error -> { - m.performLA.value = false - prefPerformLA.set(false) - laErrorToast(applicationContext, laResult.errString) - } - LAResult.Failed -> { - m.performLA.value = false - prefPerformLA.set(false) - laFailedToast(applicationContext) - } - LAResult.Unavailable -> { - m.performLA.value = false - prefPerformLA.set(false) - laUnavailableInstructionAlert() - } - } - } - ) + enableLA() } else { - authenticate( - generalGetString(R.string.auth_disable), - generalGetString(R.string.auth_confirm_credential), - this@MainActivity, - completed = { laResult -> - val prefPerformLA = m.controller.appPrefs.performLA - when (laResult) { - LAResult.Success -> { - m.performLA.value = false - prefPerformLA.set(false) - } - is LAResult.Error -> { - m.performLA.value = true - prefPerformLA.set(true) - laErrorToast(applicationContext, laResult.errString) - } - LAResult.Failed -> { - m.performLA.value = true - prefPerformLA.set(true) - laFailedToast(applicationContext) - } - LAResult.Unavailable -> { - m.performLA.value = false - prefPerformLA.set(false) - laUnavailableTurningOffAlert() - } + disableLA() + } + } + + private fun enableLA() { + val m = vm.chatModel + authenticate( + generalGetString(R.string.auth_enable), + generalGetString(R.string.auth_confirm_credential), + this@MainActivity, + completed = { laResult -> + val prefPerformLA = m.controller.appPrefs.performLA + when (laResult) { + LAResult.Success -> { + m.performLA.value = true + prefPerformLA.set(true) + laTurnedOnAlert() + } + is LAResult.Error -> { + m.performLA.value = false + prefPerformLA.set(false) + laErrorToast(applicationContext, laResult.errString) + } + LAResult.Failed -> { + m.performLA.value = false + prefPerformLA.set(false) + laFailedToast(applicationContext) + } + LAResult.Unavailable -> { + m.performLA.value = false + prefPerformLA.set(false) + laUnavailableInstructionAlert() } } - ) - } + } + ) + } + + private fun disableLA() { + val m = vm.chatModel + authenticate( + generalGetString(R.string.auth_disable), + generalGetString(R.string.auth_confirm_credential), + this@MainActivity, + completed = { laResult -> + val prefPerformLA = m.controller.appPrefs.performLA + when (laResult) { + LAResult.Success -> { + m.performLA.value = false + prefPerformLA.set(false) + } + is LAResult.Error -> { + m.performLA.value = true + prefPerformLA.set(true) + laErrorToast(applicationContext, laResult.errString) + } + LAResult.Failed -> { + m.performLA.value = true + prefPerformLA.set(true) + laFailedToast(applicationContext) + } + LAResult.Unavailable -> { + m.performLA.value = false + prefPerformLA.set(false) + laUnavailableTurningOffAlert() + } + } + } + ) } } @@ -200,6 +233,8 @@ class SimplexViewModel(application: Application): AndroidViewModel(application) fun MainPage( chatModel: ChatModel, userAuthorized: MutableState, + laFailed: MutableState, + runAuthenticate: () -> Unit, setPerformLA: (Boolean) -> Unit, showLANotice: () -> Unit ) { @@ -232,12 +267,36 @@ fun MainPage( chatModel.clearOverlays.value = false } } + + @Composable + fun retryAuthView() { + Box( + Modifier.fillMaxSize(), + contentAlignment = Alignment.Center + ) { + SimpleButton( + stringResource(R.string.auth_retry), + icon = Icons.Outlined.Replay, + click = { + laFailed.value = false + runAuthenticate() + } + ) + } + } + Box { val onboarding = chatModel.onboardingStage.value val userCreated = chatModel.userCreated.value when { onboarding == null || userCreated == null -> SplashView() - !chatsAccessAuthorized -> SplashView() + !chatsAccessAuthorized -> { + if (chatModel.controller.appPrefs.performLA.get() && laFailed.value) { + retryAuthView() + } else { + SplashView() + } + } onboarding == OnboardingStage.OnboardingComplete && userCreated -> { Box { if (chatModel.showCallView.value) ActiveCallView(chatModel) 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 6382394249..c3d3ba12a3 100644 --- a/apps/android/app/src/main/res/values-ru/strings.xml +++ b/apps/android/app/src/main/res/values-ru/strings.xml @@ -83,6 +83,7 @@ Аутентификация недоступна На устройстве не включена аутентификация. Вы можете включить блокировку SimpleX в Настройках после включения аутентификации. На устройстве выключена аутентификация. Отключение блокировки SimpleX Chat. + Повторить Ответить diff --git a/apps/android/app/src/main/res/values/strings.xml b/apps/android/app/src/main/res/values/strings.xml index cad76c4ab8..e058b748b2 100644 --- a/apps/android/app/src/main/res/values/strings.xml +++ b/apps/android/app/src/main/res/values/strings.xml @@ -83,6 +83,7 @@ Authentication unavailable Device authentication is not enabled. You can turn on SimpleX Lock via Settings, once you enable device authentication. Device authentication is disabled. Turning off SimpleX Lock. + Retry Reply diff --git a/apps/ios/Shared/ContentView.swift b/apps/ios/Shared/ContentView.swift index 408edf0911..871dd1f41e 100644 --- a/apps/ios/Shared/ContentView.swift +++ b/apps/ios/Shared/ContentView.swift @@ -11,7 +11,13 @@ struct ContentView: View { @EnvironmentObject var chatModel: ChatModel @ObservedObject var alertManager = AlertManager.shared @ObservedObject var callController = CallController.shared - @Binding var userAuthorized: Bool? + @Binding var doAuthenticate: Bool + @Binding var enteredBackground: Double? + @State private var userAuthorized: Bool? + @State private var laFailed: Bool = false + @AppStorage(DEFAULT_SHOW_LA_NOTICE) private var prefShowLANotice = false + @AppStorage(DEFAULT_LA_NOTICE_SHOWN) private var prefLANoticeShown = false + @AppStorage(DEFAULT_PERFORM_LA) private var prefPerformLA = false var body: some View { ZStack { @@ -25,6 +31,12 @@ struct ContentView: View { NtfManager.shared.requestAuthorization(onDeny: { alertManager.showAlert(notificationAlert()) }) + // Local Authentication notice is to be shown on next start after onboarding is complete + if (!prefLANoticeShown && prefShowLANotice) { + prefLANoticeShown = true + alertManager.showAlert(laNoticeAlert()) + } + prefShowLANotice = true } if chatModel.showCallView, let call = chatModel.activeCall { ActiveCallView(call: call) @@ -35,11 +47,77 @@ struct ContentView: View { OnboardingView(onboarding: step) } } + } else if prefPerformLA && laFailed { + retryAuthView() + } + } + .onChange(of: doAuthenticate) { doAuth in + if doAuth, authenticationExpired() { + runAuthenticate() } } .alert(isPresented: $alertManager.presentAlert) { alertManager.alertView! } } + private func retryAuthView() -> some View { + Button { + laFailed = false + runAuthenticate() + } label: { Label("Retry", systemImage: "arrow.counterclockwise") } + } + + private func runAuthenticate() { + if !prefPerformLA { + userAuthorized = true + } else { + userAuthorized = false + authenticate(reason: "Unlock") { laResult in + switch (laResult) { + case .success: + userAuthorized = true + case .failed: + laFailed = true + AlertManager.shared.showAlert(laFailedAlert()) + case .unavailable: + userAuthorized = true + prefPerformLA = false + AlertManager.shared.showAlert(laUnavailableTurningOffAlert()) + } + } + } + } + + private func authenticationExpired() -> Bool { + if let enteredBackground = enteredBackground { + return ProcessInfo.processInfo.systemUptime - enteredBackground >= 30 + } else { + return true + } + } + + func laNoticeAlert() -> Alert { + Alert( + title: Text("SimpleX Lock"), + message: Text("To protect your information, turn on SimpleX Lock.\nYou will be prompted to complete authentication before this feature is enabled."), + primaryButton: .default(Text("Turn on")) { + authenticate(reason: "Enable SimpleX Lock") { laResult in + switch laResult { + case .success: + prefPerformLA = true + alertManager.showAlert(laTurnedOnAlert()) + case .failed: + prefPerformLA = false + alertManager.showAlert(laFailedAlert()) + case .unavailable: + prefPerformLA = false + alertManager.showAlert(laUnavailableInstructionAlert()) + } + } + }, + secondaryButton: .cancel() + ) + } + func notificationAlert() -> Alert { Alert( title: Text("Notifications are disabled!"), diff --git a/apps/ios/Shared/Model/ChatModel.swift b/apps/ios/Shared/Model/ChatModel.swift index 0337b2be80..4d4ae6ad91 100644 --- a/apps/ios/Shared/Model/ChatModel.swift +++ b/apps/ios/Shared/Model/ChatModel.swift @@ -14,7 +14,6 @@ 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 291befb628..e1a54748b3 100644 --- a/apps/ios/Shared/SimpleXApp.swift +++ b/apps/ios/Shared/SimpleXApp.swift @@ -16,9 +16,9 @@ struct SimpleXApp: App { @StateObject private var chatModel = ChatModel.shared @ObservedObject var alertManager = AlertManager.shared @Environment(\.scenePhase) var scenePhase - @AppStorage(DEFAULT_PERFORM_LA) private var performLA = false + @AppStorage(DEFAULT_PERFORM_LA) private var prefPerformLA = false @State private var userAuthorized: Bool? = nil - @State private var doAuthenticate: Bool = true + @State private var doAuthenticate: Bool = false @State private var enteredBackground: Double? = nil init() { @@ -30,14 +30,13 @@ struct SimpleXApp: App { var body: some Scene { return WindowGroup { - ContentView(userAuthorized: $userAuthorized) + ContentView(doAuthenticate: $doAuthenticate, enteredBackground: $enteredBackground) .environmentObject(chatModel) .onOpenURL { url in logger.debug("ContentView.onOpenURL: \(url)") chatModel.appOpenUrl = url } .onAppear() { - chatModel.performLA = performLA initializeChat() } .onChange(of: scenePhase) { phase in @@ -46,50 +45,14 @@ struct SimpleXApp: App { switch (phase) { case .background: BGManager.shared.schedule() - doAuthenticate = true + doAuthenticate = false enteredBackground = ProcessInfo.processInfo.systemUptime - case .inactive: - authenticateOnPhaseChange() case .active: - authenticateOnPhaseChange() + doAuthenticate = true default: break } } } } - - private func authenticateOnPhaseChange() { - if doAuthenticate { - doAuthenticate = false - 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 let enteredBackground = enteredBackground { - return ProcessInfo.processInfo.systemUptime - enteredBackground >= 30 - } else { - return true - } - } } diff --git a/apps/ios/Shared/Views/UserSettings/SettingsButton.swift b/apps/ios/Shared/Views/UserSettings/SettingsButton.swift index 7292fd4373..23033fd7cf 100644 --- a/apps/ios/Shared/Views/UserSettings/SettingsButton.swift +++ b/apps/ios/Shared/Views/UserSettings/SettingsButton.swift @@ -11,13 +11,14 @@ import SwiftUI struct SettingsButton: View { @EnvironmentObject var chatModel: ChatModel @State private var showSettings = false + @AppStorage(DEFAULT_PERFORM_LA) private var prefPerformLA = false var body: some View { Button { showSettings = true } label: { Image(systemName: "gearshape") } .sheet(isPresented: $showSettings, content: { - SettingsView(showSettings: $showSettings) + SettingsView(showSettings: $showSettings, performLA: prefPerformLA) }) } } diff --git a/apps/ios/Shared/Views/UserSettings/SettingsView.swift b/apps/ios/Shared/Views/UserSettings/SettingsView.swift index 38d506b352..b3b1b8bc1f 100644 --- a/apps/ios/Shared/Views/UserSettings/SettingsView.swift +++ b/apps/ios/Shared/Views/UserSettings/SettingsView.swift @@ -14,12 +14,16 @@ let appVersion = Bundle.main.object(forInfoDictionaryKey: "CFBundleShortVersionS let appBuild = Bundle.main.object(forInfoDictionaryKey: "CFBundleVersion") as? String +let DEFAULT_SHOW_LA_NOTICE = "showLocalAuthenticationNotice" +let DEFAULT_LA_NOTICE_SHOWN = "localAuthenticationNoticeShown" 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_SHOW_LA_NOTICE: false, + DEFAULT_LA_NOTICE_SHOWN: false, DEFAULT_PERFORM_LA: false, DEFAULT_USE_NOTIFICATIONS: false, DEFAULT_PENDING_CONNECTIONS: true, @@ -32,10 +36,12 @@ struct SettingsView: View { @Environment(\.colorScheme) var colorScheme @EnvironmentObject var chatModel: ChatModel @Binding var showSettings: Bool + @State var performLA: Bool = false + @AppStorage(DEFAULT_LA_NOTICE_SHOWN) private var prefLANoticeShown = false @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 private var performLAToggleReset = false @State var showNotificationsAlert: Bool = false @State var whichNotificationsAlert = NotificationAlert.enable @State var alert: SettingsViewAlert? = nil @@ -72,7 +78,7 @@ struct SettingsView: View { Section("Settings") { settingsRow("lock") { - Toggle("SimpleX Lock", isOn: $chatModel.performLA) + Toggle("SimpleX Lock", isOn: $performLA) } settingsRow("link") { Toggle("Show pending connections", isOn: $pendingConnections) @@ -150,7 +156,8 @@ struct SettingsView: View { } } .navigationTitle("Your settings") - .onChange(of: chatModel.performLA) { performLAToggle in + .onChange(of: performLA) { performLAToggle in + prefLANoticeShown = true if performLAToggleReset { performLAToggleReset = false } else { @@ -181,14 +188,14 @@ struct SettingsView: View { case .failed: prefPerformLA = false withAnimation() { - chatModel.performLA = false + performLA = false } performLAToggleReset = true alert = .laFailedAlert case .unavailable: prefPerformLA = false withAnimation() { - chatModel.performLA = false + performLA = false } performLAToggleReset = true alert = .laUnavailableInstructionAlert @@ -204,7 +211,7 @@ struct SettingsView: View { case .failed: prefPerformLA = true withAnimation() { - chatModel.performLA = true + performLA = true } performLAToggleReset = true alert = .laFailedAlert