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 AppLockSetupBiometricsScreen. #1942

Merged
merged 1 commit into from
Oct 23, 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
52 changes: 52 additions & 0 deletions ElementX.xcodeproj/project.pbxproj

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,7 @@
"common_enter_your_pin" = "Enter your PIN";
"common_error" = "Error";
"common_everyone" = "Everyone";
"common_face_id_ios" = "Face ID";
"common_file" = "File";
"common_forward_message" = "Forward message";
"common_gif" = "GIF";
Expand All @@ -116,6 +117,7 @@
"common_mute" = "Mute";
"common_no_results" = "No results";
"common_offline" = "Offline";
"common_optic_id_ios" = "Optic ID";
"common_password" = "Password";
"common_people" = "People";
"common_permalink" = "Permalink";
Expand Down Expand Up @@ -152,6 +154,7 @@
"common_thread" = "Thread";
"common_topic" = "Topic";
"common_topic_placeholder" = "What is this room about?";
"common_touch_id_ios" = "Touch ID";
"common_unable_to_decrypt" = "Unable to decrypt";
"common_unable_to_invite_message" = "Invites couldn't be sent to one or more users.";
"common_unable_to_invite_title" = "Unable to send invite(s)";
Expand Down Expand Up @@ -265,6 +268,8 @@
"screen_analytics_prompt_third_party_sharing" = "We won't share your data with third parties";
"screen_analytics_prompt_title" = "Help improve %1$@";
"screen_analytics_settings_share_data" = "Share analytics data";
"screen_app_lock_biometric_authentication" = "biometric authentication";
"screen_app_lock_biometric_unlock" = "biometric unlock";
"screen_app_lock_forgot_pin" = "Forgot PIN?";
"screen_app_lock_settings_change_pin" = "Change PIN code";
"screen_app_lock_settings_enable_biometric_unlock" = "Allow biometric unlock";
Expand All @@ -274,6 +279,9 @@
"screen_app_lock_settings_remove_pin" = "Remove PIN";
"screen_app_lock_settings_remove_pin_alert_message" = "Are you sure you want to remove PIN?";
"screen_app_lock_settings_remove_pin_alert_title" = "Remove PIN?";
"screen_app_lock_setup_biometric_unlock_allow_title" = "Allow %1$@";
"screen_app_lock_setup_biometric_unlock_skip" = "I’d rather use PIN";
"screen_app_lock_setup_biometric_unlock_subtitle" = "Save yourself some time and use %1$@ to unlock the app each time";
"screen_app_lock_setup_choose_pin" = "Choose PIN";
"screen_app_lock_setup_confirm_pin" = "Confirm PIN";
"screen_app_lock_setup_pin_blacklisted_dialog_content" = "You cannot choose this as your PIN code for security reasons";
Expand Down
20 changes: 20 additions & 0 deletions ElementX/Sources/Generated/Strings.swift
Original file line number Diff line number Diff line change
Expand Up @@ -220,6 +220,8 @@ public enum L10n {
public static var commonError: String { return L10n.tr("Localizable", "common_error") }
/// Everyone
public static var commonEveryone: String { return L10n.tr("Localizable", "common_everyone") }
/// Face ID
public static var commonFaceIdIos: String { return L10n.tr("Localizable", "common_face_id_ios") }
/// File
public static var commonFile: String { return L10n.tr("Localizable", "common_file") }
/// Forward message
Expand Down Expand Up @@ -258,6 +260,8 @@ public enum L10n {
public static var commonNoResults: String { return L10n.tr("Localizable", "common_no_results") }
/// Offline
public static var commonOffline: String { return L10n.tr("Localizable", "common_offline") }
/// Optic ID
public static var commonOpticIdIos: String { return L10n.tr("Localizable", "common_optic_id_ios") }
/// Password
public static var commonPassword: String { return L10n.tr("Localizable", "common_password") }
/// People
Expand Down Expand Up @@ -344,6 +348,8 @@ public enum L10n {
public static var commonTopic: String { return L10n.tr("Localizable", "common_topic") }
/// What is this room about?
public static var commonTopicPlaceholder: String { return L10n.tr("Localizable", "common_topic_placeholder") }
/// Touch ID
public static var commonTouchIdIos: String { return L10n.tr("Localizable", "common_touch_id_ios") }
/// Unable to decrypt
public static var commonUnableToDecrypt: String { return L10n.tr("Localizable", "common_unable_to_decrypt") }
/// Invites couldn't be sent to one or more users.
Expand Down Expand Up @@ -648,6 +654,10 @@ public enum L10n {
public static var screenAnalyticsSettingsReadTermsContentLink: String { return L10n.tr("Localizable", "screen_analytics_settings_read_terms_content_link") }
/// Share analytics data
public static var screenAnalyticsSettingsShareData: String { return L10n.tr("Localizable", "screen_analytics_settings_share_data") }
/// biometric authentication
public static var screenAppLockBiometricAuthentication: String { return L10n.tr("Localizable", "screen_app_lock_biometric_authentication") }
/// biometric unlock
public static var screenAppLockBiometricUnlock: String { return L10n.tr("Localizable", "screen_app_lock_biometric_unlock") }
/// Forgot PIN?
public static var screenAppLockForgotPin: String { return L10n.tr("Localizable", "screen_app_lock_forgot_pin") }
/// Change PIN code
Expand All @@ -666,6 +676,16 @@ public enum L10n {
public static var screenAppLockSettingsRemovePinAlertMessage: String { return L10n.tr("Localizable", "screen_app_lock_settings_remove_pin_alert_message") }
/// Remove PIN?
public static var screenAppLockSettingsRemovePinAlertTitle: String { return L10n.tr("Localizable", "screen_app_lock_settings_remove_pin_alert_title") }
/// Allow %1$@
public static func screenAppLockSetupBiometricUnlockAllowTitle(_ p1: Any) -> String {
return L10n.tr("Localizable", "screen_app_lock_setup_biometric_unlock_allow_title", String(describing: p1))
}
/// I’d rather use PIN
public static var screenAppLockSetupBiometricUnlockSkip: String { return L10n.tr("Localizable", "screen_app_lock_setup_biometric_unlock_skip") }
/// Save yourself some time and use %1$@ to unlock the app each time
public static func screenAppLockSetupBiometricUnlockSubtitle(_ p1: Any) -> String {
return L10n.tr("Localizable", "screen_app_lock_setup_biometric_unlock_subtitle", String(describing: p1))
}
/// Choose PIN
public static var screenAppLockSetupChoosePin: String { return L10n.tr("Localizable", "screen_app_lock_setup_choose_pin") }
/// Confirm PIN
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,21 +28,7 @@ struct AppLockSettingsScreenViewState: BindableState {
var bindings: AppLockSettingsScreenViewStateBindings

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

struct AppLockSettingsScreenViewStateBindings {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
//
// 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 AppLockSetupBiometricsScreenCoordinatorParameters {
let appLockService: AppLockServiceProtocol
}

enum AppLockSetupBiometricsScreenCoordinatorAction {
case `continue`
}

final class AppLockSetupBiometricsScreenCoordinator: CoordinatorProtocol {
private let parameters: AppLockSetupBiometricsScreenCoordinatorParameters
private var viewModel: AppLockSetupBiometricsScreenViewModelProtocol
private let actionsSubject: PassthroughSubject<AppLockSetupBiometricsScreenCoordinatorAction, Never> = .init()
private var cancellables = Set<AnyCancellable>()

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

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

viewModel = AppLockSetupBiometricsScreenViewModel(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 .continue:
self.actionsSubject.send(.continue)
}
}
.store(in: &cancellables)
}

func toPresentable() -> AnyView {
AnyView(AppLockSetupBiometricsScreen(context: viewModel.context))
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
//
// 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 LocalAuthentication
import SFSafeSymbols

enum AppLockSetupBiometricsScreenViewModelAction {
case `continue`
}

struct AppLockSetupBiometricsScreenViewState: BindableState {
/// The supported biometry type on this device.
let biometryType: LABiometryType

var icon: SFSymbol { biometryType.systemSymbol }
var title: String { L10n.screenAppLockSetupBiometricUnlockAllowTitle(biometryType.localizedString) }
var subtitle: String { L10n.screenAppLockSetupBiometricUnlockSubtitle(biometryType.localizedString) }
}

enum AppLockSetupBiometricsScreenViewAction {
/// The user would like to use Touch/Face ID.
case allow
/// The user doesn't want to use biometrics.
case skip
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
//
// 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 AppLockSetupBiometricsScreenViewModelType = StateStoreViewModel<AppLockSetupBiometricsScreenViewState, AppLockSetupBiometricsScreenViewAction>

class AppLockSetupBiometricsScreenViewModel: AppLockSetupBiometricsScreenViewModelType, AppLockSetupBiometricsScreenViewModelProtocol {
private let appLockService: AppLockServiceProtocol
private var actionsSubject: PassthroughSubject<AppLockSetupBiometricsScreenViewModelAction, Never> = .init()

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

init(appLockService: AppLockServiceProtocol) {
self.appLockService = appLockService
super.init(initialViewState: AppLockSetupBiometricsScreenViewState(biometryType: appLockService.biometryType))
}

// MARK: - Public

override func process(viewAction: AppLockSetupBiometricsScreenViewAction) {
MXLog.info("View model: received view action: \(viewAction)")

switch viewAction {
case .allow:
MXLog.info("Enable biometric unlock.")
appLockService.biometricUnlockEnabled = true
actionsSubject.send(.continue)
case .skip:
MXLog.info("Disable biometric unlock.")
appLockService.biometricUnlockEnabled = false
actionsSubject.send(.continue)
}
}
}
Original file line number Diff line number Diff line change
@@ -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 AppLockSetupBiometricsScreenViewModelProtocol {
var actions: AnyPublisher<AppLockSetupBiometricsScreenViewModelAction, Never> { get }
var context: AppLockSetupBiometricsScreenViewModelType.Context { get }
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
//
// 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

/// The screen shown when setting up App Lock that asks the user whether
/// the would like to use Face ID/Touch ID instead of entering their PIN code.
struct AppLockSetupBiometricsScreen: View {
@ObservedObject var context: AppLockSetupBiometricsScreenViewModel.Context

var body: some View {
ScrollView {
VStack(spacing: 48) {
header
}
.padding(.horizontal, 16)
.padding(.top, UIConstants.iconTopPaddingToNavigationBar)
.frame(maxWidth: .infinity)
}
.toolbar(.visible, for: .navigationBar)
.background(Color.compound.bgCanvasDefault.ignoresSafeArea())
.safeAreaInset(edge: .bottom) {
buttons
.padding(.top, 16)
.padding(.horizontal, 16)
.background(Color.compound.bgCanvasDefault.ignoresSafeArea())
}
}

var header: some View {
VStack(spacing: 8) {
Image(systemSymbol: context.viewState.icon)
.font(.system(size: 72))
.padding(.top, 58)
.padding(.bottom, 26)

Text(context.viewState.title)
.font(.compound.headingMDBold)
.multilineTextAlignment(.center)
.foregroundColor(.compound.textPrimary)

Text(context.viewState.subtitle)
.font(.compound.bodyMD)
.multilineTextAlignment(.center)
.foregroundColor(.compound.textSecondary)
}
}

var buttons: some View {
VStack(spacing: 16) {
Button(context.viewState.title) { context.send(viewAction: .allow) }
.buttonStyle(.compound(.primary))

Button { context.send(viewAction: .skip) } label: {
Text(L10n.screenAppLockSetupBiometricUnlockSkip)
.font(.compound.bodyLGSemibold)
.padding(14)
}
}
}
}

// MARK: - Previews

struct AppLockSetupBiometricsScreen_Previews: PreviewProvider, TestablePreview {
static let faceIDViewModel = AppLockSetupBiometricsScreenViewModel(appLockService: AppLockServiceMock.mock(biometryType: .faceID))
static let touchIDViewModel = AppLockSetupBiometricsScreenViewModel(appLockService: AppLockServiceMock.mock(biometryType: .touchID))

static var previews: some View {
NavigationStack {
AppLockSetupBiometricsScreen(context: faceIDViewModel.context)
}
.previewDisplayName("Face ID")

NavigationStack {
AppLockSetupBiometricsScreen(context: touchIDViewModel.context)
}
.previewDisplayName("Touch ID")
}
}
Loading