From 85cd64384a236e426cbc1888e883516bcd48c3ab Mon Sep 17 00:00:00 2001 From: Paul Schmiedmayer Date: Sun, 10 Sep 2023 00:56:10 -0700 Subject: [PATCH 1/9] Improve Code Structure --- Package.swift | 2 +- Sources/SpeziAccessCode/AccessGuard.swift | 12 ++++-- .../AccessGuardViewModel.swift | 4 +- Sources/SpeziAccessCode/AccessGuarded.swift | 2 +- .../CodeViews/CodeOptions.swift | 4 +- .../SpeziAccessCode/CodeViews/CodeView.swift | 37 ++++++++++++----- .../CodeViews/ErrorMessageCapsule.swift | 3 ++ .../CodeViews/SetCodeView.swift | 22 ++++++---- .../Extensions/AnyTransition+Navigate.swift | 20 ++++++++++ Sources/SpeziAccessCode/SetAccessGuard.swift | 40 +++++++++++++++++++ Tests/UITests/TestApp/TestApp.swift | 25 ++++++++++-- 11 files changed, 141 insertions(+), 30 deletions(-) create mode 100644 Sources/SpeziAccessCode/Extensions/AnyTransition+Navigate.swift create mode 100644 Sources/SpeziAccessCode/SetAccessGuard.swift diff --git a/Package.swift b/Package.swift index 8db4ab5..a1725f8 100644 --- a/Package.swift +++ b/Package.swift @@ -23,7 +23,7 @@ let package = Package( dependencies: [ .package(url: "https://github.com/StanfordSpezi/Spezi", .upToNextMinor(from: "0.7.2")), .package(url: "https://github.com/StanfordSpezi/SpeziStorage", .upToNextMinor(from: "0.4.2")), - .package(url: "https://github.com/StanfordSpezi/SpeziViews", .upToNextMinor(from: "0.4.2")), + .package(url: "https://github.com/StanfordSpezi/SpeziViews", .upToNextMinor(from: "0.4.2")) ], targets: [ .target( diff --git a/Sources/SpeziAccessCode/AccessGuard.swift b/Sources/SpeziAccessCode/AccessGuard.swift index 00fdd64..6e53c13 100644 --- a/Sources/SpeziAccessCode/AccessGuard.swift +++ b/Sources/SpeziAccessCode/AccessGuard.swift @@ -39,7 +39,7 @@ public final class AccessGuard: Module { @Dependency var secureStorage: SecureStorage @Published var inTheBackground = true @Published var lastEnteredBackground: Date = .now - @Published var viewModels: [String: AccessGuardViewModel] = [:] + var viewModels: [String: AccessGuardViewModel] = [:] private var cancellables: Set = [] @@ -47,12 +47,16 @@ public final class AccessGuard: Module { public func sceneDidEnterBackground(_ scene: UIScene) { - inTheBackground = true - lastEnteredBackground = .now + Task { @MainActor in + inTheBackground = true + lastEnteredBackground = .now + } } public func sceneWillEnterForeground(_ scene: UIScene) { - inTheBackground = false + Task { @MainActor in + inTheBackground = false + } } diff --git a/Sources/SpeziAccessCode/AccessGuardViewModel.swift b/Sources/SpeziAccessCode/AccessGuardViewModel.swift index b55be80..af1fa1c 100644 --- a/Sources/SpeziAccessCode/AccessGuardViewModel.swift +++ b/Sources/SpeziAccessCode/AccessGuardViewModel.swift @@ -30,7 +30,6 @@ final class AccessGuardViewModel: ObservableObject { private var cancellables: Set = [] - var codeOption: CodeOptions? { accessCode?.codeOption } @@ -74,12 +73,15 @@ final class AccessGuardViewModel: ObservableObject { func checkAccessCode(_ code: String) throws { if let fixedCode, code == fixedCode { + locked = false return } guard code == accessCode?.code else { throw AccessGuardError.wrongPasscode } + + locked = false } func setAccessCode(_ code: String, codeOption: CodeOptions) throws { diff --git a/Sources/SpeziAccessCode/AccessGuarded.swift b/Sources/SpeziAccessCode/AccessGuarded.swift index 7400297..8cd4262 100644 --- a/Sources/SpeziAccessCode/AccessGuarded.swift +++ b/Sources/SpeziAccessCode/AccessGuarded.swift @@ -76,7 +76,7 @@ public struct AccessGuarded: View { ) { precondition( codeOption.verifyStructore(ofCode: fixedCode), - "The provided fixed code \"\(fixedCode)\" must conform to the \(codeOption.description) code option." + "The provided fixed code \"\(fixedCode)\" must conform to the \(codeOption.description.localizedString()) code option." ) self.configuration = AccessGuardConfiguration( diff --git a/Sources/SpeziAccessCode/CodeViews/CodeOptions.swift b/Sources/SpeziAccessCode/CodeViews/CodeOptions.swift index dbaf55c..74fadc4 100644 --- a/Sources/SpeziAccessCode/CodeViews/CodeOptions.swift +++ b/Sources/SpeziAccessCode/CodeViews/CodeOptions.swift @@ -79,7 +79,7 @@ public struct CodeOptions: OptionSet, Codable, CaseIterable, Identifiable { } var keyBoardType: UIKeyboardType { - if self.contains(.customAlphanumeric) { + if self == .customAlphanumeric { return .default } else { return .numberPad @@ -99,7 +99,7 @@ public struct CodeOptions: OptionSet, Codable, CaseIterable, Identifiable { func verifyStructore(ofCode code: String) -> Bool { switch self { case .fourDigitNumeric, .sixDigitNumeric: - return code.isEmpty && code.count == maxLength + return code.isNumeric && code.count == maxLength case .customNumeric: return code.isNumeric && code.count >= CodeOptions.fourDigitNumeric.maxLength case .customAlphanumeric: diff --git a/Sources/SpeziAccessCode/CodeViews/CodeView.swift b/Sources/SpeziAccessCode/CodeViews/CodeView.swift index 19b5dce..c4b99f6 100644 --- a/Sources/SpeziAccessCode/CodeViews/CodeView.swift +++ b/Sources/SpeziAccessCode/CodeViews/CodeView.swift @@ -11,14 +11,13 @@ import SwiftUI struct CodeView: View { - private let codeOption: CodeOptions - private let action: (String) async throws -> () + @Binding private var codeOption: CodeOptions + private let action: (String) async throws -> Void @State private var code: String = "" @FocusState private var focused: Bool @State private var viewState: ViewState = .idle @State private var wrongCodeCounter: Int = 0 - - + @State private var oldKeyBoardType: UIKeyboardType? var body: some View { @@ -60,19 +59,38 @@ struct CodeView: View { } .onAppear { focused = true + oldKeyBoardType = codeOption.keyBoardType } - .onChange(of: code) { newCode in + .onChange(of: code) { _ in if code.count >= codeOption.maxLength { Task { @MainActor in await checkCode() } } } + .onChange(of: codeOption) { newCodeOption in + guard oldKeyBoardType != newCodeOption.keyBoardType else { + return + } + + focused = false + oldKeyBoardType = newCodeOption.keyBoardType + + Task { @MainActor in + try await Task.sleep(for: .seconds(0.05)) + focused = true + } + } } - init(codeOption: CodeOptions, action: @escaping (String) async throws -> ()) { - self.codeOption = codeOption + init(codeOption: CodeOptions, action: @escaping (String) async throws -> Void) { + self._codeOption = .constant(codeOption) + self.action = action + } + + init(codeOption: Binding, action: @escaping (String) async throws -> Void) { + self._codeOption = codeOption self.action = action } @@ -89,7 +107,7 @@ struct CodeView: View { viewState = .idle - try? await Task.sleep(for: .seconds(0.1)) + try? await Task.sleep(for: .seconds(0.05)) focused = true } } @@ -97,10 +115,9 @@ struct CodeView: View { struct AuthenticationView_Previews: PreviewProvider { static var previews: some View { - CodeView(codeOption: .fourDigitNumeric) { code in + CodeView(codeOption: .fourDigitNumeric) { _ in try await Task.sleep(for: .seconds(1)) } .padding(.horizontal) } } - diff --git a/Sources/SpeziAccessCode/CodeViews/ErrorMessageCapsule.swift b/Sources/SpeziAccessCode/CodeViews/ErrorMessageCapsule.swift index f770b63..0b603c4 100644 --- a/Sources/SpeziAccessCode/CodeViews/ErrorMessageCapsule.swift +++ b/Sources/SpeziAccessCode/CodeViews/ErrorMessageCapsule.swift @@ -17,6 +17,9 @@ struct ErrorMessageCapsule: View { Group { if let errorMessage { Text(errorMessage) + .foregroundStyle(.white) + .padding(.horizontal) + .padding(.vertical, 4) .background { RoundedRectangle(cornerRadius: 25) .foregroundStyle(.red) diff --git a/Sources/SpeziAccessCode/CodeViews/SetCodeView.swift b/Sources/SpeziAccessCode/CodeViews/SetCodeView.swift index cc30e33..04e1c97 100644 --- a/Sources/SpeziAccessCode/CodeViews/SetCodeView.swift +++ b/Sources/SpeziAccessCode/CodeViews/SetCodeView.swift @@ -33,7 +33,7 @@ struct SetCodeView: View { Text("SET_PASSCODE_PROMPT", bundle: .module) .font(.title2) .frame(maxWidth: .infinity) - CodeView(codeOption: selectedCode) { code in + CodeView(codeOption: $selectedCode) { code in firstCode = code withAnimation { state = .repeatCode @@ -48,16 +48,17 @@ struct SetCodeView: View { } .frame(height: 60) } - .transition(.slide) + .transition(.navigate) case .repeatCode: Group { Text("SET_PASSCODE_REPEAT_PROMPT", bundle: .module) .font(.title2) .frame(maxWidth: .infinity) - CodeView(codeOption: selectedCode) { code in + CodeView(codeOption: $selectedCode) { code in if code == firstCode { do { try viewModel.setAccessCode(code, codeOption: selectedCode) + errorMessage = nil withAnimation { state = .success } @@ -71,19 +72,26 @@ struct SetCodeView: View { ErrorMessageCapsule(errorMessage: $errorMessage) .frame(height: 60) } - .transition(.slide) + .transition(.navigate) case .success: Image(systemName: "checkmark.circle.fill") + .resizable() + .aspectRatio(1.0, contentMode: .fit) + .frame(height: 100) .foregroundStyle(.green) - .transition(.slide) + .transition(.navigate) } } .toolbar { if state == .repeatCode { - ToolbarItem { + ToolbarItem(placement: .cancellationAction) { Button( action: { - + withAnimation { + state = .setCode + firstCode = "" + errorMessage = nil + } }, label: { Text("SET_PASSCODE_BACK_BUTTON", bundle: .module) } diff --git a/Sources/SpeziAccessCode/Extensions/AnyTransition+Navigate.swift b/Sources/SpeziAccessCode/Extensions/AnyTransition+Navigate.swift new file mode 100644 index 0000000..64a206c --- /dev/null +++ b/Sources/SpeziAccessCode/Extensions/AnyTransition+Navigate.swift @@ -0,0 +1,20 @@ +// +// This source file is part of the Spezi open source project +// +// SPDX-FileCopyrightText: 2022 Stanford University and the project authors (see CONTRIBUTORS.md) +// +// SPDX-License-Identifier: MIT +// + + +import SwiftUI + + +extension AnyTransition { + static var navigate: AnyTransition { + AnyTransition.asymmetric( + insertion: .move(edge: .trailing), + removal: .move(edge: .leading) + ) + } +} diff --git a/Sources/SpeziAccessCode/SetAccessGuard.swift b/Sources/SpeziAccessCode/SetAccessGuard.swift new file mode 100644 index 0000000..65a9650 --- /dev/null +++ b/Sources/SpeziAccessCode/SetAccessGuard.swift @@ -0,0 +1,40 @@ +// +// This source file is part of the Spezi open source project +// +// SPDX-FileCopyrightText: 2022 Stanford University and the project authors (see CONTRIBUTORS.md) +// +// SPDX-License-Identifier: MIT +// + +import SpeziSecureStorage +import SwiftUI + + +public struct SetAccessGuard: View { + @EnvironmentObject private var accessGuard: AccessGuard + + private let configuration: AccessGuardConfiguration + private let identifier: String + + + public var body: some View { + SetCodeView( + viewModel: accessGuard.viewModel( + for: identifier, + configuration: configuration + ) + ) + } + + + /// - Parameters: + /// - configuration: The access code configuration that defines the behaviour of the view. See ``AccessGuardConfiguration`` for more information. + /// - identifier: The identifier of the credentials that should be used to guard this view. + public init( + configuration: AccessGuardConfiguration = .code, + identifier: String + ) { + self.configuration = configuration + self.identifier = identifier + } +} diff --git a/Tests/UITests/TestApp/TestApp.swift b/Tests/UITests/TestApp/TestApp.swift index 31b4deb..1086d8a 100644 --- a/Tests/UITests/TestApp/TestApp.swift +++ b/Tests/UITests/TestApp/TestApp.swift @@ -17,11 +17,28 @@ struct UITestsApp: App { var body: some Scene { WindowGroup { - AccessGuarded { - Color.green - .overlay { - Text("Secured ...") + NavigationStack { + List { + NavigationLink("Access Guarded") { + AccessGuarded(identifier: "TestIdentifier") { + Color.green + .overlay { + Text("Secured ...") + } + } } + NavigationLink("Access Guarded Fixed") { + AccessGuarded(fixedCode: "1234") { + Color.green + .overlay { + Text("Secured with fixed code ...") + } + } + } + NavigationLink("Set Code") { + SetAccessGuard(identifier: "TestIdentifier") + } + } } .spezi(appDelegate) } From 60069d9640368f1e095594e52169ecae6b36d6a3 Mon Sep 17 00:00:00 2001 From: Paul Schmiedmayer Date: Sun, 10 Sep 2023 01:14:04 -0700 Subject: [PATCH 2/9] Improve Code --- Sources/SpeziAccessCode/AccessGuard.swift | 1 - .../AccessGuardViewModel.swift | 47 +++++++++++-------- .../CodeViews/EnterCodeView.swift | 2 +- .../CodeViews/SetCodeView.swift | 21 +++++++-- Tests/UITests/TestApp/TestApp.swift | 2 +- 5 files changed, 46 insertions(+), 27 deletions(-) diff --git a/Sources/SpeziAccessCode/AccessGuard.swift b/Sources/SpeziAccessCode/AccessGuard.swift index 6e53c13..d0ab068 100644 --- a/Sources/SpeziAccessCode/AccessGuard.swift +++ b/Sources/SpeziAccessCode/AccessGuard.swift @@ -73,7 +73,6 @@ public final class AccessGuard: Module { configuration: configuration ) viewModels[identifier] = viewModel - viewModel.objectWillChange.sink(receiveValue: { self.objectWillChange.send() }).store(in: &cancellables) return viewModel } } diff --git a/Sources/SpeziAccessCode/AccessGuardViewModel.swift b/Sources/SpeziAccessCode/AccessGuardViewModel.swift index af1fa1c..5cc14b4 100644 --- a/Sources/SpeziAccessCode/AccessGuardViewModel.swift +++ b/Sources/SpeziAccessCode/AccessGuardViewModel.swift @@ -19,7 +19,7 @@ final class AccessGuardViewModel: ObservableObject { } - @Published private(set) var locked = true + @MainActor @Published private(set) var locked = true let configuration: AccessGuardConfiguration private var identifier: String @@ -31,7 +31,11 @@ final class AccessGuardViewModel: ObservableObject { var codeOption: CodeOptions? { - accessCode?.codeOption + if fixedCode != nil { + return configuration.codeOptions + } else { + return accessCode?.codeOption + } } @@ -60,31 +64,32 @@ final class AccessGuardViewModel: ObservableObject { private func updateState() { - if accessGuard?.inTheBackground == true { - locked = true - } else if let lastEnteredBackground = accessGuard?.lastEnteredBackground, - lastEnteredBackground.addingTimeInterval(configuration.timeout) >= .now { - locked = false - } else { - locked = true + Task { @MainActor in + if let lastEnteredBackground = accessGuard?.lastEnteredBackground, lastEnteredBackground.addingTimeInterval(configuration.timeout) >= .now { + locked = false + } else { + locked = true + } } } - func checkAccessCode(_ code: String) throws { - if let fixedCode, code == fixedCode { + func checkAccessCode(_ code: String) async throws { + try await MainActor.run { + if let fixedCode, code == fixedCode { + locked = false + return + } + + guard code == accessCode?.code else { + throw AccessGuardError.wrongPasscode + } + locked = false - return - } - - guard code == accessCode?.code else { - throw AccessGuardError.wrongPasscode } - - locked = false } - func setAccessCode(_ code: String, codeOption: CodeOptions) throws { + func setAccessCode(_ code: String, codeOption: CodeOptions) async throws { guard fixedCode == nil else { throw AccessGuardError.storeCodeError } @@ -96,5 +101,9 @@ final class AccessGuardViewModel: ObservableObject { } try secureStorage.store(credentials: Credentials(username: identifier, password: accessCodeData)) + + await MainActor.run { + locked = true + } } } diff --git a/Sources/SpeziAccessCode/CodeViews/EnterCodeView.swift b/Sources/SpeziAccessCode/CodeViews/EnterCodeView.swift index 11ec833..f7c70bf 100644 --- a/Sources/SpeziAccessCode/CodeViews/EnterCodeView.swift +++ b/Sources/SpeziAccessCode/CodeViews/EnterCodeView.swift @@ -25,7 +25,7 @@ struct EnterCodeView: View { if let codeOption = viewModel.codeOption { CodeView(codeOption: codeOption) { code in do { - try viewModel.checkAccessCode(code) + try await viewModel.checkAccessCode(code) } catch { wrongCodeCounter += 1 errorMessage = String(localized: "ACCESS_CODE_PASSCODE_ERROR \(wrongCodeCounter)", bundle: .module) diff --git a/Sources/SpeziAccessCode/CodeViews/SetCodeView.swift b/Sources/SpeziAccessCode/CodeViews/SetCodeView.swift index 04e1c97..aade08c 100644 --- a/Sources/SpeziAccessCode/CodeViews/SetCodeView.swift +++ b/Sources/SpeziAccessCode/CodeViews/SetCodeView.swift @@ -23,6 +23,13 @@ struct SetCodeView: View { @State private var errorMessage: String? + private var codeOptions: [CodeOptions] { + CodeOptions.allCases.filter { codeOption in + viewModel.configuration.codeOptions.contains(codeOption) + } + } + + var body: some View { ZStack { Color(uiColor: .systemBackground) @@ -39,10 +46,14 @@ struct SetCodeView: View { state = .repeatCode } } - Menu(selectedCode.description.localizedString()) { - ForEach(CodeOptions.allCases) { codeOption in - Button(codeOption.description.localizedString()) { - selectedCode = codeOption + Group { + if codeOptions.count > 1 { + Menu(selectedCode.description.localizedString()) { + ForEach(codeOptions) { codeOption in + Button(codeOption.description.localizedString()) { + selectedCode = codeOption + } + } } } } @@ -57,7 +68,7 @@ struct SetCodeView: View { CodeView(codeOption: $selectedCode) { code in if code == firstCode { do { - try viewModel.setAccessCode(code, codeOption: selectedCode) + try await viewModel.setAccessCode(code, codeOption: selectedCode) errorMessage = nil withAnimation { state = .success diff --git a/Tests/UITests/TestApp/TestApp.swift b/Tests/UITests/TestApp/TestApp.swift index 1086d8a..326c332 100644 --- a/Tests/UITests/TestApp/TestApp.swift +++ b/Tests/UITests/TestApp/TestApp.swift @@ -20,7 +20,7 @@ struct UITestsApp: App { NavigationStack { List { NavigationLink("Access Guarded") { - AccessGuarded(identifier: "TestIdentifier") { + AccessGuarded(configuration: .code(timeout: 5), identifier: "TestIdentifier") { Color.green .overlay { Text("Secured ...") From 3564e712112c1cffc531686856917de1ea9425d7 Mon Sep 17 00:00:00 2001 From: Paul Schmiedmayer Date: Sun, 10 Sep 2023 01:17:06 -0700 Subject: [PATCH 3/9] Update wording --- .github/workflows/build-and-test.yml | 6 +++--- .spi.yml | 2 +- CITATION.cff | 4 ++-- CONTRIBUTORS.md | 3 +-- Package.swift | 10 +++++----- README.md | 16 ++++++++-------- .../AccessGuard.swift | 0 .../AccessGuardConfiguration.swift | 0 .../AccessGuardError.swift | 0 .../AccessGuardView.swift | 0 .../AccessGuardViewModel.swift | 0 .../AccessGuarded.swift | 0 .../CodeViews/CodeOptions.swift | 0 .../CodeViews/CodeView.swift | 0 .../CodeViews/EnterCodeView.swift | 0 .../CodeViews/ErrorMessageCapsule.swift | 0 .../CodeViews/SetCodeView.swift | 0 .../Extensions/AnyTransition+Navigate.swift | 0 .../Extensions/String+Numeric.swift | 0 .../GuardType.swift | 0 .../Resources/de.lproj/Localizable.strings | 0 .../Resources/en.lproj/Localizable.strings | 0 .../SetAccessGuard.swift | 0 .../SpeziAccessGuardTests.swift} | 6 +++--- Tests/UITests/TestApp.xctestplan | 4 ++-- Tests/UITests/TestApp.xctestplan.license | 2 +- .../AccentColor.colorset/Contents.json.license | 2 +- .../AppIcon.appiconset/Contents.json.license | 2 +- .../Assets.xcassets/Contents.json.license | 2 +- Tests/UITests/TestApp/TestApp.swift | 4 ++-- Tests/UITests/TestApp/TestAppDelegate.swift | 2 +- .../UITests/TestAppUITests/TestAppUITests.swift | 4 ++-- Tests/UITests/TestAppWatchApp.xctestplan.license | 2 +- Tests/UITests/UITests.xcodeproj/project.pbxproj | 14 +++++++------- .../UITests.xcodeproj/project.pbxproj.license | 2 +- .../contents.xcworkspacedata.license | 2 +- .../IDEWorkspaceChecks.plist.license | 2 +- .../xcshareddata/xcschemes/TestApp.xcscheme | 6 +++--- .../xcschemes/TestApp.xcscheme.license | 2 +- .../xcschemes/TestAppWatchApp.xcscheme.license | 2 +- 40 files changed, 50 insertions(+), 51 deletions(-) rename Sources/{SpeziAccessCode => SpeziAccessGuard}/AccessGuard.swift (100%) rename Sources/{SpeziAccessCode => SpeziAccessGuard}/AccessGuardConfiguration.swift (100%) rename Sources/{SpeziAccessCode => SpeziAccessGuard}/AccessGuardError.swift (100%) rename Sources/{SpeziAccessCode => SpeziAccessGuard}/AccessGuardView.swift (100%) rename Sources/{SpeziAccessCode => SpeziAccessGuard}/AccessGuardViewModel.swift (100%) rename Sources/{SpeziAccessCode => SpeziAccessGuard}/AccessGuarded.swift (100%) rename Sources/{SpeziAccessCode => SpeziAccessGuard}/CodeViews/CodeOptions.swift (100%) rename Sources/{SpeziAccessCode => SpeziAccessGuard}/CodeViews/CodeView.swift (100%) rename Sources/{SpeziAccessCode => SpeziAccessGuard}/CodeViews/EnterCodeView.swift (100%) rename Sources/{SpeziAccessCode => SpeziAccessGuard}/CodeViews/ErrorMessageCapsule.swift (100%) rename Sources/{SpeziAccessCode => SpeziAccessGuard}/CodeViews/SetCodeView.swift (100%) rename Sources/{SpeziAccessCode => SpeziAccessGuard}/Extensions/AnyTransition+Navigate.swift (100%) rename Sources/{SpeziAccessCode => SpeziAccessGuard}/Extensions/String+Numeric.swift (100%) rename Sources/{SpeziAccessCode => SpeziAccessGuard}/GuardType.swift (100%) rename Sources/{SpeziAccessCode => SpeziAccessGuard}/Resources/de.lproj/Localizable.strings (100%) rename Sources/{SpeziAccessCode => SpeziAccessGuard}/Resources/en.lproj/Localizable.strings (100%) rename Sources/{SpeziAccessCode => SpeziAccessGuard}/SetAccessGuard.swift (100%) rename Tests/{SpeziAccessCodeTests/SpeziAccessCodeTests.swift => SpeziAccessGuardTests/SpeziAccessGuardTests.swift} (67%) diff --git a/.github/workflows/build-and-test.yml b/.github/workflows/build-and-test.yml index 6a82adc..8e8b144 100644 --- a/.github/workflows/build-and-test.yml +++ b/.github/workflows/build-and-test.yml @@ -21,8 +21,8 @@ jobs: uses: StanfordSpezi/.github/.github/workflows/xcodebuild-or-fastlane.yml@v2 with: runsonlabels: '["macOS", "self-hosted"]' - scheme: SpeziAccessCode - artifactname: SpeziAccessCode.xcresult + scheme: SpeziAccessGuard + artifactname: SpeziAccessGuard.xcresult ios: name: Build and Test iOS uses: StanfordSpezi/.github/.github/workflows/xcodebuild-or-fastlane.yml@v2 @@ -46,4 +46,4 @@ jobs: needs: [packageios, ios, ipados] uses: StanfordSpezi/.github/.github/workflows/create-and-upload-coverage-report.yml@v2 with: - coveragereports: SpeziAccessCode.xcresult TestApp.xcresult TestAppiPadOS.xcresult + coveragereports: SpeziAccessGuard.xcresult TestApp.xcresult TestAppiPadOS.xcresult diff --git a/.spi.yml b/.spi.yml index 2ad9283..3d3cdea 100644 --- a/.spi.yml +++ b/.spi.yml @@ -11,4 +11,4 @@ builder: configs: - platform: ios documentation_targets: - - SpeziAccessCode + - SpeziAccessGuard diff --git a/CITATION.cff b/CITATION.cff index 0931913..ef1cc56 100644 --- a/CITATION.cff +++ b/CITATION.cff @@ -12,5 +12,5 @@ authors: - family-names: "Schmiedmayer" given-names: "Paul" orcid: "https://orcid.org/0000-0002-8607-9148" -title: "SpeziAccessCode" -url: "https://github.com/StanfordSpezi/SpeziAccessCode" +title: "SpeziAccessGuard" +url: "https://github.com/StanfordSpezi/SpeziAccessGuard" diff --git a/CONTRIBUTORS.md b/CONTRIBUTORS.md index 6e41eb3..6dcbf94 100644 --- a/CONTRIBUTORS.md +++ b/CONTRIBUTORS.md @@ -10,8 +10,7 @@ --> -SpeziAccessCode contributors +SpeziAccessGuard contributors ==================== * [Paul Schmiedmayer](https://github.com/PSchmiedmayer) -* [Vishnu Ravi](https://github.com/vishnuravi) diff --git a/Package.swift b/Package.swift index a1725f8..058f4dc 100644 --- a/Package.swift +++ b/Package.swift @@ -12,13 +12,13 @@ import PackageDescription let package = Package( - name: "SpeziAccessCode", + name: "SpeziAccessGuard", defaultLocalization: "en", platforms: [ .iOS(.v16) ], products: [ - .library(name: "SpeziAccessCode", targets: ["SpeziAccessCode"]) + .library(name: "SpeziAccessGuard", targets: ["SpeziAccessGuard"]) ], dependencies: [ .package(url: "https://github.com/StanfordSpezi/Spezi", .upToNextMinor(from: "0.7.2")), @@ -27,7 +27,7 @@ let package = Package( ], targets: [ .target( - name: "SpeziAccessCode", + name: "SpeziAccessGuard", dependencies: [ .product(name: "Spezi", package: "Spezi"), .product(name: "SpeziSecureStorage", package: "SpeziStorage"), @@ -35,9 +35,9 @@ let package = Package( ] ), .testTarget( - name: "SpeziAccessCodeTests", + name: "SpeziAccessGuardTests", dependencies: [ - .target(name: "SpeziAccessCode") + .target(name: "SpeziAccessGuard") ] ) ] diff --git a/README.md b/README.md index c7bef2b..acebe5d 100644 --- a/README.md +++ b/README.md @@ -8,13 +8,13 @@ SPDX-License-Identifier: MIT --> -# SpeziAccessCode +# SpeziAccessGuard -[![Build and Test](https://github.com/StanfordSpezi/SpeziAccessCode/actions/workflows/build-and-test.yml/badge.svg)](https://github.com/StanfordSpezi/SpeziAccessCode/actions/workflows/build-and-test.yml) -[![codecov](https://codecov.io/gh/StanfordSpezi/SpeziAccessCode/branch/main/graph/badge.svg?token=X7BQYSUKOH)](https://codecov.io/gh/StanfordSpezi/SpeziAccessCode) +[![Build and Test](https://github.com/StanfordSpezi/SpeziAccessGuard/actions/workflows/build-and-test.yml/badge.svg)](https://github.com/StanfordSpezi/SpeziAccessGuard/actions/workflows/build-and-test.yml) +[![codecov](https://codecov.io/gh/StanfordSpezi/SpeziAccessGuard/branch/main/graph/badge.svg?token=X7BQYSUKOH)](https://codecov.io/gh/StanfordSpezi/SpeziAccessGuard) [![DOI](https://zenodo.org/badge/573230182.svg)](https://zenodo.org/badge/latestdoi/573230182) -[![](https://img.shields.io/endpoint?url=https%3A%2F%2Fswiftpackageindex.com%2Fapi%2Fpackages%2FStanfordSpezi%2FSpeziAccessCode%2Fbadge%3Ftype%3Dswift-versions)](https://swiftpackageindex.com/StanfordSpezi/SpeziAccessCode) -[![](https://img.shields.io/endpoint?url=https%3A%2F%2Fswiftpackageindex.com%2Fapi%2Fpackages%2FStanfordSpezi%2FSpeziAccessCode%2Fbadge%3Ftype%3Dplatforms)](https://swiftpackageindex.com/StanfordSpezi/SpeziAccessCode) +[![](https://img.shields.io/endpoint?url=https%3A%2F%2Fswiftpackageindex.com%2Fapi%2Fpackages%2FStanfordSpezi%2FSpeziAccessGuard%2Fbadge%3Ftype%3Dswift-versions)](https://swiftpackageindex.com/StanfordSpezi/SpeziAccessGuard) +[![](https://img.shields.io/endpoint?url=https%3A%2F%2Fswiftpackageindex.com%2Fapi%2Fpackages%2FStanfordSpezi%2FSpeziAccessGuard%2Fbadge%3Ftype%3Dplatforms)](https://swiftpackageindex.com/StanfordSpezi/SpeziAccessGuard) ## How To Use This Template @@ -30,7 +30,7 @@ Follow these steps to customize it to your needs: - ... add ensure that the targets are all added in the code coverage settings of your .xctestplan file in the Xcode Project (*Shared Settings > Code Coverage > Code Coverage*). 4. You will either need to add the [CodeCov GitHub App](https://github.com/apps/codecov) or add a codecov.io token to your [GitHub Actions Secrets](https://docs.github.com/en/actions/security-guides/encrypted-secrets#creating-encrypted-secrets-for-an-environment) following the instructions of the [Codecov GitHub Action](https://github.com/marketplace/actions/codecov#usage). The StanfordBDHG organization already has the [CodeCov GitHub App](https://github.com/apps/codecov) installed. If you do not want to cover test coverage data, you can remove the code coverage job in the `build-and-test.yml` GitHub Action. 5. Adjust this README.md to describe your project and adjust the badges at the top to point to the correct GitHub Action of your repository and Codecov badge. -6. The Swift Package template includes a Swift Package Index configuration file to automatically build the package and [host the documentation on the Swift Package Index website](https://blog.swiftpackageindex.com/posts/auto-generating-auto-hosting-and-auto-updating-docc-documentation/). Adjust the `.spi.yml` file to include all targets that you want to build documentation for. You can follow the [instructions of the Swift Package Index](https://swiftpackageindex.com/add-a-package) to include your Swift Package in the Swift Package Index. You can link to the [API documentation](https://swiftpackageindex.com/StanfordSpezi/SpeziAccessCode/documentation) from your README file. +6. The Swift Package template includes a Swift Package Index configuration file to automatically build the package and [host the documentation on the Swift Package Index website](https://blog.swiftpackageindex.com/posts/auto-generating-auto-hosting-and-auto-updating-docc-documentation/). Adjust the `.spi.yml` file to include all targets that you want to build documentation for. You can follow the [instructions of the Swift Package Index](https://swiftpackageindex.com/add-a-package) to include your Swift Package in the Swift Package Index. You can link to the [API documentation](https://swiftpackageindex.com/StanfordSpezi/SpeziAccessGuard/documentation) from your README file. 7. Adjust the CITATION.cff file to amend information about the new Swift Package ([learn more about CITATION files on GitHub](https://docs.github.com/en/repositories/managing-your-repositorys-settings-and-features/customizing-your-repository/about-citation-files)) and [register the Swift Package on Zenodo](https://docs.github.com/en/repositories/archiving-a-github-repository/referencing-and-citing-content). @@ -44,12 +44,12 @@ The project can be added to your Xcode project or Swift Package using the [Swift ## License -This project is licensed under the MIT License. See [Licenses](https://github.com/StanfordSpezi/SpeziAccessCode/tree/main/LICENSES) for more information. +This project is licensed under the MIT License. See [Licenses](https://github.com/StanfordSpezi/SpeziAccessGuard/tree/main/LICENSES) for more information. ## Contributors This project is developed as part of the Stanford Byers Center for Biodesign at Stanford University. -See [CONTRIBUTORS.md](https://github.com/StanfordSpezi/SpeziAccessCode/tree/main/CONTRIBUTORS.md) for a full list of all SpeziAccessCode contributors. +See [CONTRIBUTORS.md](https://github.com/StanfordSpezi/SpeziAccessGuard/tree/main/CONTRIBUTORS.md) for a full list of all SpeziAccessGuard contributors. ![Stanford Byers Center for Biodesign Logo](https://raw.githubusercontent.com/StanfordSpezi/.github/main/assets/biodesign-footer-light.png#gh-light-mode-only) ![Stanford Byers Center for Biodesign Logo](https://raw.githubusercontent.com/StanfordSpezi/.github/main/assets/biodesign-footer-dark.png#gh-dark-mode-only) diff --git a/Sources/SpeziAccessCode/AccessGuard.swift b/Sources/SpeziAccessGuard/AccessGuard.swift similarity index 100% rename from Sources/SpeziAccessCode/AccessGuard.swift rename to Sources/SpeziAccessGuard/AccessGuard.swift diff --git a/Sources/SpeziAccessCode/AccessGuardConfiguration.swift b/Sources/SpeziAccessGuard/AccessGuardConfiguration.swift similarity index 100% rename from Sources/SpeziAccessCode/AccessGuardConfiguration.swift rename to Sources/SpeziAccessGuard/AccessGuardConfiguration.swift diff --git a/Sources/SpeziAccessCode/AccessGuardError.swift b/Sources/SpeziAccessGuard/AccessGuardError.swift similarity index 100% rename from Sources/SpeziAccessCode/AccessGuardError.swift rename to Sources/SpeziAccessGuard/AccessGuardError.swift diff --git a/Sources/SpeziAccessCode/AccessGuardView.swift b/Sources/SpeziAccessGuard/AccessGuardView.swift similarity index 100% rename from Sources/SpeziAccessCode/AccessGuardView.swift rename to Sources/SpeziAccessGuard/AccessGuardView.swift diff --git a/Sources/SpeziAccessCode/AccessGuardViewModel.swift b/Sources/SpeziAccessGuard/AccessGuardViewModel.swift similarity index 100% rename from Sources/SpeziAccessCode/AccessGuardViewModel.swift rename to Sources/SpeziAccessGuard/AccessGuardViewModel.swift diff --git a/Sources/SpeziAccessCode/AccessGuarded.swift b/Sources/SpeziAccessGuard/AccessGuarded.swift similarity index 100% rename from Sources/SpeziAccessCode/AccessGuarded.swift rename to Sources/SpeziAccessGuard/AccessGuarded.swift diff --git a/Sources/SpeziAccessCode/CodeViews/CodeOptions.swift b/Sources/SpeziAccessGuard/CodeViews/CodeOptions.swift similarity index 100% rename from Sources/SpeziAccessCode/CodeViews/CodeOptions.swift rename to Sources/SpeziAccessGuard/CodeViews/CodeOptions.swift diff --git a/Sources/SpeziAccessCode/CodeViews/CodeView.swift b/Sources/SpeziAccessGuard/CodeViews/CodeView.swift similarity index 100% rename from Sources/SpeziAccessCode/CodeViews/CodeView.swift rename to Sources/SpeziAccessGuard/CodeViews/CodeView.swift diff --git a/Sources/SpeziAccessCode/CodeViews/EnterCodeView.swift b/Sources/SpeziAccessGuard/CodeViews/EnterCodeView.swift similarity index 100% rename from Sources/SpeziAccessCode/CodeViews/EnterCodeView.swift rename to Sources/SpeziAccessGuard/CodeViews/EnterCodeView.swift diff --git a/Sources/SpeziAccessCode/CodeViews/ErrorMessageCapsule.swift b/Sources/SpeziAccessGuard/CodeViews/ErrorMessageCapsule.swift similarity index 100% rename from Sources/SpeziAccessCode/CodeViews/ErrorMessageCapsule.swift rename to Sources/SpeziAccessGuard/CodeViews/ErrorMessageCapsule.swift diff --git a/Sources/SpeziAccessCode/CodeViews/SetCodeView.swift b/Sources/SpeziAccessGuard/CodeViews/SetCodeView.swift similarity index 100% rename from Sources/SpeziAccessCode/CodeViews/SetCodeView.swift rename to Sources/SpeziAccessGuard/CodeViews/SetCodeView.swift diff --git a/Sources/SpeziAccessCode/Extensions/AnyTransition+Navigate.swift b/Sources/SpeziAccessGuard/Extensions/AnyTransition+Navigate.swift similarity index 100% rename from Sources/SpeziAccessCode/Extensions/AnyTransition+Navigate.swift rename to Sources/SpeziAccessGuard/Extensions/AnyTransition+Navigate.swift diff --git a/Sources/SpeziAccessCode/Extensions/String+Numeric.swift b/Sources/SpeziAccessGuard/Extensions/String+Numeric.swift similarity index 100% rename from Sources/SpeziAccessCode/Extensions/String+Numeric.swift rename to Sources/SpeziAccessGuard/Extensions/String+Numeric.swift diff --git a/Sources/SpeziAccessCode/GuardType.swift b/Sources/SpeziAccessGuard/GuardType.swift similarity index 100% rename from Sources/SpeziAccessCode/GuardType.swift rename to Sources/SpeziAccessGuard/GuardType.swift diff --git a/Sources/SpeziAccessCode/Resources/de.lproj/Localizable.strings b/Sources/SpeziAccessGuard/Resources/de.lproj/Localizable.strings similarity index 100% rename from Sources/SpeziAccessCode/Resources/de.lproj/Localizable.strings rename to Sources/SpeziAccessGuard/Resources/de.lproj/Localizable.strings diff --git a/Sources/SpeziAccessCode/Resources/en.lproj/Localizable.strings b/Sources/SpeziAccessGuard/Resources/en.lproj/Localizable.strings similarity index 100% rename from Sources/SpeziAccessCode/Resources/en.lproj/Localizable.strings rename to Sources/SpeziAccessGuard/Resources/en.lproj/Localizable.strings diff --git a/Sources/SpeziAccessCode/SetAccessGuard.swift b/Sources/SpeziAccessGuard/SetAccessGuard.swift similarity index 100% rename from Sources/SpeziAccessCode/SetAccessGuard.swift rename to Sources/SpeziAccessGuard/SetAccessGuard.swift diff --git a/Tests/SpeziAccessCodeTests/SpeziAccessCodeTests.swift b/Tests/SpeziAccessGuardTests/SpeziAccessGuardTests.swift similarity index 67% rename from Tests/SpeziAccessCodeTests/SpeziAccessCodeTests.swift rename to Tests/SpeziAccessGuardTests/SpeziAccessGuardTests.swift index a0c927a..bee7af0 100644 --- a/Tests/SpeziAccessCodeTests/SpeziAccessCodeTests.swift +++ b/Tests/SpeziAccessGuardTests/SpeziAccessGuardTests.swift @@ -6,12 +6,12 @@ // SPDX-License-Identifier: MIT // -@testable import SpeziAccessCode +@testable import SpeziAccessGuard import XCTest -final class SpeziAccessCodeTests: XCTestCase { - func testSpeziAccessCode() throws { +final class SpeziAccessGuardTests: XCTestCase { + func testSpeziAccessGuard() throws { XCTAssert(true) } } diff --git a/Tests/UITests/TestApp.xctestplan b/Tests/UITests/TestApp.xctestplan index 767f0ae..801c951 100644 --- a/Tests/UITests/TestApp.xctestplan +++ b/Tests/UITests/TestApp.xctestplan @@ -13,8 +13,8 @@ "targets" : [ { "containerPath" : "container:..\/..", - "identifier" : "SpeziAccessCode", - "name" : "SpeziAccessCode" + "identifier" : "SpeziAccessGuard", + "name" : "SpeziAccessGuard" } ] }, diff --git a/Tests/UITests/TestApp.xctestplan.license b/Tests/UITests/TestApp.xctestplan.license index f385b8b..914f561 100644 --- a/Tests/UITests/TestApp.xctestplan.license +++ b/Tests/UITests/TestApp.xctestplan.license @@ -1,4 +1,4 @@ -This source file is part of the SpeziAccessCode open-source project +This source file is part of the Spezi open source project SPDX-FileCopyrightText: 2022 Stanford University and the project authors (see CONTRIBUTORS.md) diff --git a/Tests/UITests/TestApp/Assets.xcassets/AccentColor.colorset/Contents.json.license b/Tests/UITests/TestApp/Assets.xcassets/AccentColor.colorset/Contents.json.license index f385b8b..914f561 100644 --- a/Tests/UITests/TestApp/Assets.xcassets/AccentColor.colorset/Contents.json.license +++ b/Tests/UITests/TestApp/Assets.xcassets/AccentColor.colorset/Contents.json.license @@ -1,4 +1,4 @@ -This source file is part of the SpeziAccessCode open-source project +This source file is part of the Spezi open source project SPDX-FileCopyrightText: 2022 Stanford University and the project authors (see CONTRIBUTORS.md) diff --git a/Tests/UITests/TestApp/Assets.xcassets/AppIcon.appiconset/Contents.json.license b/Tests/UITests/TestApp/Assets.xcassets/AppIcon.appiconset/Contents.json.license index f385b8b..914f561 100644 --- a/Tests/UITests/TestApp/Assets.xcassets/AppIcon.appiconset/Contents.json.license +++ b/Tests/UITests/TestApp/Assets.xcassets/AppIcon.appiconset/Contents.json.license @@ -1,4 +1,4 @@ -This source file is part of the SpeziAccessCode open-source project +This source file is part of the Spezi open source project SPDX-FileCopyrightText: 2022 Stanford University and the project authors (see CONTRIBUTORS.md) diff --git a/Tests/UITests/TestApp/Assets.xcassets/Contents.json.license b/Tests/UITests/TestApp/Assets.xcassets/Contents.json.license index f385b8b..914f561 100644 --- a/Tests/UITests/TestApp/Assets.xcassets/Contents.json.license +++ b/Tests/UITests/TestApp/Assets.xcassets/Contents.json.license @@ -1,4 +1,4 @@ -This source file is part of the SpeziAccessCode open-source project +This source file is part of the Spezi open source project SPDX-FileCopyrightText: 2022 Stanford University and the project authors (see CONTRIBUTORS.md) diff --git a/Tests/UITests/TestApp/TestApp.swift b/Tests/UITests/TestApp/TestApp.swift index 326c332..e8712c2 100644 --- a/Tests/UITests/TestApp/TestApp.swift +++ b/Tests/UITests/TestApp/TestApp.swift @@ -1,12 +1,12 @@ // -// This source file is part of the SpeziAccessCode open-source project +// This source file is part of the Spezi open source project // // SPDX-FileCopyrightText: 2022 Stanford University and the project authors (see CONTRIBUTORS.md) // // SPDX-License-Identifier: MIT // -import SpeziAccessCode +import SpeziAccessGuard import SwiftUI diff --git a/Tests/UITests/TestApp/TestAppDelegate.swift b/Tests/UITests/TestApp/TestAppDelegate.swift index 399f6c7..f5f8cda 100644 --- a/Tests/UITests/TestApp/TestAppDelegate.swift +++ b/Tests/UITests/TestApp/TestAppDelegate.swift @@ -7,7 +7,7 @@ // import Spezi -import SpeziAccessCode +import SpeziAccessGuard import SwiftUI diff --git a/Tests/UITests/TestAppUITests/TestAppUITests.swift b/Tests/UITests/TestAppUITests/TestAppUITests.swift index 022354a..2181a4d 100644 --- a/Tests/UITests/TestAppUITests/TestAppUITests.swift +++ b/Tests/UITests/TestAppUITests/TestAppUITests.swift @@ -1,5 +1,5 @@ // -// This source file is part of the SpeziAccessCode open-source project +// This source file is part of the Spezi open source project // // SPDX-FileCopyrightText: 2022 Stanford University and the project authors (see CONTRIBUTORS.md) // @@ -17,7 +17,7 @@ class TestAppUITests: XCTestCase { } - func testSpeziAccessCode() throws { + func testSpeziAccessGuard() throws { let app = XCUIApplication() app.launch() XCTAssert(app.staticTexts["Stanford University"].waitForExistence(timeout: 0.1)) diff --git a/Tests/UITests/TestAppWatchApp.xctestplan.license b/Tests/UITests/TestAppWatchApp.xctestplan.license index f385b8b..914f561 100644 --- a/Tests/UITests/TestAppWatchApp.xctestplan.license +++ b/Tests/UITests/TestAppWatchApp.xctestplan.license @@ -1,4 +1,4 @@ -This source file is part of the SpeziAccessCode open-source project +This source file is part of the Spezi open source project SPDX-FileCopyrightText: 2022 Stanford University and the project authors (see CONTRIBUTORS.md) diff --git a/Tests/UITests/UITests.xcodeproj/project.pbxproj b/Tests/UITests/UITests.xcodeproj/project.pbxproj index fd76c86..4a15965 100644 --- a/Tests/UITests/UITests.xcodeproj/project.pbxproj +++ b/Tests/UITests/UITests.xcodeproj/project.pbxproj @@ -7,7 +7,7 @@ objects = { /* Begin PBXBuildFile section */ - 2F68C3C8292EA52000B3E12C /* SpeziAccessCode in Frameworks */ = {isa = PBXBuildFile; productRef = 2F68C3C7292EA52000B3E12C /* SpeziAccessCode */; }; + 2F68C3C8292EA52000B3E12C /* SpeziAccessGuard in Frameworks */ = {isa = PBXBuildFile; productRef = 2F68C3C7292EA52000B3E12C /* SpeziAccessGuard */; }; 2F6D139A28F5F386007C25D6 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 2F6D139928F5F386007C25D6 /* Assets.xcassets */; }; 2F7E29302AAD5E29000458CE /* TestAppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2F7E292F2AAD5E29000458CE /* TestAppDelegate.swift */; }; 2F8A431329130A8C005D2B8F /* TestAppUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2F8A431229130A8C005D2B8F /* TestAppUITests.swift */; }; @@ -38,7 +38,7 @@ /* End PBXCopyFilesBuildPhase section */ /* Begin PBXFileReference section */ - 2F68C3C6292E9F8F00B3E12C /* SpeziAccessCode */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = SpeziAccessCode; path = ../..; sourceTree = ""; }; + 2F68C3C6292E9F8F00B3E12C /* SpeziAccessGuard */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = SpeziAccessGuard; path = ../..; sourceTree = ""; }; 2F6D139228F5F384007C25D6 /* TestApp.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = TestApp.app; sourceTree = BUILT_PRODUCTS_DIR; }; 2F6D139928F5F386007C25D6 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 2F6D13AC28F5F386007C25D6 /* TestAppUITests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = TestAppUITests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; @@ -53,7 +53,7 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( - 2F68C3C8292EA52000B3E12C /* SpeziAccessCode in Frameworks */, + 2F68C3C8292EA52000B3E12C /* SpeziAccessGuard in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -71,7 +71,7 @@ isa = PBXGroup; children = ( 2FB0758A299DDB9000C0B37F /* TestApp.xctestplan */, - 2F68C3C6292E9F8F00B3E12C /* SpeziAccessCode */, + 2F68C3C6292E9F8F00B3E12C /* SpeziAccessGuard */, 2F6D139428F5F384007C25D6 /* TestApp */, 2F6D13AF28F5F386007C25D6 /* TestAppUITests */, 2F6D139328F5F384007C25D6 /* Products */, @@ -131,7 +131,7 @@ ); name = TestApp; packageProductDependencies = ( - 2F68C3C7292EA52000B3E12C /* SpeziAccessCode */, + 2F68C3C7292EA52000B3E12C /* SpeziAccessGuard */, ); productName = Example; productReference = 2F6D139228F5F384007C25D6 /* TestApp.app */; @@ -639,9 +639,9 @@ /* End XCConfigurationList section */ /* Begin XCSwiftPackageProductDependency section */ - 2F68C3C7292EA52000B3E12C /* SpeziAccessCode */ = { + 2F68C3C7292EA52000B3E12C /* SpeziAccessGuard */ = { isa = XCSwiftPackageProductDependency; - productName = SpeziAccessCode; + productName = SpeziAccessGuard; }; /* End XCSwiftPackageProductDependency section */ }; diff --git a/Tests/UITests/UITests.xcodeproj/project.pbxproj.license b/Tests/UITests/UITests.xcodeproj/project.pbxproj.license index f385b8b..914f561 100644 --- a/Tests/UITests/UITests.xcodeproj/project.pbxproj.license +++ b/Tests/UITests/UITests.xcodeproj/project.pbxproj.license @@ -1,4 +1,4 @@ -This source file is part of the SpeziAccessCode open-source project +This source file is part of the Spezi open source project SPDX-FileCopyrightText: 2022 Stanford University and the project authors (see CONTRIBUTORS.md) diff --git a/Tests/UITests/UITests.xcodeproj/project.xcworkspace/contents.xcworkspacedata.license b/Tests/UITests/UITests.xcodeproj/project.xcworkspace/contents.xcworkspacedata.license index f385b8b..914f561 100644 --- a/Tests/UITests/UITests.xcodeproj/project.xcworkspace/contents.xcworkspacedata.license +++ b/Tests/UITests/UITests.xcodeproj/project.xcworkspace/contents.xcworkspacedata.license @@ -1,4 +1,4 @@ -This source file is part of the SpeziAccessCode open-source project +This source file is part of the Spezi open source project SPDX-FileCopyrightText: 2022 Stanford University and the project authors (see CONTRIBUTORS.md) diff --git a/Tests/UITests/UITests.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist.license b/Tests/UITests/UITests.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist.license index f385b8b..914f561 100644 --- a/Tests/UITests/UITests.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist.license +++ b/Tests/UITests/UITests.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist.license @@ -1,4 +1,4 @@ -This source file is part of the SpeziAccessCode open-source project +This source file is part of the Spezi open source project SPDX-FileCopyrightText: 2022 Stanford University and the project authors (see CONTRIBUTORS.md) diff --git a/Tests/UITests/UITests.xcodeproj/xcshareddata/xcschemes/TestApp.xcscheme b/Tests/UITests/UITests.xcodeproj/xcshareddata/xcschemes/TestApp.xcscheme index 6e7043a..cf6bf65 100644 --- a/Tests/UITests/UITests.xcodeproj/xcshareddata/xcschemes/TestApp.xcscheme +++ b/Tests/UITests/UITests.xcodeproj/xcshareddata/xcschemes/TestApp.xcscheme @@ -28,9 +28,9 @@ buildForAnalyzing = "NO"> diff --git a/Tests/UITests/UITests.xcodeproj/xcshareddata/xcschemes/TestApp.xcscheme.license b/Tests/UITests/UITests.xcodeproj/xcshareddata/xcschemes/TestApp.xcscheme.license index f385b8b..914f561 100644 --- a/Tests/UITests/UITests.xcodeproj/xcshareddata/xcschemes/TestApp.xcscheme.license +++ b/Tests/UITests/UITests.xcodeproj/xcshareddata/xcschemes/TestApp.xcscheme.license @@ -1,4 +1,4 @@ -This source file is part of the SpeziAccessCode open-source project +This source file is part of the Spezi open source project SPDX-FileCopyrightText: 2022 Stanford University and the project authors (see CONTRIBUTORS.md) diff --git a/Tests/UITests/UITests.xcodeproj/xcshareddata/xcschemes/TestAppWatchApp.xcscheme.license b/Tests/UITests/UITests.xcodeproj/xcshareddata/xcschemes/TestAppWatchApp.xcscheme.license index f385b8b..914f561 100644 --- a/Tests/UITests/UITests.xcodeproj/xcshareddata/xcschemes/TestAppWatchApp.xcscheme.license +++ b/Tests/UITests/UITests.xcodeproj/xcshareddata/xcschemes/TestAppWatchApp.xcscheme.license @@ -1,4 +1,4 @@ -This source file is part of the SpeziAccessCode open-source project +This source file is part of the Spezi open source project SPDX-FileCopyrightText: 2022 Stanford University and the project authors (see CONTRIBUTORS.md) From c39840162a1470ca721f5ddbd0bed93911f629c1 Mon Sep 17 00:00:00 2001 From: Paul Schmiedmayer Date: Sun, 10 Sep 2023 12:50:35 -0700 Subject: [PATCH 4/9] Update README --- README.md | 4 ---- 1 file changed, 4 deletions(-) diff --git a/README.md b/README.md index acebe5d..47b1a0c 100644 --- a/README.md +++ b/README.md @@ -11,10 +11,6 @@ SPDX-License-Identifier: MIT # SpeziAccessGuard [![Build and Test](https://github.com/StanfordSpezi/SpeziAccessGuard/actions/workflows/build-and-test.yml/badge.svg)](https://github.com/StanfordSpezi/SpeziAccessGuard/actions/workflows/build-and-test.yml) -[![codecov](https://codecov.io/gh/StanfordSpezi/SpeziAccessGuard/branch/main/graph/badge.svg?token=X7BQYSUKOH)](https://codecov.io/gh/StanfordSpezi/SpeziAccessGuard) -[![DOI](https://zenodo.org/badge/573230182.svg)](https://zenodo.org/badge/latestdoi/573230182) -[![](https://img.shields.io/endpoint?url=https%3A%2F%2Fswiftpackageindex.com%2Fapi%2Fpackages%2FStanfordSpezi%2FSpeziAccessGuard%2Fbadge%3Ftype%3Dswift-versions)](https://swiftpackageindex.com/StanfordSpezi/SpeziAccessGuard) -[![](https://img.shields.io/endpoint?url=https%3A%2F%2Fswiftpackageindex.com%2Fapi%2Fpackages%2FStanfordSpezi%2FSpeziAccessGuard%2Fbadge%3Ftype%3Dplatforms)](https://swiftpackageindex.com/StanfordSpezi/SpeziAccessGuard) ## How To Use This Template From bfff4611609e223cfaa26599c068af611aaa19c7 Mon Sep 17 00:00:00 2001 From: Paul Schmiedmayer Date: Sun, 10 Sep 2023 14:52:28 -0700 Subject: [PATCH 5/9] Update functionality --- Sources/SpeziAccessGuard/AccessGuard.swift | 43 +++-- .../AccessGuardConfiguration.swift | 59 ++++--- .../AccessGuardViewModel.swift | 46 +++-- Sources/SpeziAccessGuard/AccessGuarded.swift | 60 +------ .../CodeViews/CodeOptions.swift | 2 +- .../SpeziAccessGuard/CodeViews/CodeView.swift | 4 +- .../CodeViews/EnterCodeView.swift | 3 +- .../CodeViews/SetCodeView.swift | 163 ++++++++++-------- .../Extensions/AnyTransition+Navigate.swift | 20 --- .../Resources/de.lproj/Localizable.strings | 25 ++- .../Resources/en.lproj/Localizable.strings | 21 ++- Sources/SpeziAccessGuard/SetAccessGuard.swift | 21 +-- Tests/UITests/TestApp/ContentView.swift | 50 ++++++ Tests/UITests/TestApp/TestApp.swift | 25 +-- Tests/UITests/TestApp/TestAppDelegate.swift | 7 +- .../TestAppUITests/TestAppUITests.swift | 7 +- .../UITests/UITests.xcodeproj/project.pbxproj | 4 + 17 files changed, 308 insertions(+), 252 deletions(-) delete mode 100644 Sources/SpeziAccessGuard/Extensions/AnyTransition+Navigate.swift create mode 100644 Tests/UITests/TestApp/ContentView.swift diff --git a/Sources/SpeziAccessGuard/AccessGuard.swift b/Sources/SpeziAccessGuard/AccessGuard.swift index d0ab068..7a56385 100644 --- a/Sources/SpeziAccessGuard/AccessGuard.swift +++ b/Sources/SpeziAccessGuard/AccessGuard.swift @@ -39,11 +39,14 @@ public final class AccessGuard: Module { @Dependency var secureStorage: SecureStorage @Published var inTheBackground = true @Published var lastEnteredBackground: Date = .now - var viewModels: [String: AccessGuardViewModel] = [:] + private let configurations: [AccessGuardConfiguration] + private var viewModels: [String: AccessGuardViewModel] = [:] private var cancellables: Set = [] - public init() { } + public init(_ configurations: [AccessGuardConfiguration]) { + self.configurations = configurations + } public func sceneDidEnterBackground(_ scene: UIScene) { @@ -60,19 +63,35 @@ public final class AccessGuard: Module { } - func viewModel(for identifier: String, fixedCode: String? = nil, configuration: AccessGuardConfiguration) -> AccessGuardViewModel { - if let viewModel = viewModels[identifier] { + @MainActor + public func resetAccessCode(for identifier: AccessGuardConfiguration.Identifier) throws { + try viewModel(for: identifier).resetAccessCode() + } + + @MainActor + public func setupComplete(for identifier: AccessGuardConfiguration.Identifier) -> Bool { + viewModel(for: identifier).setup + } + + + @MainActor + func viewModel(for identifier: AccessGuardConfiguration.Identifier) -> AccessGuardViewModel { + guard let configuration = configurations.first(where: { $0.identifier == identifier }) else { + preconditionFailure( + """ + Did not find a AccessGuardConfiguration with the identifier `\(identifier)`. + + Please ensure that you have defined an AccessGuardConfiguration with the identifier in your `AccessGuard` configuration. + """ + ) + } + + guard let viewModel = viewModels[identifier] else { + let viewModel = AccessGuardViewModel(accessGuard: self, secureStorage: secureStorage, configuration: configuration) + viewModels[identifier] = viewModel return viewModel } - let viewModel = AccessGuardViewModel( - identifier, - fixedCode: fixedCode, - accessGuard: self, - secureStorage: secureStorage, - configuration: configuration - ) - viewModels[identifier] = viewModel return viewModel } } diff --git a/Sources/SpeziAccessGuard/AccessGuardConfiguration.swift b/Sources/SpeziAccessGuard/AccessGuardConfiguration.swift index 90f54f1..9a26975 100644 --- a/Sources/SpeziAccessGuard/AccessGuardConfiguration.swift +++ b/Sources/SpeziAccessGuard/AccessGuardConfiguration.swift @@ -12,82 +12,87 @@ import SpeziViews /// Configures the behaviour of the ``AccessGuard`` view. public struct AccessGuardConfiguration { + public typealias Identifier = String + public enum Defaults { public static let codeOptions: CodeOptions = .fourDigitNumeric public static let timeout: TimeInterval = 5 * 60 } + let identifier: Identifier let guardType: GuardType let codeOptions: CodeOptions let timeout: TimeInterval - - - /// Enforce a code if the device is not protected with an access code. - /// - /// > Warning: Not yet implemented - private static var codeIfUnprotected: AccessGuardConfiguration { - codeIfUnprotected() - } - - /// Enforce a code. - public static var code: AccessGuardConfiguration { - code() - } - - /// Enforce a code & biometrics authentication if setup on the device. - /// - /// > Warning: Not yet implemented - private static var biometrics: AccessGuardConfiguration { - biometrics() - } + let fixedCode: String? init( + identifier: Identifier, guardType: GuardType, codeOptions: CodeOptions, - timeout: TimeInterval + timeout: TimeInterval, + fixedCode: String? = nil ) { + self.identifier = identifier self.guardType = guardType self.codeOptions = codeOptions self.timeout = timeout + self.fixedCode = fixedCode } - /// Enforce a code if the device is not protected with an access code. + /// Enforce an access code if the device is not protected with an access code. /// - Parameters: /// - codeOptions: The code options, see ``CodeOptions``. /// - timeout: The timeout when the view should be locked based on the time the scene is not in the foreground. /// /// > Warning: Not yet implemented private static func codeIfUnprotected( + identifier: Identifier, codeOptions: CodeOptions = Defaults.codeOptions, timeout: TimeInterval = Defaults.timeout ) -> AccessGuardConfiguration { - AccessGuardConfiguration(guardType: .codeIfUnprotected, codeOptions: codeOptions, timeout: timeout) + AccessGuardConfiguration(identifier: identifier, guardType: .codeIfUnprotected, codeOptions: codeOptions, timeout: timeout) } - /// Enforce a code. + /// Enforce an access code. /// - Parameters: /// - codeOptions: The code options, see ``CodeOptions``. /// - timeout: The timeout when the view should be locked based on the time the scene is not in the foreground. public static func code( + identifier: Identifier, + codeOptions: CodeOptions = .fourDigitNumeric, + timeout: TimeInterval = Defaults.timeout + ) -> AccessGuardConfiguration { + AccessGuardConfiguration(identifier: identifier, guardType: .code, codeOptions: codeOptions, timeout: timeout) + } + + /// Enforce a fixed access code. + /// - Parameters: + /// - code: The fixed access code. + /// - codeOptions: The code options, see ``CodeOptions``. + /// - timeout: The timeout when the view should be locked based on the time the scene is not in the foreground. + public static func fixed( + identifier: Identifier, + code: String, codeOptions: CodeOptions = .fourDigitNumeric, timeout: TimeInterval = Defaults.timeout ) -> AccessGuardConfiguration { - AccessGuardConfiguration(guardType: .code, codeOptions: codeOptions, timeout: timeout) + AccessGuardConfiguration(identifier: identifier, guardType: .code, codeOptions: codeOptions, timeout: timeout, fixedCode: code) } - /// Enforce a code & biometrics authentication if setup on the device. + /// Enforce an access code & biometrics authentication if setup on the device. /// - Parameters: /// - codeOptions: The code options, see ``CodeOptions``. /// - timeout: The timeout when the view should be locked based on the time the scene is not in the foreground. /// /// > Warning: Not yet implemented private static func biometrics( + identifier: Identifier, codeOptions: CodeOptions = .all, timeout: TimeInterval = Defaults.timeout ) -> AccessGuardConfiguration { - AccessGuardConfiguration(guardType: .biometrics, codeOptions: codeOptions, timeout: timeout) + AccessGuardConfiguration(identifier: identifier, guardType: .biometrics, codeOptions: codeOptions, timeout: timeout) } } diff --git a/Sources/SpeziAccessGuard/AccessGuardViewModel.swift b/Sources/SpeziAccessGuard/AccessGuardViewModel.swift index 5cc14b4..67d6825 100644 --- a/Sources/SpeziAccessGuard/AccessGuardViewModel.swift +++ b/Sources/SpeziAccessGuard/AccessGuardViewModel.swift @@ -22,16 +22,18 @@ final class AccessGuardViewModel: ObservableObject { @MainActor @Published private(set) var locked = true let configuration: AccessGuardConfiguration - private var identifier: String - private var fixedCode: String? private var accessCode: AccessCode? private weak var accessGuard: AccessGuard? private let secureStorage: SecureStorage private var cancellables: Set = [] + var setup: Bool { + accessCode != nil || configuration.fixedCode != nil + } + var codeOption: CodeOptions? { - if fixedCode != nil { + if configuration.fixedCode != nil { return configuration.codeOptions } else { return accessCode?.codeOption @@ -39,14 +41,13 @@ final class AccessGuardViewModel: ObservableObject { } - init(_ identifier: String, fixedCode: String? = nil, accessGuard: AccessGuard, secureStorage: SecureStorage, configuration: AccessGuardConfiguration) { + @MainActor + init(accessGuard: AccessGuard, secureStorage: SecureStorage, configuration: AccessGuardConfiguration) { self.configuration = configuration self.accessGuard = accessGuard self.secureStorage = secureStorage - self.identifier = identifier - self.fixedCode = fixedCode - if let credentials = try? secureStorage.retrieveCredentials(identifier), + if let credentials = try? secureStorage.retrieveCredentials(configuration.identifier), let accessCode = try? JSONDecoder().decode(AccessCode.self, from: Data(credentials.password.utf8)), accessCode.codeOption.verifyStructore(ofCode: accessCode.code) { self.accessCode = accessCode @@ -54,29 +55,44 @@ final class AccessGuardViewModel: ObservableObject { self.accessCode = nil } + self.locked = setup + accessGuard.objectWillChange .sink { - self.updateState() + self.lockAfterInactivity() self.objectWillChange.send() } .store(in: &cancellables) } - private func updateState() { + private func lockAfterInactivity() { Task { @MainActor in - if let lastEnteredBackground = accessGuard?.lastEnteredBackground, lastEnteredBackground.addingTimeInterval(configuration.timeout) >= .now { - locked = false - } else { + if let lastEnteredBackground = accessGuard?.lastEnteredBackground, + lastEnteredBackground.addingTimeInterval(configuration.timeout) < .now { locked = true } } } + @MainActor + func resetAccessCode() throws { + guard configuration.fixedCode == nil else { + return + } + + do { + try secureStorage.deleteCredentials(configuration.identifier) + accessCode = nil + self.locked = setup + } catch { + print("Error resetting access code: \(error)") + } + } func checkAccessCode(_ code: String) async throws { try await MainActor.run { - if let fixedCode, code == fixedCode { + if let fixedCode = configuration.fixedCode, code == fixedCode { locked = false return } @@ -90,7 +106,7 @@ final class AccessGuardViewModel: ObservableObject { } func setAccessCode(_ code: String, codeOption: CodeOptions) async throws { - guard fixedCode == nil else { + guard configuration.fixedCode == nil else { throw AccessGuardError.storeCodeError } @@ -100,7 +116,7 @@ final class AccessGuardViewModel: ObservableObject { throw AccessGuardError.storeCodeError } - try secureStorage.store(credentials: Credentials(username: identifier, password: accessCodeData)) + try secureStorage.store(credentials: Credentials(username: configuration.identifier, password: accessCodeData)) await MainActor.run { locked = true diff --git a/Sources/SpeziAccessGuard/AccessGuarded.swift b/Sources/SpeziAccessGuard/AccessGuarded.swift index 8cd4262..e393f4d 100644 --- a/Sources/SpeziAccessGuard/AccessGuarded.swift +++ b/Sources/SpeziAccessGuard/AccessGuarded.swift @@ -13,79 +13,33 @@ import SwiftUI /// A view that guards the access to a view. /// /// ```swift -/// AccessGuarded { -/// Text("Secured View") -/// } -/// ``` -/// -/// The view can be configured using the ``AccessGuardConfiguration``, the default configuration enables a code or biometrics authentication. -/// ```swift -/// AccessGuarded(.code) { +/// AccessGuarded(identifier: "MyAccessGuardIdentifier") { /// Text("Secured View") /// } /// ``` public struct AccessGuarded: View { @EnvironmentObject private var accessGuard: AccessGuard - private let configuration: AccessGuardConfiguration - private let identifier: String - private let fixedCode: String? + private let identifier: AccessGuardConfiguration.Identifier private let guardedView: GuardedView public var body: some View { AccessGuardView( - viewModel: accessGuard.viewModel( - for: identifier, - fixedCode: fixedCode, - configuration: configuration - ), + viewModel: accessGuard.viewModel(for: identifier), guardedView: guardedView ) } /// - Parameters: - /// - configuration: The access code configuration that defines the behaviour of the view. See ``AccessGuardConfiguration`` for more information. - /// - identifier: The identifier of the credentials that should be used to guard this view. + /// - identifier: The identifier of the access guard configuration that should be used to guard this view. /// - guarded: The guarded view. public init( - configuration: AccessGuardConfiguration = .code, - identifier: String? = nil, + _ identifier: AccessGuardConfiguration.Identifier, @ViewBuilder guarded guardedView: () -> GuardedView ) { - self.configuration = configuration - self.identifier = identifier ?? String(describing: GuardedView.self) - self.fixedCode = nil - self.guardedView = guardedView() - } - - /// Create a ``AccessGuarded`` view that is protected by a fixed code. - /// - /// We generally advise not to use fixed codes, applications should use ``init(configuration:identifier:guarded:)``. - /// - Parameters: - /// - codeOption: The code options, see ``CodeOptions``. - /// - timeout: The timeout when the view should be locked based on the time the scene is not in the foreground. - /// - fixedCode: The identifier of the credentials that should be used to guard this view. - /// - guarded: The guarded view. - public init( - codeOption: CodeOptions = AccessGuardConfiguration.Defaults.codeOptions, - timeout: TimeInterval = AccessGuardConfiguration.Defaults.timeout, - fixedCode: String, - @ViewBuilder guarded guardedView: () -> GuardedView - ) { - precondition( - codeOption.verifyStructore(ofCode: fixedCode), - "The provided fixed code \"\(fixedCode)\" must conform to the \(codeOption.description.localizedString()) code option." - ) - - self.configuration = AccessGuardConfiguration( - guardType: .code, - codeOptions: codeOption, - timeout: timeout - ) - self.identifier = String(describing: GuardedView.self) - self.fixedCode = fixedCode + self.identifier = identifier self.guardedView = guardedView() } } @@ -93,7 +47,7 @@ public struct AccessGuarded: View { struct AccessCodeGuard_Previews: PreviewProvider { static var previews: some View { - AccessGuarded { + AccessGuarded("MyGuardedView") { Text("Guarded View") } } diff --git a/Sources/SpeziAccessGuard/CodeViews/CodeOptions.swift b/Sources/SpeziAccessGuard/CodeViews/CodeOptions.swift index 74fadc4..7044b05 100644 --- a/Sources/SpeziAccessGuard/CodeViews/CodeOptions.swift +++ b/Sources/SpeziAccessGuard/CodeViews/CodeOptions.swift @@ -74,7 +74,7 @@ public struct CodeOptions: OptionSet, Codable, CaseIterable, Identifiable { case .customAlphanumeric: return LocalizedStringResource("CODE_OPTIONS_CUSTOM_ALPHANUMERIC_DIGIT", bundle: .atURL(from: .module)) default: - return LocalizedStringResource("CODE_OPTIONS_DEFAULT_DIGIT", bundle: .atURL(from: .module)) + return LocalizedStringResource("CODE_OPTIONS_UNKNOWN", bundle: .atURL(from: .module)) } } diff --git a/Sources/SpeziAccessGuard/CodeViews/CodeView.swift b/Sources/SpeziAccessGuard/CodeViews/CodeView.swift index c4b99f6..8fa0567 100644 --- a/Sources/SpeziAccessGuard/CodeViews/CodeView.swift +++ b/Sources/SpeziAccessGuard/CodeViews/CodeView.swift @@ -57,7 +57,7 @@ struct CodeView: View { focused = true } } - .onAppear { + .task { focused = true oldKeyBoardType = codeOption.keyBoardType } @@ -98,6 +98,7 @@ struct CodeView: View { private func checkCode() async { focused = false viewState = .processing + do { try await action(code) } catch { @@ -106,7 +107,6 @@ struct CodeView: View { } viewState = .idle - try? await Task.sleep(for: .seconds(0.05)) focused = true } diff --git a/Sources/SpeziAccessGuard/CodeViews/EnterCodeView.swift b/Sources/SpeziAccessGuard/CodeViews/EnterCodeView.swift index f7c70bf..5134b6f 100644 --- a/Sources/SpeziAccessGuard/CodeViews/EnterCodeView.swift +++ b/Sources/SpeziAccessGuard/CodeViews/EnterCodeView.swift @@ -28,7 +28,8 @@ struct EnterCodeView: View { try await viewModel.checkAccessCode(code) } catch { wrongCodeCounter += 1 - errorMessage = String(localized: "ACCESS_CODE_PASSCODE_ERROR \(wrongCodeCounter)", bundle: .module) + let errorMessageTemplate = NSLocalizedString("ACCESS_CODE_PASSCODE_ERROR %@", bundle: .module, comment: "") + errorMessage = String(format: errorMessageTemplate, "\(wrongCodeCounter)") throw error } } diff --git a/Sources/SpeziAccessGuard/CodeViews/SetCodeView.swift b/Sources/SpeziAccessGuard/CodeViews/SetCodeView.swift index aade08c..377c725 100644 --- a/Sources/SpeziAccessGuard/CodeViews/SetCodeView.swift +++ b/Sources/SpeziAccessGuard/CodeViews/SetCodeView.swift @@ -11,15 +11,18 @@ import SwiftUI struct SetCodeView: View { enum SetCodeState { + case oldCode case setCode case repeatCode case success } + let action: @MainActor () async -> Void + @ObservedObject var viewModel: AccessGuardViewModel @State private var selectedCode: CodeOptions = .fourDigitNumeric @State private var firstCode: String = "" - @State private var state: SetCodeState = .setCode + @State private var state: SetCodeState = .oldCode @State private var errorMessage: String? @@ -31,87 +34,105 @@ struct SetCodeView: View { var body: some View { - ZStack { - Color(uiColor: .systemBackground) - VStack(spacing: 32) { - switch state { - case .setCode: - Group { - Text("SET_PASSCODE_PROMPT", bundle: .module) - .font(.title2) - .frame(maxWidth: .infinity) - CodeView(codeOption: $selectedCode) { code in - firstCode = code - withAnimation { - state = .repeatCode - } - } - Group { - if codeOptions.count > 1 { - Menu(selectedCode.description.localizedString()) { - ForEach(codeOptions) { codeOption in - Button(codeOption.description.localizedString()) { - selectedCode = codeOption - } - } - } - } + switch state { + case .oldCode: + EnterCodeView(viewModel: viewModel) + .onChange(of: viewModel.locked) { locked in + if !locked { + withAnimation { + state = .setCode } - .frame(height: 60) } - .transition(.navigate) - case .repeatCode: - Group { - Text("SET_PASSCODE_REPEAT_PROMPT", bundle: .module) - .font(.title2) - .frame(maxWidth: .infinity) - CodeView(codeOption: $selectedCode) { code in - if code == firstCode { - do { - try await viewModel.setAccessCode(code, codeOption: selectedCode) - errorMessage = nil - withAnimation { - state = .success - } - } catch let error as AccessGuardError { - errorMessage = error.failureReason + } + .task { + if !viewModel.setup { + state = .setCode + } + } + .transition(.opacity) + case .setCode: + VStack(spacing: 32) { + Text("SET_PASSCODE_PROMPT", bundle: .module) + .font(.title2) + .frame(maxWidth: .infinity) + CodeView(codeOption: $selectedCode) { code in + firstCode = code + withAnimation { + state = .repeatCode + } + } + VStack { + if codeOptions.count > 1 { + Spacer() + Menu(selectedCode.description.localizedString()) { + ForEach(codeOptions) { codeOption in + Button(codeOption.description.localizedString()) { + selectedCode = codeOption } - } else { - errorMessage = String(localized: "SET_PASSCODE_REPEAT_NOT_EQUAL", bundle: .module) } } - ErrorMessageCapsule(errorMessage: $errorMessage) - .frame(height: 60) + } else { + Rectangle() + .foregroundStyle(.clear) } - .transition(.navigate) - case .success: - Image(systemName: "checkmark.circle.fill") - .resizable() - .aspectRatio(1.0, contentMode: .fit) - .frame(height: 100) - .foregroundStyle(.green) - .transition(.navigate) } + .frame(height: 80) } - .toolbar { - if state == .repeatCode { - ToolbarItem(placement: .cancellationAction) { - Button( - action: { - withAnimation { - state = .setCode - firstCode = "" - errorMessage = nil - } - }, label: { - Text("SET_PASSCODE_BACK_BUTTON", bundle: .module) - } - ) + .transition(.opacity) + case .repeatCode: + VStack(spacing: 32) { + Text("SET_PASSCODE_REPEAT_PROMPT", bundle: .module) + .font(.title2) + .frame(maxWidth: .infinity) + CodeView(codeOption: $selectedCode) { code in + if code == firstCode { + do { + try await viewModel.setAccessCode(code, codeOption: selectedCode) + errorMessage = nil + await action() + try await Task.sleep(for: .seconds(0.2)) + withAnimation { + state = .success + } + } catch let error as AccessGuardError { + errorMessage = error.failureReason } + } else { + errorMessage = String(localized: "SET_PASSCODE_REPEAT_NOT_EQUAL", bundle: .module) } } - .navigationBarBackButtonHidden(state == .repeatCode) - .padding(.horizontal) + VStack { + ErrorMessageCapsule(errorMessage: $errorMessage) + Button( + action: { + withAnimation { + state = .setCode + firstCode = "" + errorMessage = nil + } + }, label: { + Text("SET_PASSCODE_BACK_BUTTON", bundle: .module) + } + ) + } + .frame(height: 80) + } + .transition(.opacity) + .navigationBarBackButtonHidden() + case .success: + Image(systemName: "checkmark.circle.fill") + .resizable() + .aspectRatio(1.0, contentMode: .fit) + .frame(height: 100) + .foregroundStyle(.green) + .transition(.slide) + .accessibilityHidden(true) } } + + + init(viewModel: AccessGuardViewModel, action: @MainActor @escaping () async -> Void) { + self.viewModel = viewModel + self.action = action + } } diff --git a/Sources/SpeziAccessGuard/Extensions/AnyTransition+Navigate.swift b/Sources/SpeziAccessGuard/Extensions/AnyTransition+Navigate.swift deleted file mode 100644 index 64a206c..0000000 --- a/Sources/SpeziAccessGuard/Extensions/AnyTransition+Navigate.swift +++ /dev/null @@ -1,20 +0,0 @@ -// -// This source file is part of the Spezi open source project -// -// SPDX-FileCopyrightText: 2022 Stanford University and the project authors (see CONTRIBUTORS.md) -// -// SPDX-License-Identifier: MIT -// - - -import SwiftUI - - -extension AnyTransition { - static var navigate: AnyTransition { - AnyTransition.asymmetric( - insertion: .move(edge: .trailing), - removal: .move(edge: .leading) - ) - } -} diff --git a/Sources/SpeziAccessGuard/Resources/de.lproj/Localizable.strings b/Sources/SpeziAccessGuard/Resources/de.lproj/Localizable.strings index 4c122a0..deb4231 100644 --- a/Sources/SpeziAccessGuard/Resources/de.lproj/Localizable.strings +++ b/Sources/SpeziAccessGuard/Resources/de.lproj/Localizable.strings @@ -6,11 +6,24 @@ // SPDX-License-Identifier: MIT // -// MARK: - General Views -"ACCESS_GUARD_DONE" = "Fertig"; -// MARK: - Passcode -"ACCESS_CODE_PASSCODE_PROMPT" = "Bitte Passwort eingeben"; +// MARK: - Code-Optionen +"CODE_OPTIONS_FOUR_DIGIT" = "Vier Ziffern"; +"CODE_OPTIONS_SIX_DIGIT" = "Sechs Ziffern"; +"CODE_OPTIONS_CUSTOM_NUMERIC_DIGIT" = "Benutzerdefinierte Nummer"; +"CODE_OPTIONS_CUSTOM_ALPHANUMERIC_DIGIT" = "Benutzerdefiniert Alphanumerisch"; +"CODE_OPTIONS_UNKNOWN" = "Unbekannte Option"; -// MARK: - Biometrics -"ACCESS_CODE_BIOMETRICS_PROMPT" = "Bitte Authentifizieren"; +// MARK: - Passwort +"ACCESS_CODE_PASSCODE_PROMPT" = "Bitte geben Sie Ihren Code ein"; +"SET_PASSCODE_BACK_BUTTON" = "Zurücksetzen"; +"SET_PASSCODE_REPEAT_NOT_EQUAL" = "Codes stimmen nicht überein"; +"SET_PASSCODE_REPEAT_PROMPT" = "Bitte wiederholen Sie Ihren neuen Code"; +"SET_PASSCODE_PROMPT" = "Bitte geben Sie Ihren neuen Code ein"; +"ACCESS_CODE_PASSCODE_PROMPT" = "Bitte geben Sie Ihren Code ein"; +"ACCESS_CODE_PASSCODE_ERROR %@" = "%@ falsche Versuche"; + +// MARK: - Fehler +"ACCESS_GUARD_ERROR_TITLE" = "Code-Fehler"; +"ACCESS_GUARD_ERROR_WRONG_PASSCODE_REASON" = "Falscher Code"; +"ACCESS_GUARD_ERROR_STORE_CODE_ERROR_REASON" = "Der Code konnte nicht gespeichert werden."; diff --git a/Sources/SpeziAccessGuard/Resources/en.lproj/Localizable.strings b/Sources/SpeziAccessGuard/Resources/en.lproj/Localizable.strings index c11b060..8fe7a4f 100644 --- a/Sources/SpeziAccessGuard/Resources/en.lproj/Localizable.strings +++ b/Sources/SpeziAccessGuard/Resources/en.lproj/Localizable.strings @@ -6,11 +6,24 @@ // SPDX-License-Identifier: MIT // -// MARK: - General Views -"ACCESS_GUARD_DONE" = "Done"; + +// MARK: - Code Options +"CODE_OPTIONS_FOUR_DIGIT" = "Four Digits"; +"CODE_OPTIONS_SIX_DIGIT" = "Six Digits"; +"CODE_OPTIONS_CUSTOM_NUMERIC_DIGIT" = "Custom Numeric"; +"CODE_OPTIONS_CUSTOM_ALPHANUMERIC_DIGIT" = "Custom Alphanumeric"; +"CODE_OPTIONS_UNKNOWN" = "Unknown Option"; // MARK: - Passcode "ACCESS_CODE_PASSCODE_PROMPT" = "Please enter your passcode"; +"SET_PASSCODE_BACK_BUTTON" = "Reset"; +"SET_PASSCODE_REPEAT_NOT_EQUAL" = "Passcodes not equal"; +"SET_PASSCODE_REPEAT_PROMPT" = "Please repeat your new passcode"; +"SET_PASSCODE_PROMPT" = "Please enter your new passcode"; +"ACCESS_CODE_PASSCODE_PROMPT" = "Please enter your passcode"; +"ACCESS_CODE_PASSCODE_ERROR %@" = "%@ wrong tries"; -// MARK: - Biometrics -"ACCESS_CODE_BIOMETRICS_PROMPT %@" = "Please authenticate"; +// MARK: - Errors +"ACCESS_GUARD_ERROR_TITLE" = "Passcode Error"; +"ACCESS_GUARD_ERROR_WRONG_PASSCODE_REASON" = "Wrong Passcode"; +"ACCESS_GUARD_ERROR_STORE_CODE_ERROR_REASON" = "Could not store the passcode."; diff --git a/Sources/SpeziAccessGuard/SetAccessGuard.swift b/Sources/SpeziAccessGuard/SetAccessGuard.swift index 65a9650..0f00d65 100644 --- a/Sources/SpeziAccessGuard/SetAccessGuard.swift +++ b/Sources/SpeziAccessGuard/SetAccessGuard.swift @@ -13,28 +13,23 @@ import SwiftUI public struct SetAccessGuard: View { @EnvironmentObject private var accessGuard: AccessGuard - private let configuration: AccessGuardConfiguration - private let identifier: String + private let identifier: AccessGuardConfiguration.Identifier + private let action: @MainActor () async -> Void public var body: some View { - SetCodeView( - viewModel: accessGuard.viewModel( - for: identifier, - configuration: configuration - ) - ) + SetCodeView(viewModel: accessGuard.viewModel(for: identifier), action: action) } /// - Parameters: - /// - configuration: The access code configuration that defines the behaviour of the view. See ``AccessGuardConfiguration`` for more information. - /// - identifier: The identifier of the credentials that should be used to guard this view. + /// - identifier: The identifier of the access guard configuration that should be used to guard this view. + /// - action: An action that should be performed once the password has been set. public init( - configuration: AccessGuardConfiguration = .code, - identifier: String + identifier: AccessGuardConfiguration.Identifier, + action: (@MainActor () async -> Void)? = nil ) { - self.configuration = configuration self.identifier = identifier + self.action = action ?? {} } } diff --git a/Tests/UITests/TestApp/ContentView.swift b/Tests/UITests/TestApp/ContentView.swift new file mode 100644 index 0000000..b385d62 --- /dev/null +++ b/Tests/UITests/TestApp/ContentView.swift @@ -0,0 +1,50 @@ +// +// This source file is part of the Spezi open source project +// +// SPDX-FileCopyrightText: 2022 Stanford University and the project authors (see CONTRIBUTORS.md) +// +// SPDX-License-Identifier: MIT +// + +import SpeziAccessGuard +import SwiftUI + + +struct ContentView: View { + @Environment(\.dismiss) private var dismiss + @EnvironmentObject private var accessGuard: AccessGuard + + + var body: some View { + NavigationStack { + List { + NavigationLink("Access Guarded") { + AccessGuarded("TestIdentifier") { + Color.green + .overlay { + Text("Secured ...") + } + } + } + NavigationLink("Access Guarded Fixed") { + AccessGuarded("TestFixedIdentifier") { + Color.green + .overlay { + Text("Secured with fixed code ...") + } + } + } + NavigationLink("Set Code") { + SetAccessGuard(identifier: "TestIdentifier") + } + } + .toolbar { + ToolbarItem { + Button("Reset Access Guard") { + try? accessGuard.resetAccessCode(for: "TestIdentifier") + } + } + } + } + } +} diff --git a/Tests/UITests/TestApp/TestApp.swift b/Tests/UITests/TestApp/TestApp.swift index e8712c2..628f9ad 100644 --- a/Tests/UITests/TestApp/TestApp.swift +++ b/Tests/UITests/TestApp/TestApp.swift @@ -6,7 +6,6 @@ // SPDX-License-Identifier: MIT // -import SpeziAccessGuard import SwiftUI @@ -17,29 +16,7 @@ struct UITestsApp: App { var body: some Scene { WindowGroup { - NavigationStack { - List { - NavigationLink("Access Guarded") { - AccessGuarded(configuration: .code(timeout: 5), identifier: "TestIdentifier") { - Color.green - .overlay { - Text("Secured ...") - } - } - } - NavigationLink("Access Guarded Fixed") { - AccessGuarded(fixedCode: "1234") { - Color.green - .overlay { - Text("Secured with fixed code ...") - } - } - } - NavigationLink("Set Code") { - SetAccessGuard(identifier: "TestIdentifier") - } - } - } + ContentView() .spezi(appDelegate) } } diff --git a/Tests/UITests/TestApp/TestAppDelegate.swift b/Tests/UITests/TestApp/TestAppDelegate.swift index f5f8cda..2a62d85 100644 --- a/Tests/UITests/TestApp/TestAppDelegate.swift +++ b/Tests/UITests/TestApp/TestAppDelegate.swift @@ -14,7 +14,12 @@ import SwiftUI class TestAppDelegate: SpeziAppDelegate { override var configuration: Configuration { Configuration { - AccessGuard() + AccessGuard( + [ + .code(identifier: "TestIdentifier"), + .fixed(identifier: "TestFixedIdentifier", code: "1234") + ] + ) } } } diff --git a/Tests/UITests/TestAppUITests/TestAppUITests.swift b/Tests/UITests/TestAppUITests/TestAppUITests.swift index 2181a4d..b7761cf 100644 --- a/Tests/UITests/TestAppUITests/TestAppUITests.swift +++ b/Tests/UITests/TestAppUITests/TestAppUITests.swift @@ -17,9 +17,12 @@ class TestAppUITests: XCTestCase { } - func testSpeziAccessGuard() throws { + func testFixedCode() throws { let app = XCUIApplication() app.launch() - XCTAssert(app.staticTexts["Stanford University"].waitForExistence(timeout: 0.1)) + + app.buttons["Access Guarded Fixed"].tap() + + } } diff --git a/Tests/UITests/UITests.xcodeproj/project.pbxproj b/Tests/UITests/UITests.xcodeproj/project.pbxproj index 4a15965..803bd57 100644 --- a/Tests/UITests/UITests.xcodeproj/project.pbxproj +++ b/Tests/UITests/UITests.xcodeproj/project.pbxproj @@ -10,6 +10,7 @@ 2F68C3C8292EA52000B3E12C /* SpeziAccessGuard in Frameworks */ = {isa = PBXBuildFile; productRef = 2F68C3C7292EA52000B3E12C /* SpeziAccessGuard */; }; 2F6D139A28F5F386007C25D6 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 2F6D139928F5F386007C25D6 /* Assets.xcassets */; }; 2F7E29302AAD5E29000458CE /* TestAppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2F7E292F2AAD5E29000458CE /* TestAppDelegate.swift */; }; + 2F7E29322AAE61DB000458CE /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2F7E29312AAE61DB000458CE /* ContentView.swift */; }; 2F8A431329130A8C005D2B8F /* TestAppUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2F8A431229130A8C005D2B8F /* TestAppUITests.swift */; }; 2FA7382C290ADFAA007ACEB9 /* TestApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2FA7382B290ADFAA007ACEB9 /* TestApp.swift */; }; /* End PBXBuildFile section */ @@ -43,6 +44,7 @@ 2F6D139928F5F386007C25D6 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 2F6D13AC28F5F386007C25D6 /* TestAppUITests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = TestAppUITests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 2F7E292F2AAD5E29000458CE /* TestAppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TestAppDelegate.swift; sourceTree = ""; }; + 2F7E29312AAE61DB000458CE /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = ""; }; 2F8A431229130A8C005D2B8F /* TestAppUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TestAppUITests.swift; sourceTree = ""; }; 2FA7382B290ADFAA007ACEB9 /* TestApp.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TestApp.swift; sourceTree = ""; }; 2FB0758A299DDB9000C0B37F /* TestApp.xctestplan */ = {isa = PBXFileReference; lastKnownFileType = text; path = TestApp.xctestplan; sourceTree = ""; }; @@ -92,6 +94,7 @@ isa = PBXGroup; children = ( 2FA7382B290ADFAA007ACEB9 /* TestApp.swift */, + 2F7E29312AAE61DB000458CE /* ContentView.swift */, 2F7E292F2AAD5E29000458CE /* TestAppDelegate.swift */, 2F6D139928F5F386007C25D6 /* Assets.xcassets */, ); @@ -218,6 +221,7 @@ files = ( 2F7E29302AAD5E29000458CE /* TestAppDelegate.swift in Sources */, 2FA7382C290ADFAA007ACEB9 /* TestApp.swift in Sources */, + 2F7E29322AAE61DB000458CE /* ContentView.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; From bdd4c8d3ab1a3695efb30a1a9c18000991966a96 Mon Sep 17 00:00:00 2001 From: Paul Schmiedmayer Date: Sun, 10 Sep 2023 15:13:31 -0700 Subject: [PATCH 6/9] Add Tests --- .../SpeziAccessGuard/CodeViews/CodeView.swift | 1 + .../CodeViews/SetCodeView.swift | 2 +- .../Resources/de.lproj/Localizable.strings | 2 + .../Resources/en.lproj/Localizable.strings | 2 + Tests/UITests/TestApp/TestAppDelegate.swift | 2 +- .../TestAppUITests/TestAppUITests.swift | 100 ++++++++++++++++++ 6 files changed, 107 insertions(+), 2 deletions(-) diff --git a/Sources/SpeziAccessGuard/CodeViews/CodeView.swift b/Sources/SpeziAccessGuard/CodeViews/CodeView.swift index 8fa0567..73b26a2 100644 --- a/Sources/SpeziAccessGuard/CodeViews/CodeView.swift +++ b/Sources/SpeziAccessGuard/CodeViews/CodeView.swift @@ -27,6 +27,7 @@ struct CodeView: View { .textFieldStyle(.roundedBorder) .focused($focused) .disabled(viewState == .processing) + .accessibilityLabel(Text("PASSCODE_FIELD", bundle: .module)) .overlay { ZStack { if codeOption.maxLength == 4 { diff --git a/Sources/SpeziAccessGuard/CodeViews/SetCodeView.swift b/Sources/SpeziAccessGuard/CodeViews/SetCodeView.swift index 377c725..94b6ed6 100644 --- a/Sources/SpeziAccessGuard/CodeViews/SetCodeView.swift +++ b/Sources/SpeziAccessGuard/CodeViews/SetCodeView.swift @@ -126,7 +126,7 @@ struct SetCodeView: View { .frame(height: 100) .foregroundStyle(.green) .transition(.slide) - .accessibilityHidden(true) + .accessibilityLabel(Text("PASSCODE_SET_SUCCESS", bundle: .module)) } } diff --git a/Sources/SpeziAccessGuard/Resources/de.lproj/Localizable.strings b/Sources/SpeziAccessGuard/Resources/de.lproj/Localizable.strings index deb4231..50f2021 100644 --- a/Sources/SpeziAccessGuard/Resources/de.lproj/Localizable.strings +++ b/Sources/SpeziAccessGuard/Resources/de.lproj/Localizable.strings @@ -15,11 +15,13 @@ "CODE_OPTIONS_UNKNOWN" = "Unbekannte Option"; // MARK: - Passwort +"PASSCODE_FIELD" = "Passwort Textfeld"; "ACCESS_CODE_PASSCODE_PROMPT" = "Bitte geben Sie Ihren Code ein"; "SET_PASSCODE_BACK_BUTTON" = "Zurücksetzen"; "SET_PASSCODE_REPEAT_NOT_EQUAL" = "Codes stimmen nicht überein"; "SET_PASSCODE_REPEAT_PROMPT" = "Bitte wiederholen Sie Ihren neuen Code"; "SET_PASSCODE_PROMPT" = "Bitte geben Sie Ihren neuen Code ein"; +"PASSCODE_SET_SUCCESS" = "Neuen Code erfolgreich gespeichert"; "ACCESS_CODE_PASSCODE_PROMPT" = "Bitte geben Sie Ihren Code ein"; "ACCESS_CODE_PASSCODE_ERROR %@" = "%@ falsche Versuche"; diff --git a/Sources/SpeziAccessGuard/Resources/en.lproj/Localizable.strings b/Sources/SpeziAccessGuard/Resources/en.lproj/Localizable.strings index 8fe7a4f..c9a13c1 100644 --- a/Sources/SpeziAccessGuard/Resources/en.lproj/Localizable.strings +++ b/Sources/SpeziAccessGuard/Resources/en.lproj/Localizable.strings @@ -15,11 +15,13 @@ "CODE_OPTIONS_UNKNOWN" = "Unknown Option"; // MARK: - Passcode +"PASSCODE_FIELD" = "Passcode Field"; "ACCESS_CODE_PASSCODE_PROMPT" = "Please enter your passcode"; "SET_PASSCODE_BACK_BUTTON" = "Reset"; "SET_PASSCODE_REPEAT_NOT_EQUAL" = "Passcodes not equal"; "SET_PASSCODE_REPEAT_PROMPT" = "Please repeat your new passcode"; "SET_PASSCODE_PROMPT" = "Please enter your new passcode"; +"PASSCODE_SET_SUCCESS" = "Passcode set was successful"; "ACCESS_CODE_PASSCODE_PROMPT" = "Please enter your passcode"; "ACCESS_CODE_PASSCODE_ERROR %@" = "%@ wrong tries"; diff --git a/Tests/UITests/TestApp/TestAppDelegate.swift b/Tests/UITests/TestApp/TestAppDelegate.swift index 2a62d85..1fa19a3 100644 --- a/Tests/UITests/TestApp/TestAppDelegate.swift +++ b/Tests/UITests/TestApp/TestAppDelegate.swift @@ -16,7 +16,7 @@ class TestAppDelegate: SpeziAppDelegate { Configuration { AccessGuard( [ - .code(identifier: "TestIdentifier"), + .code(identifier: "TestIdentifier", timeout: 10), .fixed(identifier: "TestFixedIdentifier", code: "1234") ] ) diff --git a/Tests/UITests/TestAppUITests/TestAppUITests.swift b/Tests/UITests/TestAppUITests/TestAppUITests.swift index b7761cf..399b0dd 100644 --- a/Tests/UITests/TestAppUITests/TestAppUITests.swift +++ b/Tests/UITests/TestAppUITests/TestAppUITests.swift @@ -22,7 +22,107 @@ class TestAppUITests: XCTestCase { app.launch() app.buttons["Access Guarded Fixed"].tap() + app.secureTextFields["Passcode Field"].tap() + app.secureTextFields["Passcode Field"].typeText("1234") + XCTAssert(app.staticTexts["Secured with fixed code ..."].waitForExistence(timeout: 2.0)) + XCTAssert(app.staticTexts["Secured with fixed code ..."].isHittable) + } + + + func testAccessCode() throws { + let app = XCUIApplication() + app.launch() + + app.buttons["Reset Access Guard"].tap() + + app.buttons["Access Guarded"].tap() + + XCTAssert(app.staticTexts["Secured ..."].waitForExistence(timeout: 2.0)) + XCTAssert(app.staticTexts["Secured ..."].isHittable) + + app.buttons["Back"].tap() + + + XCTAssert(app.buttons["Set Code"].waitForExistence(timeout: 2.0)) + app.buttons["Set Code"].tap() + + // Set first passcode + XCTAssert(app.staticTexts["Please enter your new passcode"].waitForExistence(timeout: 2.0)) + XCTAssert(app.secureTextFields["Passcode Field"].waitForExistence(timeout: 2.0)) + app.secureTextFields["Passcode Field"].tap() + app.secureTextFields["Passcode Field"].typeText("1111") + + // Go back once + XCTAssert(app.staticTexts["Please repeat your new passcode"].waitForExistence(timeout: 2.0)) + XCTAssert(app.buttons["Reset"].waitForExistence(timeout: 2.0)) + app.buttons["Reset"].tap() + + // Enter new first passcode + XCTAssert(app.staticTexts["Please enter your new passcode"].waitForExistence(timeout: 2.0)) + XCTAssert(app.secureTextFields["Passcode Field"].waitForExistence(timeout: 2.0)) + app.secureTextFields["Passcode Field"].tap() + app.secureTextFields["Passcode Field"].typeText("1112") + + // Enter a wrong repeat passcode + XCTAssert(app.staticTexts["Please repeat your new passcode"].waitForExistence(timeout: 2.0)) + XCTAssert(app.secureTextFields["Passcode Field"].waitForExistence(timeout: 2.0)) + app.secureTextFields["Passcode Field"].tap() + app.secureTextFields["Passcode Field"].typeText("1113") + + // Enter correct repeat passcode + XCTAssert(app.staticTexts["Passcodes not equal"].waitForExistence(timeout: 2.0)) + XCTAssert(app.secureTextFields["Passcode Field"].waitForExistence(timeout: 2.0)) + app.secureTextFields["Passcode Field"].tap() + app.secureTextFields["Passcode Field"].typeText("1112") + + // Success + XCTAssert(app.images["Passcode set was successful"].waitForExistence(timeout: 2.0)) + app.buttons["Back"].tap() + + // Try the new passcode + XCTAssert(app.buttons["Access Guarded"].waitForExistence(timeout: 2.0)) + app.buttons["Access Guarded"].tap() + + XCTAssert(app.secureTextFields["Passcode Field"].waitForExistence(timeout: 2.0)) + app.secureTextFields["Passcode Field"].tap() + app.secureTextFields["Passcode Field"].typeText("1112") + + XCTAssert(app.staticTexts["Secured ..."].waitForExistence(timeout: 2.0)) + XCTAssert(app.staticTexts["Secured ..."].isHittable) + + // Go to the home screen and see if the view is still visable in less than 10 seconds. + let springboard = XCUIApplication(bundleIdentifier: "com.apple.springboard") + springboard.activate() + XCTAssert(springboard.wait(for: .runningForeground, timeout: 2)) + app.activate() + + XCTAssert(app.staticTexts["Secured ..."].waitForExistence(timeout: 2.0)) + XCTAssert(app.staticTexts["Secured ..."].isHittable) + + // Try with a time longer than the timeout: + springboard.activate() + XCTAssert(springboard.wait(for: .runningForeground, timeout: 2)) + + sleep(11) + + app.activate() + XCTAssert(app.secureTextFields["Passcode Field"].waitForExistence(timeout: 2.0)) + app.secureTextFields["Passcode Field"].tap() + app.secureTextFields["Passcode Field"].typeText("1112") + + XCTAssert(app.staticTexts["Secured ..."].waitForExistence(timeout: 2.0)) + XCTAssert(app.staticTexts["Secured ..."].isHittable) + + // Go Back to the main view: + app.buttons["Back"].tap() + + // Check that the passcode is removed if it is no longer set. + app.buttons["Reset Access Guard"].tap() + + app.buttons["Access Guarded"].tap() + XCTAssert(app.staticTexts["Secured ..."].waitForExistence(timeout: 2.0)) + XCTAssert(app.staticTexts["Secured ..."].isHittable) } } From 39a3209d5f0063e0368e10ebcbe4cd293cb484db Mon Sep 17 00:00:00 2001 From: Paul Schmiedmayer Date: Sun, 10 Sep 2023 15:16:09 -0700 Subject: [PATCH 7/9] Update SwiftLint --- .../AccessGuardConfiguration.swift | 6 ++++- .../AccessGuardViewModel.swift | 2 +- .../TestAppUITests/TestAppUITests.swift | 2 +- .../UITests/UITests.xcodeproj/project.pbxproj | 22 +++++++++++++++++++ 4 files changed, 29 insertions(+), 3 deletions(-) diff --git a/Sources/SpeziAccessGuard/AccessGuardConfiguration.swift b/Sources/SpeziAccessGuard/AccessGuardConfiguration.swift index 9a26975..0e7db4f 100644 --- a/Sources/SpeziAccessGuard/AccessGuardConfiguration.swift +++ b/Sources/SpeziAccessGuard/AccessGuardConfiguration.swift @@ -12,10 +12,14 @@ import SpeziViews /// Configures the behaviour of the ``AccessGuard`` view. public struct AccessGuardConfiguration { - public typealias Identifier = String + /// Unique identifier for the ``AccessGuardConfiguration`` + public typealias Identifier = String + /// The defaults for the ``AccessGuardConfiguration`` public enum Defaults { + /// Default code option, subject to change in the future. public static let codeOptions: CodeOptions = .fourDigitNumeric + /// Default timeout option, subject to change in the future. public static let timeout: TimeInterval = 5 * 60 } diff --git a/Sources/SpeziAccessGuard/AccessGuardViewModel.swift b/Sources/SpeziAccessGuard/AccessGuardViewModel.swift index 67d6825..0c39f08 100644 --- a/Sources/SpeziAccessGuard/AccessGuardViewModel.swift +++ b/Sources/SpeziAccessGuard/AccessGuardViewModel.swift @@ -68,7 +68,7 @@ final class AccessGuardViewModel: ObservableObject { private func lockAfterInactivity() { Task { @MainActor in - if let lastEnteredBackground = accessGuard?.lastEnteredBackground, + if let lastEnteredBackground = accessGuard?.lastEnteredBackground, lastEnteredBackground.addingTimeInterval(configuration.timeout) < .now { locked = true } diff --git a/Tests/UITests/TestAppUITests/TestAppUITests.swift b/Tests/UITests/TestAppUITests/TestAppUITests.swift index 399b0dd..18f7fbb 100644 --- a/Tests/UITests/TestAppUITests/TestAppUITests.swift +++ b/Tests/UITests/TestAppUITests/TestAppUITests.swift @@ -30,7 +30,7 @@ class TestAppUITests: XCTestCase { } - func testAccessCode() throws { + func testAccessCode() throws { // swiftlint:disable:this function_body_length let app = XCUIApplication() app.launch() diff --git a/Tests/UITests/UITests.xcodeproj/project.pbxproj b/Tests/UITests/UITests.xcodeproj/project.pbxproj index 803bd57..d0b4d1a 100644 --- a/Tests/UITests/UITests.xcodeproj/project.pbxproj +++ b/Tests/UITests/UITests.xcodeproj/project.pbxproj @@ -127,6 +127,7 @@ 2F6D138F28F5F384007C25D6 /* Frameworks */, 2F6D139028F5F384007C25D6 /* Resources */, 2F9CBECE2A76C412009818FF /* Embed Watch Content */, + 2F7F7DB12AAE772F007C5EC6 /* ShellScript */, ); buildRules = ( ); @@ -214,6 +215,27 @@ }; /* End PBXResourcesBuildPhase section */ +/* Begin PBXShellScriptBuildPhase section */ + 2F7F7DB12AAE772F007C5EC6 /* ShellScript */ = { + isa = PBXShellScriptBuildPhase; + alwaysOutOfDate = 1; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + ); + outputFileListPaths = ( + ); + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "if [ \"${CONFIGURATION}\" = \"Debug\" ]; then\n export PATH=\"$PATH:/opt/homebrew/bin\"\n if which swiftlint > /dev/null; then\n cd ../../ && swiftlint\n else\n echo \"warning: SwiftLint not installed, download from https://github.com/realm/SwiftLint\"\n fi\nfi\n"; + }; +/* End PBXShellScriptBuildPhase section */ + /* Begin PBXSourcesBuildPhase section */ 2F6D138E28F5F384007C25D6 /* Sources */ = { isa = PBXSourcesBuildPhase; From 8fb5c16cb78dc2ba7fedcbd54ea247feedbebec6 Mon Sep 17 00:00:00 2001 From: Paul Schmiedmayer Date: Sun, 10 Sep 2023 15:22:23 -0700 Subject: [PATCH 8/9] Fix Build errors --- Sources/SpeziAccessGuard/AccessGuardError.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Sources/SpeziAccessGuard/AccessGuardError.swift b/Sources/SpeziAccessGuard/AccessGuardError.swift index a1b9888..af7e4ab 100644 --- a/Sources/SpeziAccessGuard/AccessGuardError.swift +++ b/Sources/SpeziAccessGuard/AccessGuardError.swift @@ -21,9 +21,9 @@ enum AccessGuardError: LocalizedError { var failureReason: String { switch self { case .wrongPasscode: - String(localized: "ACCESS_GUARD_ERROR_WRONG_PASSCODE_REASON", bundle: .module) + return String(localized: "ACCESS_GUARD_ERROR_WRONG_PASSCODE_REASON", bundle: .module) case .storeCodeError: - String(localized: "ACCESS_GUARD_ERROR_STORE_CODE_ERROR_REASON", bundle: .module) + return String(localized: "ACCESS_GUARD_ERROR_STORE_CODE_ERROR_REASON", bundle: .module) } } } From 8ff2e2daca6a8d98e9fb91aa9ddf75f1ff587627 Mon Sep 17 00:00:00 2001 From: Paul Schmiedmayer Date: Sun, 10 Sep 2023 15:37:00 -0700 Subject: [PATCH 9/9] Improve the documentation --- Sources/SpeziAccessGuard/AccessGuard.swift | 6 +++++- Sources/SpeziAccessGuard/AccessGuarded.swift | 7 ++++++- Sources/SpeziAccessGuard/CodeViews/SetCodeView.swift | 7 ++++++- .../Resources/de.lproj/Localizable.strings | 1 + .../Resources/en.lproj/Localizable.strings | 1 + Sources/SpeziAccessGuard/SetAccessGuard.swift | 1 + 6 files changed, 20 insertions(+), 3 deletions(-) diff --git a/Sources/SpeziAccessGuard/AccessGuard.swift b/Sources/SpeziAccessGuard/AccessGuard.swift index 7a56385..4507a34 100644 --- a/Sources/SpeziAccessGuard/AccessGuard.swift +++ b/Sources/SpeziAccessGuard/AccessGuard.swift @@ -22,7 +22,11 @@ import SwiftUI /// class ExampleAppDelegate: SpeziAppDelegate { /// override var configuration: Configuration { /// Configuration { -/// AccessGuard() +/// AccessGuard( +/// [ +/// .code(identifier: "TestIdentifier") +/// ] +/// ) /// // ... /// } /// } diff --git a/Sources/SpeziAccessGuard/AccessGuarded.swift b/Sources/SpeziAccessGuard/AccessGuarded.swift index e393f4d..6945e07 100644 --- a/Sources/SpeziAccessGuard/AccessGuarded.swift +++ b/Sources/SpeziAccessGuard/AccessGuarded.swift @@ -12,11 +12,16 @@ import SwiftUI /// A view that guards the access to a view. /// +/// > Important: You will need to register the ``AccessGuard`` module in your Spezi using the [`configuration`](https://swiftpackageindex.com/stanfordspezi/spezi/documentation/spezi/speziappdelegate/configuration) +/// in a [`SpeziAppDelegate`](https://swiftpackageindex.com/stanfordspezi/spezi/documentation/spezi/speziappdelegate) as detailed in the ``AccessGuard`` documentation. +/// /// ```swift -/// AccessGuarded(identifier: "MyAccessGuardIdentifier") { +/// AccessGuarded(identifier: "TestIdentifier") { /// Text("Secured View") /// } /// ``` +/// +/// > Tip: You can allow a user to set the passcode using the ``SetAccessGuard`` view. public struct AccessGuarded: View { @EnvironmentObject private var accessGuard: AccessGuard diff --git a/Sources/SpeziAccessGuard/CodeViews/SetCodeView.swift b/Sources/SpeziAccessGuard/CodeViews/SetCodeView.swift index 94b6ed6..29dd933 100644 --- a/Sources/SpeziAccessGuard/CodeViews/SetCodeView.swift +++ b/Sources/SpeziAccessGuard/CodeViews/SetCodeView.swift @@ -56,14 +56,19 @@ struct SetCodeView: View { .font(.title2) .frame(maxWidth: .infinity) CodeView(codeOption: $selectedCode) { code in + guard selectedCode.verifyStructore(ofCode: code) else { + errorMessage = String(localized: "PASSCODE_NOT_ACCORDING_TO_FORMAT", bundle: .module) + return + } + firstCode = code withAnimation { state = .repeatCode } } VStack { + ErrorMessageCapsule(errorMessage: $errorMessage) if codeOptions.count > 1 { - Spacer() Menu(selectedCode.description.localizedString()) { ForEach(codeOptions) { codeOption in Button(codeOption.description.localizedString()) { diff --git a/Sources/SpeziAccessGuard/Resources/de.lproj/Localizable.strings b/Sources/SpeziAccessGuard/Resources/de.lproj/Localizable.strings index 50f2021..a8d4518 100644 --- a/Sources/SpeziAccessGuard/Resources/de.lproj/Localizable.strings +++ b/Sources/SpeziAccessGuard/Resources/de.lproj/Localizable.strings @@ -21,6 +21,7 @@ "SET_PASSCODE_REPEAT_NOT_EQUAL" = "Codes stimmen nicht überein"; "SET_PASSCODE_REPEAT_PROMPT" = "Bitte wiederholen Sie Ihren neuen Code"; "SET_PASSCODE_PROMPT" = "Bitte geben Sie Ihren neuen Code ein"; +"PASSCODE_NOT_ACCORDING_TO_FORMAT" = "Der Code ist nicht korrekt formatiert"; "PASSCODE_SET_SUCCESS" = "Neuen Code erfolgreich gespeichert"; "ACCESS_CODE_PASSCODE_PROMPT" = "Bitte geben Sie Ihren Code ein"; "ACCESS_CODE_PASSCODE_ERROR %@" = "%@ falsche Versuche"; diff --git a/Sources/SpeziAccessGuard/Resources/en.lproj/Localizable.strings b/Sources/SpeziAccessGuard/Resources/en.lproj/Localizable.strings index c9a13c1..6211bf5 100644 --- a/Sources/SpeziAccessGuard/Resources/en.lproj/Localizable.strings +++ b/Sources/SpeziAccessGuard/Resources/en.lproj/Localizable.strings @@ -21,6 +21,7 @@ "SET_PASSCODE_REPEAT_NOT_EQUAL" = "Passcodes not equal"; "SET_PASSCODE_REPEAT_PROMPT" = "Please repeat your new passcode"; "SET_PASSCODE_PROMPT" = "Please enter your new passcode"; +"PASSCODE_NOT_ACCORDING_TO_FORMAT" = "The code is not conforming to the format"; "PASSCODE_SET_SUCCESS" = "Passcode set was successful"; "ACCESS_CODE_PASSCODE_PROMPT" = "Please enter your passcode"; "ACCESS_CODE_PASSCODE_ERROR %@" = "%@ wrong tries"; diff --git a/Sources/SpeziAccessGuard/SetAccessGuard.swift b/Sources/SpeziAccessGuard/SetAccessGuard.swift index 0f00d65..72ffecc 100644 --- a/Sources/SpeziAccessGuard/SetAccessGuard.swift +++ b/Sources/SpeziAccessGuard/SetAccessGuard.swift @@ -10,6 +10,7 @@ import SpeziSecureStorage import SwiftUI +/// Allows a user to set a code for a ``AccessGuarded`` view. public struct SetAccessGuard: View { @EnvironmentObject private var accessGuard: AccessGuard