Initial implementation for AppLockSetupFlowCoordinator. (#1949)

Rename AppLockSettingsScreen to AppLockSetupSettingsScreen.
Implement forced logout and persist PIN entry attempts.
This commit is contained in:
Doug
2023-10-25 12:03:37 +01:00
committed by GitHub
parent 30b18031a0
commit c6e4dc2a37
53 changed files with 773 additions and 234 deletions
@@ -0,0 +1,65 @@
//
// Copyright 2022 New Vector Ltd
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
import Combine
import SwiftUI
struct AppLockSetupSettingsScreenCoordinatorParameters {
/// Whether App Lock is mandatory and can be disabled by the user.
let isMandatory: Bool
let appLockService: AppLockServiceProtocol
}
enum AppLockSetupSettingsScreenCoordinatorAction {
case changePINCode
case appLockDisabled
}
final class AppLockSetupSettingsScreenCoordinator: CoordinatorProtocol {
private let parameters: AppLockSetupSettingsScreenCoordinatorParameters
private var viewModel: AppLockSetupSettingsScreenViewModelProtocol
private let actionsSubject: PassthroughSubject<AppLockSetupSettingsScreenCoordinatorAction, Never> = .init()
private var cancellables = Set<AnyCancellable>()
var actions: AnyPublisher<AppLockSetupSettingsScreenCoordinatorAction, Never> {
actionsSubject.eraseToAnyPublisher()
}
init(parameters: AppLockSetupSettingsScreenCoordinatorParameters) {
self.parameters = parameters
viewModel = AppLockSetupSettingsScreenViewModel(appLockService: parameters.appLockService)
}
func start() {
viewModel.actions.sink { [weak self] action in
MXLog.info("Coordinator: received view model action: \(action)")
guard let self else { return }
switch action {
case .changePINCode:
actionsSubject.send(.changePINCode)
case .appLockDisabled:
actionsSubject.send(.appLockDisabled)
}
}
.store(in: &cancellables)
}
func toPresentable() -> AnyView {
AnyView(AppLockSetupSettingsScreen(context: viewModel.context))
}
}
@@ -0,0 +1,54 @@
//
// Copyright 2022 New Vector Ltd
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
import Foundation
import LocalAuthentication
enum AppLockSetupSettingsScreenViewModelAction {
/// The user would like to enter new PIN code.
case changePINCode
/// The user has disabled the App Lock feature.
case appLockDisabled
}
struct AppLockSetupSettingsScreenViewState: BindableState {
/// Whether App Lock is mandatory and can be disabled by the user.
let isMandatory: Bool
let biometryType: LABiometryType
var bindings: AppLockSetupSettingsScreenViewStateBindings
var supportsBiometry: Bool { biometryType != .none }
var enableBiometryTitle: String { L10n.screenAppLockSetupBiometricUnlockAllowTitle(biometryType.localizedString) }
}
struct AppLockSetupSettingsScreenViewStateBindings {
var enableBiometrics: Bool
var alertInfo: AlertInfo<AppLockSetupSettingsScreenAlertType>?
}
enum AppLockSetupSettingsScreenAlertType {
/// The alert shown to confirm the user would like to remove their PIN.
case confirmRemovePINCode
}
enum AppLockSetupSettingsScreenViewAction {
/// The user would like to enter a new PIN code.
case changePINCode
/// The user would like to disable the App Lock feature.
case disable
/// The user has toggled the biometrics setting.
case enableBiometricsChanged
}
@@ -0,0 +1,68 @@
//
// Copyright 2022 New Vector Ltd
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
import Combine
import SwiftUI
typealias AppLockSetupSettingsScreenViewModelType = StateStoreViewModel<AppLockSetupSettingsScreenViewState, AppLockSetupSettingsScreenViewAction>
class AppLockSetupSettingsScreenViewModel: AppLockSetupSettingsScreenViewModelType, AppLockSetupSettingsScreenViewModelProtocol {
private let appLockService: AppLockServiceProtocol
private var actionsSubject: PassthroughSubject<AppLockSetupSettingsScreenViewModelAction, Never> = .init()
var actions: AnyPublisher<AppLockSetupSettingsScreenViewModelAction, Never> {
actionsSubject.eraseToAnyPublisher()
}
init(appLockService: AppLockServiceProtocol) {
self.appLockService = appLockService
super.init(initialViewState: AppLockSetupSettingsScreenViewState(isMandatory: appLockService.isMandatory,
biometryType: appLockService.biometryType,
bindings: .init(enableBiometrics: appLockService.biometricUnlockEnabled)))
}
// MARK: - Public
override func process(viewAction: AppLockSetupSettingsScreenViewAction) {
MXLog.info("View model: received view action: \(viewAction)")
switch viewAction {
case .changePINCode:
actionsSubject.send(.changePINCode)
case .disable:
showRemovePINAlert()
case .enableBiometricsChanged:
appLockService.biometricUnlockEnabled = state.bindings.enableBiometrics
}
}
// MARK: - Private
/// Shows a confirmation alert to the user before removing their PIN code.
private func showRemovePINAlert() {
state.bindings.alertInfo = .init(id: .confirmRemovePINCode,
title: L10n.screenAppLockSettingsRemovePinAlertTitle,
message: L10n.screenAppLockSettingsRemovePinAlertMessage,
primaryButton: .init(title: L10n.actionYes) { self.completeRemovePIN() },
secondaryButton: .init(title: L10n.actionCancel, role: .cancel, action: nil))
}
/// Removes the user's PIN code, disabling the App Lock feature.
private func completeRemovePIN() {
appLockService.disable()
actionsSubject.send(.appLockDisabled)
}
}
@@ -0,0 +1,23 @@
//
// Copyright 2022 New Vector Ltd
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
import Combine
@MainActor
protocol AppLockSetupSettingsScreenViewModelProtocol {
var actions: AnyPublisher<AppLockSetupSettingsScreenViewModelAction, Never> { get }
var context: AppLockSetupSettingsScreenViewModelType.Context { get }
}
@@ -0,0 +1,75 @@
//
// Copyright 2022 New Vector Ltd
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
import Compound
import SwiftUI
struct AppLockSetupSettingsScreen: View {
@ObservedObject var context: AppLockSetupSettingsScreenViewModel.Context
var body: some View {
Form {
Section {
ListRow(label: .plain(title: L10n.screenAppLockSettingsChangePin),
kind: .button { context.send(viewAction: .changePINCode) })
if !context.viewState.isMandatory {
ListRow(label: .plain(title: L10n.screenAppLockSettingsRemovePin, role: .destructive),
kind: .button { context.send(viewAction: .disable) })
}
}
if context.viewState.supportsBiometry {
Section {
ListRow(label: .plain(title: context.viewState.enableBiometryTitle),
kind: .toggle($context.enableBiometrics))
.onChange(of: context.enableBiometrics) { _ in
context.send(viewAction: .enableBiometricsChanged)
}
}
}
}
.compoundList()
.navigationTitle(L10n.commonScreenLock)
.navigationBarTitleDisplayMode(.inline)
.alert(item: $context.alertInfo)
}
}
// MARK: - Previews
struct AppLockSetupSettingsScreen_Previews: PreviewProvider, TestablePreview {
static let faceIDViewModel = AppLockSetupSettingsScreenViewModel(appLockService: AppLockServiceMock.mock(biometryType: .faceID))
static let touchIDViewModel = AppLockSetupSettingsScreenViewModel(appLockService: AppLockServiceMock.mock(isMandatory: true, biometryType: .touchID))
static let biometricsUnavailableViewModel = AppLockSetupSettingsScreenViewModel(appLockService: AppLockServiceMock.mock(biometryType: .none))
static var previews: some View {
NavigationStack {
AppLockSetupSettingsScreen(context: faceIDViewModel.context)
}
.previewDisplayName("Face ID")
NavigationStack {
AppLockSetupSettingsScreen(context: touchIDViewModel.context)
}
.previewDisplayName("Touch ID (Mandatory)")
NavigationStack {
AppLockSetupSettingsScreen(context: biometricsUnavailableViewModel.context)
}
.previewDisplayName("PIN only")
}
}