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>
This commit is contained in:
JRoberts
2022-05-28 22:09:46 +04:00
committed by GitHub
parent b56ad77502
commit c3c712aa02
8 changed files with 248 additions and 139 deletions
@@ -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<Boolean?>(null)
private val enteredBackground = mutableStateOf<Long?>(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<Boolean?>,
laFailed: MutableState<Boolean>,
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)
@@ -83,6 +83,7 @@
<string name="auth_unavailable">Аутентификация недоступна</string>
<string name="auth_unavailable_instruction_desc">На устройстве не включена аутентификация. Вы можете включить блокировку SimpleX в Настройках после включения аутентификации.</string>
<string name="auth_unavailable_turning_off_desc">На устройстве выключена аутентификация. Отключение блокировки SimpleX Chat.</string>
<string name="auth_retry">Повторить</string>
<!-- Chat Actions - ChatItemView.kt (and general) -->
<string name="reply_verb">Ответить</string>
@@ -83,6 +83,7 @@
<string name="auth_unavailable">Authentication unavailable</string>
<string name="auth_unavailable_instruction_desc">Device authentication is not enabled. You can turn on SimpleX Lock via Settings, once you enable device authentication.</string>
<string name="auth_unavailable_turning_off_desc">Device authentication is disabled. Turning off SimpleX Lock.</string>
<string name="auth_retry">Retry</string>
<!-- Chat Actions - ChatItemView.kt (and general) -->
<string name="reply_verb">Reply</string>
+79 -1
View File
@@ -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!"),
-1
View File
@@ -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
+5 -42
View File
@@ -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
}
}
}
@@ -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)
})
}
}
@@ -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