diff --git a/Sources/SpeziAccessGuard/AccessGuard.swift b/Sources/SpeziAccessGuard/AccessGuard.swift index 4507a34..268102e 100644 --- a/Sources/SpeziAccessGuard/AccessGuard.swift +++ b/Sources/SpeziAccessGuard/AccessGuard.swift @@ -39,44 +39,73 @@ import SwiftUI /// /// You can use the ``AccessGuarded`` SwiftUI [`View`](https://developer.apple.com/documentation/swiftui/view) in your SwiftUI application to /// enforce a code or biometrics-based access guard to SwiftUI views. -public final class AccessGuard: Module { - @Dependency var secureStorage: SecureStorage - @Published var inTheBackground = true - @Published var lastEnteredBackground: Date = .now +public final class AccessGuard: Module, DefaultInitializable { + @Dependency private var secureStorage: SecureStorage + @Published private(set) var inTheBackground = true + @Published private(set) var lastEnteredBackground: Date = .now private let configurations: [AccessGuardConfiguration] private var viewModels: [String: AccessGuardViewModel] = [:] private var cancellables: Set = [] + public convenience init() { + self.init([]) + } + public init(_ configurations: [AccessGuardConfiguration]) { self.configurations = configurations } + @_documentation(visibility: internal) public func sceneDidEnterBackground(_ scene: UIScene) { Task { @MainActor in inTheBackground = true lastEnteredBackground = .now + + for viewModel in viewModels.values { + viewModel.didEnterBackground() + } } } + @_documentation(visibility: internal) public func sceneWillEnterForeground(_ scene: UIScene) { Task { @MainActor in inTheBackground = false + + for viewModel in viewModels.values { + viewModel.willEnterForeground(lastEnteredBackground: lastEnteredBackground) + } } } + /// Resets the access guard for an identifier. + /// + /// The function removes the code and all stored information. + /// - Parameter identifier: The identifier of the access guard. @MainActor public func resetAccessCode(for identifier: AccessGuardConfiguration.Identifier) throws { try viewModel(for: identifier).resetAccessCode() } + /// Determine the setup state of an access lock. + /// + /// Use the ``SetAccessGuard`` view to setup an access guard. + /// - Parameter identifier: The identifier of the access guard. + /// - Returns: Returns `true` of the access guard is successfully setup. False if no access guard is setup. @MainActor public func setupComplete(for identifier: AccessGuardConfiguration.Identifier) -> Bool { viewModel(for: identifier).setup } + /// Locks an access guard. + /// - Parameter identifier: The identifier of the access guard that should be locked. + @MainActor + public func lock(identifier: AccessGuardConfiguration.Identifier) async { + await viewModel(for: identifier).lock() + } @MainActor func viewModel(for identifier: AccessGuardConfiguration.Identifier) -> AccessGuardViewModel { diff --git a/Sources/SpeziAccessGuard/AccessGuardView.swift b/Sources/SpeziAccessGuard/AccessGuardView.swift index ac42a2d..a03089f 100644 --- a/Sources/SpeziAccessGuard/AccessGuardView.swift +++ b/Sources/SpeziAccessGuard/AccessGuardView.swift @@ -10,12 +10,12 @@ import Spezi import SwiftUI -public struct AccessGuardView: View { +struct AccessGuardView: View { private let guardedView: GuardedView @StateObject private var viewModel: AccessGuardViewModel - public var body: some View { + var body: some View { guardedView .overlay { if viewModel.locked { diff --git a/Sources/SpeziAccessGuard/AccessGuardViewModel.swift b/Sources/SpeziAccessGuard/AccessGuardViewModel.swift index 0c39f08..9789ba6 100644 --- a/Sources/SpeziAccessGuard/AccessGuardViewModel.swift +++ b/Sources/SpeziAccessGuard/AccessGuardViewModel.swift @@ -57,21 +57,22 @@ final class AccessGuardViewModel: ObservableObject { self.locked = setup - accessGuard.objectWillChange + + self.objectWillChange .sink { - self.lockAfterInactivity() - self.objectWillChange.send() + accessGuard.objectWillChange.send() } .store(in: &cancellables) } - private func lockAfterInactivity() { - Task { @MainActor in - if let lastEnteredBackground = accessGuard?.lastEnteredBackground, - lastEnteredBackground.addingTimeInterval(configuration.timeout) < .now { - locked = true - } + @MainActor + func didEnterBackground() {} + + @MainActor + func willEnterForeground(lastEnteredBackground: Date) { + if lastEnteredBackground.addingTimeInterval(configuration.timeout) < .now { + locked = true } } @@ -105,6 +106,12 @@ final class AccessGuardViewModel: ObservableObject { } } + func lock() async { + await MainActor.run { + locked = true + } + } + func setAccessCode(_ code: String, codeOption: CodeOptions) async throws { guard configuration.fixedCode == nil else { throw AccessGuardError.storeCodeError @@ -118,8 +125,7 @@ final class AccessGuardViewModel: ObservableObject { try secureStorage.store(credentials: Credentials(username: configuration.identifier, password: accessCodeData)) - await MainActor.run { - locked = true - } + // Ensure that the model is in a state as if the user has just entered the access code. + try await checkAccessCode(code) } } diff --git a/Sources/SpeziAccessGuard/CodeViews/CodeOptions.swift b/Sources/SpeziAccessGuard/CodeViews/CodeOptions.swift index 7044b05..a50dcb6 100644 --- a/Sources/SpeziAccessGuard/CodeViews/CodeOptions.swift +++ b/Sources/SpeziAccessGuard/CodeViews/CodeOptions.swift @@ -47,7 +47,7 @@ public struct CodeOptions: OptionSet, Codable, CaseIterable, Identifiable { public static let all: CodeOptions = [.fourDigitNumeric, .sixDigitNumeric, .customNumeric, .customAlphanumeric] - public var id: Int { + @_documentation(visibility: internal) public var id: Int { rawValue } @@ -63,7 +63,7 @@ public struct CodeOptions: OptionSet, Codable, CaseIterable, Identifiable { return Int.max } - public var description: LocalizedStringResource { + var description: LocalizedStringResource { switch self { case .fourDigitNumeric: return LocalizedStringResource("CODE_OPTIONS_FOUR_DIGIT", bundle: .atURL(from: .module)) @@ -86,11 +86,12 @@ public struct CodeOptions: OptionSet, Codable, CaseIterable, Identifiable { } } - public let rawValue: Int + @_documentation(visibility: internal) public let rawValue: Int /// Raw initializer for the ``CodeOptions`` option set. Do not use this initializer. /// - Parameter rawValue: The raw option set value. + @_documentation(visibility: internal) public init(rawValue: Int) { self.rawValue = rawValue } diff --git a/Tests/UITests/TestAppUITests/TestAppUITests.swift b/Tests/UITests/TestAppUITests/TestAppUITests.swift index 18f7fbb..c45f9f6 100644 --- a/Tests/UITests/TestAppUITests/TestAppUITests.swift +++ b/Tests/UITests/TestAppUITests/TestAppUITests.swift @@ -80,7 +80,15 @@ class TestAppUITests: XCTestCase { XCTAssert(app.images["Passcode set was successful"].waitForExistence(timeout: 2.0)) app.buttons["Back"].tap() + // View should be unlocked as we just set the passcode ... + XCTAssert(app.buttons["Access Guarded"].waitForExistence(timeout: 2.0)) + app.buttons["Access Guarded"].tap() + XCTAssert(app.staticTexts["Secured ..."].waitForExistence(timeout: 2.0)) + XCTAssert(app.staticTexts["Secured ..."].isHittable) + // Try the new passcode + app.terminate() + app.launch() XCTAssert(app.buttons["Access Guarded"].waitForExistence(timeout: 2.0)) app.buttons["Access Guarded"].tap()