Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add App Lock settings screen #1917

Merged
merged 3 commits into from
Oct 19, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
128 changes: 92 additions & 36 deletions ElementX.xcodeproj/project.pbxproj

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,16 @@
"untranslated" = "Untranslated";

"screen_app_lock_title" = "%@ is locked";
"screen_app_lock_settings_change_pin" = "Change PIN code";
"screen_app_lock_settings_remove_pin" = "Remove PIN";
"screen_app_lock_settings_enable_touch_id_ios" = "Allow Touch ID";
"screen_app_lock_settings_enable_face_id_ios" = "Allow Face ID";
"screen_app_lock_settings_enable_optic_id_ios" = "Allow Optic ID";
"screen_app_lock_settings_enable_biometric_unlock" = "Allow biometric unlock";
"screen_app_lock_settings_remove_pin_alert_title" = "Remove PIN?";
"screen_app_lock_settings_remove_pin_alert_message" = "Are you sure you want to remove PIN?";
"common_unlock" = "Unlock";
"common_screen_lock" = "Screen lock";

// MARK: - Soft logout

Expand Down
1 change: 1 addition & 0 deletions ElementX/Sources/Application/AppCoordinator.swift
Original file line number Diff line number Diff line change
Expand Up @@ -425,6 +425,7 @@ class AppCoordinator: AppCoordinatorProtocol, AuthenticationCoordinatorDelegate,
let navigationSplitCoordinator = NavigationSplitCoordinator(placeholderCoordinator: PlaceholderScreenCoordinator())
let userSessionFlowCoordinator = UserSessionFlowCoordinator(userSession: userSession,
navigationSplitCoordinator: navigationSplitCoordinator,
appLockService: appLockFlowCoordinator.appLockService,
bugReportService: ServiceLocator.shared.bugReportService,
roomTimelineControllerFactory: RoomTimelineControllerFactory(),
appSettings: appSettings,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ enum UserSessionFlowCoordinatorAction {
class UserSessionFlowCoordinator: FlowCoordinatorProtocol {
private let userSession: UserSessionProtocol
private let navigationSplitCoordinator: NavigationSplitCoordinator
private let appLockService: AppLockServiceProtocol
private let bugReportService: BugReportServiceProtocol
private let roomTimelineControllerFactory: RoomTimelineControllerFactoryProtocol
private let appSettings: AppSettings
Expand All @@ -48,13 +49,15 @@ class UserSessionFlowCoordinator: FlowCoordinatorProtocol {

init(userSession: UserSessionProtocol,
navigationSplitCoordinator: NavigationSplitCoordinator,
appLockService: AppLockServiceProtocol,
bugReportService: BugReportServiceProtocol,
roomTimelineControllerFactory: RoomTimelineControllerFactoryProtocol,
appSettings: AppSettings,
analytics: AnalyticsService) {
stateMachine = UserSessionFlowCoordinatorStateMachine()
self.userSession = userSession
self.navigationSplitCoordinator = navigationSplitCoordinator
self.appLockService = appLockService
self.bugReportService = bugReportService
self.roomTimelineControllerFactory = roomTimelineControllerFactory
self.appSettings = appSettings
Expand Down Expand Up @@ -328,6 +331,7 @@ class UserSessionFlowCoordinator: FlowCoordinatorProtocol {
let parameters = SettingsScreenCoordinatorParameters(navigationStackCoordinator: settingsNavigationStackCoordinator,
userIndicatorController: userIndicatorController,
userSession: userSession,
appLockService: appLockService,
bugReportService: bugReportService,
notificationSettings: userSession.clientProxy.notificationSettings,
appSettings: appSettings)
Expand Down
18 changes: 18 additions & 0 deletions ElementX/Sources/Generated/Strings+Untranslated.swift
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,26 @@ import Foundation
// swiftlint:disable explicit_type_interface function_parameter_count identifier_name line_length
// swiftlint:disable nesting type_body_length type_name vertical_whitespace_opening_braces
public enum UntranslatedL10n {
/// Screen lock
public static var commonScreenLock: String { return UntranslatedL10n.tr("Untranslated", "common_screen_lock") }
/// Unlock
public static var commonUnlock: String { return UntranslatedL10n.tr("Untranslated", "common_unlock") }
/// Change PIN code
public static var screenAppLockSettingsChangePin: String { return UntranslatedL10n.tr("Untranslated", "screen_app_lock_settings_change_pin") }
/// Allow biometric unlock
public static var screenAppLockSettingsEnableBiometricUnlock: String { return UntranslatedL10n.tr("Untranslated", "screen_app_lock_settings_enable_biometric_unlock") }
/// Allow Face ID
public static var screenAppLockSettingsEnableFaceIdIos: String { return UntranslatedL10n.tr("Untranslated", "screen_app_lock_settings_enable_face_id_ios") }
/// Allow Optic ID
public static var screenAppLockSettingsEnableOpticIdIos: String { return UntranslatedL10n.tr("Untranslated", "screen_app_lock_settings_enable_optic_id_ios") }
/// Allow Touch ID
public static var screenAppLockSettingsEnableTouchIdIos: String { return UntranslatedL10n.tr("Untranslated", "screen_app_lock_settings_enable_touch_id_ios") }
/// Remove PIN
public static var screenAppLockSettingsRemovePin: String { return UntranslatedL10n.tr("Untranslated", "screen_app_lock_settings_remove_pin") }
/// Are you sure you want to remove PIN?
public static var screenAppLockSettingsRemovePinAlertMessage: String { return UntranslatedL10n.tr("Untranslated", "screen_app_lock_settings_remove_pin_alert_message") }
/// Remove PIN?
public static var screenAppLockSettingsRemovePinAlertTitle: String { return UntranslatedL10n.tr("Untranslated", "screen_app_lock_settings_remove_pin_alert_title") }
/// %@ is locked
public static func screenAppLockTitle(_ p1: Any) -> String {
return UntranslatedL10n.tr("Untranslated", "screen_app_lock_title", String(describing: p1))
Expand Down
127 changes: 125 additions & 2 deletions ElementX/Sources/Mocks/Generated/GeneratedMocks.swift
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,12 @@
// DO NOT EDIT

// swiftlint:disable all
import AnalyticsEvents
import Combine
import Foundation
import SwiftUI
import AnalyticsEvents
import LocalAuthentication
import MatrixRustSDK
import SwiftUI
class AnalyticsClientMock: AnalyticsClientProtocol {
var isRunning: Bool {
get { return underlyingIsRunning }
Expand Down Expand Up @@ -115,6 +116,128 @@ class AnalyticsClientMock: AnalyticsClientProtocol {
updateUserPropertiesClosure?(userProperties)
}
}
class AppLockServiceMock: AppLockServiceProtocol {
var isEnabled: Bool {
get { return underlyingIsEnabled }
set(value) { underlyingIsEnabled = value }
}
var underlyingIsEnabled: Bool!
var biometryType: LABiometryType {
get { return underlyingBiometryType }
set(value) { underlyingBiometryType = value }
}
var underlyingBiometryType: LABiometryType!
var biometricUnlockEnabled: Bool {
get { return underlyingBiometricUnlockEnabled }
set(value) { underlyingBiometricUnlockEnabled = value }
}
var underlyingBiometricUnlockEnabled: Bool!

//MARK: - setupPINCode

var setupPINCodeCallsCount = 0
var setupPINCodeCalled: Bool {
return setupPINCodeCallsCount > 0
}
var setupPINCodeReceivedPinCode: String?
var setupPINCodeReceivedInvocations: [String] = []
var setupPINCodeReturnValue: Result<Void, AppLockServiceError>!
var setupPINCodeClosure: ((String) -> Result<Void, AppLockServiceError>)?

func setupPINCode(_ pinCode: String) -> Result<Void, AppLockServiceError> {
setupPINCodeCallsCount += 1
setupPINCodeReceivedPinCode = pinCode
setupPINCodeReceivedInvocations.append(pinCode)
if let setupPINCodeClosure = setupPINCodeClosure {
return setupPINCodeClosure(pinCode)
} else {
return setupPINCodeReturnValue
}
}
//MARK: - disable

var disableCallsCount = 0
var disableCalled: Bool {
return disableCallsCount > 0
}
var disableClosure: (() -> Void)?

func disable() {
disableCallsCount += 1
disableClosure?()
}
//MARK: - applicationDidEnterBackground

var applicationDidEnterBackgroundCallsCount = 0
var applicationDidEnterBackgroundCalled: Bool {
return applicationDidEnterBackgroundCallsCount > 0
}
var applicationDidEnterBackgroundClosure: (() -> Void)?

func applicationDidEnterBackground() {
applicationDidEnterBackgroundCallsCount += 1
applicationDidEnterBackgroundClosure?()
}
//MARK: - computeNeedsUnlock

var computeNeedsUnlockWillEnterForegroundAtCallsCount = 0
var computeNeedsUnlockWillEnterForegroundAtCalled: Bool {
return computeNeedsUnlockWillEnterForegroundAtCallsCount > 0
}
var computeNeedsUnlockWillEnterForegroundAtReceivedDate: Date?
var computeNeedsUnlockWillEnterForegroundAtReceivedInvocations: [Date] = []
var computeNeedsUnlockWillEnterForegroundAtReturnValue: Bool!
var computeNeedsUnlockWillEnterForegroundAtClosure: ((Date) -> Bool)?

func computeNeedsUnlock(willEnterForegroundAt date: Date) -> Bool {
computeNeedsUnlockWillEnterForegroundAtCallsCount += 1
computeNeedsUnlockWillEnterForegroundAtReceivedDate = date
computeNeedsUnlockWillEnterForegroundAtReceivedInvocations.append(date)
if let computeNeedsUnlockWillEnterForegroundAtClosure = computeNeedsUnlockWillEnterForegroundAtClosure {
return computeNeedsUnlockWillEnterForegroundAtClosure(date)
} else {
return computeNeedsUnlockWillEnterForegroundAtReturnValue
}
}
//MARK: - unlock

var unlockWithCallsCount = 0
var unlockWithCalled: Bool {
return unlockWithCallsCount > 0
}
var unlockWithReceivedPinCode: String?
var unlockWithReceivedInvocations: [String] = []
var unlockWithReturnValue: Bool!
var unlockWithClosure: ((String) -> Bool)?

func unlock(with pinCode: String) -> Bool {
unlockWithCallsCount += 1
unlockWithReceivedPinCode = pinCode
unlockWithReceivedInvocations.append(pinCode)
if let unlockWithClosure = unlockWithClosure {
return unlockWithClosure(pinCode)
} else {
return unlockWithReturnValue
}
}
//MARK: - unlockWithBiometrics

var unlockWithBiometricsCallsCount = 0
var unlockWithBiometricsCalled: Bool {
return unlockWithBiometricsCallsCount > 0
}
var unlockWithBiometricsReturnValue: Bool!
var unlockWithBiometricsClosure: (() -> Bool)?

func unlockWithBiometrics() -> Bool {
unlockWithBiometricsCallsCount += 1
if let unlockWithBiometricsClosure = unlockWithBiometricsClosure {
return unlockWithBiometricsClosure()
} else {
return unlockWithBiometricsReturnValue
}
}
}
class AudioConverterMock: AudioConverterProtocol {

//MARK: - convertToOpusOgg
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -47,8 +47,7 @@ struct AppLockScreen: View {

// Add TestablePreview conformance once we have designs.
struct AppLockScreen_Previews: PreviewProvider {
static let viewModel = AppLockScreenViewModel(appLockService: AppLockService(keychainController: KeychainControllerMock(),
appSettings: ServiceLocator.shared.settings))
static let viewModel = AppLockScreenViewModel(appLockService: AppLockServiceMock.mock())

static var previews: some View {
NavigationStack {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
//
// 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 AppLockSettingsScreenCoordinatorParameters {
let appLockService: AppLockServiceProtocol
}

enum AppLockSettingsScreenCoordinatorAction {
case done
}

final class AppLockSettingsScreenCoordinator: CoordinatorProtocol {
private let parameters: AppLockSettingsScreenCoordinatorParameters
private var viewModel: AppLockSettingsScreenViewModelProtocol
private let actionsSubject: PassthroughSubject<AppLockSettingsScreenCoordinatorAction, Never> = .init()
private var cancellables = Set<AnyCancellable>()

var actions: AnyPublisher<AppLockSettingsScreenCoordinatorAction, Never> {
actionsSubject.eraseToAnyPublisher()
}

init(parameters: AppLockSettingsScreenCoordinatorParameters) {
self.parameters = parameters

viewModel = AppLockSettingsScreenViewModel(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:
break
case .done:
self.actionsSubject.send(.done)
}
}
.store(in: &cancellables)
}

func toPresentable() -> AnyView {
AnyView(AppLockSettingsScreen(context: viewModel.context))
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
//
// 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 AppLockSettingsScreenViewModelAction {
/// The user would like to enter new PIN code.
case changePINCode
case done
}

struct AppLockSettingsScreenViewState: BindableState {
let biometryType: LABiometryType
var bindings: AppLockSettingsScreenViewStateBindings

var supportsBiometry: Bool { biometryType != .none }
var enableBiometryTitle: String {
switch biometryType {
case .none:
return L10n.commonError
case .touchID:
return UntranslatedL10n.screenAppLockSettingsEnableTouchIdIos
case .faceID:
return UntranslatedL10n.screenAppLockSettingsEnableFaceIdIos
// Requires Xcode 15:
// case .opticID:
// UntranslatedL10n.screenAppLockSettingsEnableOpticIdIos
@unknown default:
return UntranslatedL10n.screenAppLockSettingsEnableBiometricUnlock
}
}
}

struct AppLockSettingsScreenViewStateBindings {
var enableBiometrics: Bool
var alertInfo: AlertInfo<AppLockSettingsScreenAlertType>?
}

enum AppLockSettingsScreenAlertType {
/// The alert shown to confirm the user would like to remove their PIN.
case confirmRemovePINCode
}

enum AppLockSettingsScreenViewAction {
/// 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
case done
}
Loading