From 5cea28576f12f0d35cb04f0bbc76e3cd4436e228 Mon Sep 17 00:00:00 2001 From: Marino Faggiana Date: Thu, 22 Oct 2026 16:05:45 +0200 Subject: [PATCH 01/14] feat: add shared SwiftUI passcode component Support passcode authentication, creation, changes and removal with biometrics and shared lockout protection. Keep the existing passcode flow active until callers are migrated. Signed-off-by: Marino Faggiana --- AGENTS.md | 1 + Nextcloud.xcodeproj/project.pbxproj | 20 ++ .../en.lproj/Localizable.strings | 3 + .../Utility/Passcode/NCPasscodeModel.swift | 227 ++++++++++++++++++ .../Utility/Passcode/NCPasscodeView.swift | 182 ++++++++++++++ 5 files changed, 433 insertions(+) create mode 100644 iOSClient/Utility/Passcode/NCPasscodeModel.swift create mode 100644 iOSClient/Utility/Passcode/NCPasscodeView.swift diff --git a/AGENTS.md b/AGENTS.md index d842b1a70b..0ab6360ac2 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -26,6 +26,7 @@ Other platforms like Android are irrelevant for this project. |-----------------|-----------------------------------------------------|----------------------| | `.github` | GitHub CI workflows (build & test, SwiftLint, extension-target builds) and issue templates. | Keep all three checks green: `xcode.yml` (builds the `Nextcloud` scheme and runs its tests against a Nextcloud server provisioned in CI), `lint.yml` (SwiftLint), `additional-targets.yml` (build-checks the extension schemes). CI skips draft PRs. | | `iOSClient` | Main iOS client source code. | — | +| `iOSClient/Utility/Passcode` | Shared SwiftUI passcode component, developed alongside the existing TOPasscode implementation. | Keep the existing passcode flow active until callers are migrated. | | `iOSClient/Supporting Files` | Translation files synced with Transifex (see `iOSClient/.tx/config`). | Only add new strings to `en.lproj` (`Localizable.strings`, plurals in `Localizable.stringsdict`); never edit the other `*.lproj` dirs — Transifex overwrites them. | | `Brand` | Branding layer: `NCBrand.swift` (app constants, colors), `Database.swift` (Realm database name and schema version), Info.plists and entitlements for all targets, launch screen and intro screens. | Bump `databaseSchemaVersion` in `Brand/Database.swift` when changing Realm models. | | `File Provider Extension` | Contains business logic for the iOS File Provider extension. | — | diff --git a/Nextcloud.xcodeproj/project.pbxproj b/Nextcloud.xcodeproj/project.pbxproj index cbbab6073d..3037743842 100644 --- a/Nextcloud.xcodeproj/project.pbxproj +++ b/Nextcloud.xcodeproj/project.pbxproj @@ -1114,6 +1114,10 @@ F7FF2CB12842159500EBB7A1 /* NCSectionHeader.xib in Resources */ = {isa = PBXBuildFile; fileRef = F7FF2CB02842159500EBB7A1 /* NCSectionHeader.xib */; }; F7FFFCA02FB300440015441E /* NCAssistantSharedTextStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = F7FFFC9D2FB300440015441E /* NCAssistantSharedTextStore.swift */; }; F7FFFCA22FB300600015441E /* NCAssistantSharedTextStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = F7FFFC9D2FB300440015441E /* NCAssistantSharedTextStore.swift */; }; + 9F0E3B428AB44A6480190C2D /* NCPasscodeModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 131F8E2F43AD414E9DB53019 /* NCPasscodeModel.swift */; }; + 87768C6AB8E040219DF85898 /* NCPasscodeModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 131F8E2F43AD414E9DB53019 /* NCPasscodeModel.swift */; }; + 7670DABED44E4D56AD99E6DC /* NCPasscodeView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F902B24E44354B6D9F329F12 /* NCPasscodeView.swift */; }; + 9310075861174A62B1C9FED6 /* NCPasscodeView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F902B24E44354B6D9F329F12 /* NCPasscodeView.swift */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -2161,6 +2165,8 @@ F7FDFF592E437E55000D7688 /* NCAccount.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NCAccount.swift; sourceTree = ""; }; F7FF2CB02842159500EBB7A1 /* NCSectionHeader.xib */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.xib; path = NCSectionHeader.xib; sourceTree = ""; }; F7FFFC9D2FB300440015441E /* NCAssistantSharedTextStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NCAssistantSharedTextStore.swift; sourceTree = ""; }; + 131F8E2F43AD414E9DB53019 /* NCPasscodeModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NCPasscodeModel.swift; sourceTree = ""; }; + F902B24E44354B6D9F329F12 /* NCPasscodeView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NCPasscodeView.swift; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFileSystemSynchronizedRootGroup section */ @@ -3617,6 +3623,7 @@ F7BFFA991A24D7BB0044ED85 /* Utility */ = { isa = PBXGroup; children = ( + B6DE60601BB3410D92EB99EC /* Passcode */, F39170A82CB8201B006127BC /* FileAutoRenamer+Extensions.swift */, F33918C32C7CD8F2002D9AA1 /* FileNameValidator+Extensions.swift */, F752BA042E58C05200616A26 /* Maintenance.swift */, @@ -4045,6 +4052,15 @@ path = Account; sourceTree = ""; }; + B6DE60601BB3410D92EB99EC /* Passcode */ = { + isa = PBXGroup; + children = ( + 131F8E2F43AD414E9DB53019 /* NCPasscodeModel.swift */, + F902B24E44354B6D9F329F12 /* NCPasscodeView.swift */, + ); + path = Passcode; + sourceTree = ""; + }; /* End PBXGroup section */ /* Begin PBXNativeTarget section */ @@ -5059,6 +5075,8 @@ F768823A2C0DD230001CF441 /* NCPreferences.swift in Sources */, A1E2EE010000000000000004 /* NCEndToEndKeySet.swift in Sources */, F737DA9E2B7B893C0063BAFC /* NCPasscode.swift in Sources */, + 87768C6AB8E040219DF85898 /* NCPasscodeModel.swift in Sources */, + 9310075861174A62B1C9FED6 /* NCPasscodeView.swift in Sources */, F7C30DF7291BC0D30017149B /* NCNetworkingE2EEUpload.swift in Sources */, F7C687EC2D22BDE5004757BC /* NCManageDatabase+RecommendedFiles.swift in Sources */, F33918C72C7CD8F2002D9AA1 /* FileNameValidator+Extensions.swift in Sources */, @@ -5573,6 +5591,8 @@ F768822A2C0DD1E7001CF441 /* NCSettingsModel.swift in Sources */, F7BCE48E301091EA00876F4D /* NCMediaDatePickerViewController.swift in Sources */, F737DA9D2B7B893C0063BAFC /* NCPasscode.swift in Sources */, + 9F0E3B428AB44A6480190C2D /* NCPasscodeModel.swift in Sources */, + 7670DABED44E4D56AD99E6DC /* NCPasscodeView.swift in Sources */, F77C97392953131000FDDD09 /* NCCameraRoll.swift in Sources */, F7CADEFD2EA159210057849E /* NCMetadataUploadTranfersSuccess.swift in Sources */, F343A4B32A1E01FF00DDA874 /* PHAsset+Extension.swift in Sources */, diff --git a/iOSClient/Supporting Files/en.lproj/Localizable.strings b/iOSClient/Supporting Files/en.lproj/Localizable.strings index 4823b05018..6dd675d09c 100644 --- a/iOSClient/Supporting Files/en.lproj/Localizable.strings +++ b/iOSClient/Supporting Files/en.lproj/Localizable.strings @@ -879,3 +879,6 @@ "_seek_backward_10_" = "Go back 10 seconds"; "_seek_forward_10_" = "Go forward 10 seconds"; + +"_passcode_incorrect_" = "Incorrect passcode. Try again."; +"_passcode_digits_entered_" = "Passcode digits entered"; diff --git a/iOSClient/Utility/Passcode/NCPasscodeModel.swift b/iOSClient/Utility/Passcode/NCPasscodeModel.swift new file mode 100644 index 0000000000..a6ab62163a --- /dev/null +++ b/iOSClient/Utility/Passcode/NCPasscodeModel.swift @@ -0,0 +1,227 @@ +// SPDX-FileCopyrightText: Nextcloud GmbH +// SPDX-FileCopyrightText: 2026 Marino Faggiana +// SPDX-License-Identifier: GPL-3.0-or-later + +import SwiftUI +import LocalAuthentication + +/// Owns one passcode operation; persistent protection is shared through Keychain. +@MainActor +final class NCPasscodeModel: ObservableObject { + enum Mode { + case authenticate, create, change, disable + } + + enum Result: Equatable { + case authenticated, configured, disabled, cancelled, reset + } + + private enum Step { + case authenticate, enterNew, confirmNew + } + + let mode: NCPasscodeModel.Mode + let allowCancel: Bool + @Published private(set) var digits = "" + @Published private(set) var message = "" + @Published private(set) var lockoutEnd: Date? + @Published private(set) var isAuthenticating = false + @Published private(set) var result: NCPasscodeModel.Result? + @Published private var step: Step + var onCompletion: (@MainActor (NCPasscodeModel.Result) -> Void)? + + private var isPreview = false + private var newPasscode = "" + private var biometricContext: LAContext? + private var biometricRequest = UUID() + private var didStart = false + private let preferences = NCPreferences() + + init(mode: NCPasscodeModel.Mode, allowCancel: Bool) { + self.mode = mode + self.allowCancel = allowCancel + self.step = mode == .create ? .enterNew : .authenticate + } + +#if DEBUG + /// Preview data never reads or changes stored credentials or starts biometrics. + static func preview(mode: NCPasscodeModel.Mode = .authenticate, locked: Bool = false) -> NCPasscodeModel { + let model = NCPasscodeModel(mode: mode, allowCancel: mode != .authenticate) + model.isPreview = true + model.digits = locked ? "" : "123" + model.lockoutEnd = locked ? Date().addingTimeInterval(60) : nil + return model + } +#endif + + var title: String { + switch step { + case .authenticate: return "Enter Passcode" + case .enterNew: return "Enter a new passcode" + case .confirmNew: return "Confirm new passcode" + } + } + + var biometricSymbol: String? { + if isPreview { return step == .authenticate ? "faceid" : nil } + guard step == .authenticate, preferences.touchFaceID, preferences.passcode != nil else { return nil } + let context = LAContext() + guard context.canEvaluatePolicy(.deviceOwnerAuthenticationWithBiometrics, error: nil) else { return nil } + return context.biometryType == .faceID ? "faceid" : "touchid" + } + + var canEnterCode: Bool { + result == nil && lockoutEnd == nil && !isAuthenticating + } + + func start() async { + guard !isPreview else { return } + refreshProtection() + guard !didStart else { return } + didStart = true + await authenticateWithBiometrics() + } + + /// Re-read shared counters on foreground entry and while the lockout is visible. + func refreshProtection() { + guard !isPreview else { return } + guard result == nil else { return } + if let deadline = preferences.passcodeLockoutEnd, deadline <= Date() { + preferences.clearPasscodeFailures() + } + if preferences.resetAppCounterFail && preferences.passcodeCounterFailReset >= NCBrandOptions.shared.resetAppPasscodeAttempts { + finish(.reset) + return + } + if preferences.passcodeCounterFail >= 3 && preferences.passcodeLockoutEnd == nil { + preferences.passcodeLockoutEnd = Date().addingTimeInterval(TimeInterval(NCBrandOptions.shared.passcodeSecondsFail)) + } + lockoutEnd = preferences.passcodeLockoutEnd + if lockoutEnd != nil { + digits = "" + cancelBiometrics() + } + } + + func append(_ digit: Int) { + refreshProtection() + guard canEnterCode, (0...9).contains(digit), digits.count < 6 else { return } + message = "" + digits += String(digit) + if digits.count == 6 && !isPreview { + submit() + } + } + + func deleteDigit() { + guard canEnterCode, !digits.isEmpty else { return } + digits.removeLast() + } + + func cancel() { + guard allowCancel else { return } + finish(.cancelled) + } + + func cancelBiometrics() { + biometricRequest = UUID() + biometricContext?.invalidate() + biometricContext = nil + isAuthenticating = false + } + + func authenticateWithBiometrics() async { + guard !isPreview else { return } + refreshProtection() + guard canEnterCode, biometricSymbol != nil else { return } + let context = LAContext() + let request = UUID() + biometricRequest = request + biometricContext = context + isAuthenticating = true + defer { + if biometricRequest == request { + biometricContext = nil + isAuthenticating = false + } + } + do { + let success = try await context.evaluatePolicy(.deviceOwnerAuthenticationWithBiometrics, + localizedReason: NCBrandOptions.shared.brand) + guard biometricRequest == request, result == nil else { return } + refreshProtectionAfterBiometrics(success: success) + } catch { + guard biometricRequest == request, result == nil else { return } + // Cancellation and choosing the keypad are not failed authentication attempts. + if (error as? LAError)?.code == .authenticationFailed { + registerFailure() + } + } + } + + private func refreshProtectionAfterBiometrics(success: Bool) { + refreshProtection() + guard result == nil, lockoutEnd == nil else { return } + if success { + authenticated() + } + } + + private func submit() { + let code = digits + digits = "" + switch step { + case .authenticate: + guard let saved = preferences.passcode, code == saved else { + registerFailure() + return + } + authenticated() + case .enterNew: + newPasscode = code + step = .confirmNew + case .confirmNew: + guard code == newPasscode else { + newPasscode = "" + step = .enterNew + message = "Passcodes didn't match. Try again." + return + } + preferences.passcode = code + preferences.clearPasscodeFailures() + finish(.configured) + } + } + + private func authenticated() { + preferences.clearPasscodeFailures() + switch mode { + case .change: + step = .enterNew + case .disable: + guard !NCBrandOptions.shared.enforce_passcode_lock else { return } + preferences.passcode = nil + finish(.disabled) + case .authenticate: + finish(.authenticated) + case .create: + break + } + } + + private func registerFailure() { + preferences.passcodeCounterFail += 1 + preferences.passcodeCounterFailReset += 1 + message = "_passcode_incorrect_" + refreshProtection() + } + + private func finish(_ result: NCPasscodeModel.Result) { + guard self.result == nil else { return } + self.result = result + digits = "" + newPasscode = "" + cancelBiometrics() + onCompletion?(result) + } +} diff --git a/iOSClient/Utility/Passcode/NCPasscodeView.swift b/iOSClient/Utility/Passcode/NCPasscodeView.swift new file mode 100644 index 0000000000..18b17c7c45 --- /dev/null +++ b/iOSClient/Utility/Passcode/NCPasscodeView.swift @@ -0,0 +1,182 @@ +// SPDX-FileCopyrightText: Nextcloud GmbH +// SPDX-FileCopyrightText: 2026 Marino Faggiana +// SPDX-License-Identifier: GPL-3.0-or-later + +import SwiftUI + +/// Shared keypad for authentication, creation, change and removal of the app code. +@MainActor +struct NCPasscodeView: View { + @StateObject private var model: NCPasscodeModel + @Environment(\.scenePhase) private var scenePhase + + init(model: NCPasscodeModel) { + _model = StateObject(wrappedValue: model) + } + + /// SwiftUI callers handle the result and dismiss their sheet in completion. + init(mode: NCPasscodeModel.Mode = .authenticate, allowCancel: Bool = false, + completion: @escaping @MainActor (NCPasscodeModel.Result) -> Void) { + _model = StateObject(wrappedValue: { + let model = NCPasscodeModel(mode: mode, allowCancel: allowCancel) + model.onCompletion = completion + return model + }()) + } + + /// UIKit callers receive the result after the passcode screen has closed. + static func present(from presenter: UIViewController, mode: NCPasscodeModel.Mode = .authenticate, + allowCancel: Bool = false, completion: @escaping @MainActor (NCPasscodeModel.Result) -> Void) { + guard !(presenter is UIHostingController) else { return } + var top = presenter + while let presented = top.presentedViewController { + if presented is UIHostingController { return } + top = presented + } + let model = NCPasscodeModel(mode: mode, allowCancel: allowCancel) + let controller = UIHostingController(rootView: NCPasscodeView(model: model)) + controller.modalPresentationStyle = .fullScreen + controller.isModalInPresentation = true + model.onCompletion = { [weak controller] result in + controller?.dismiss(animated: true) { + completion(result) + } + } + top.present(controller, animated: true) + } + + var body: some View { + GeometryReader { geometry in + ScrollView { + VStack(spacing: 24) { + Image(systemName: "lock.shield") + .font(.largeTitle) + .accessibilityHidden(true) + Text(NSLocalizedString(model.title, comment: "")) + .font(.title2) + .multilineTextAlignment(.center) + .accessibilityAddTraits(.isHeader) + HStack(spacing: 16) { + ForEach(0..<6) { index in + Image(systemName: index < model.digits.count ? "circle.fill" : "circle") + .font(.body) + } + } + .accessibilityElement(children: .ignore) + .accessibilityLabel(NSLocalizedString("_passcode_digits_entered_", comment: "")) + .accessibilityValue("\(model.digits.count) / 6") + + if let deadline = model.lockoutEnd { + TimelineView(.periodic(from: .now, by: 1)) { timeline in + VStack(spacing: 8) { + Text(NSLocalizedString("_passcode_counter_fail_", comment: "")) + Text("\(max(0, Int(deadline.timeIntervalSince(timeline.date).rounded(.up)))) " + NSLocalizedString("_seconds_", comment: "")) + } + .multilineTextAlignment(.center) + } + } else { + Text(NSLocalizedString(model.message, comment: "")) + .foregroundStyle(.red) + .multilineTextAlignment(.center) + .frame(minHeight: 24) + } + keypad + if model.allowCancel { + Button(NSLocalizedString("_cancel_", comment: "")) { + model.cancel() + } + .padding(.top, 8) + } + } + .padding(24) + .frame(maxWidth: 360) + .frame(maxWidth: .infinity, minHeight: geometry.size.height) + } + } + .background(Color(uiColor: .systemBackground)) + .interactiveDismissDisabled() + .task { await model.start() } + .task(id: model.lockoutEnd) { + guard let deadline = model.lockoutEnd else { return } + let remaining = max(0, deadline.timeIntervalSinceNow) + do { + try await Task.sleep(for: .seconds(remaining)) + model.refreshProtection() + } catch { return } + } + .onChange(of: scenePhase) { _, phase in + if phase == .active { + model.refreshProtection() + } else if phase == .background { + model.cancelBiometrics() + } + } + .onDisappear { model.cancelBiometrics() } + } + + private var keypad: some View { + Grid(horizontalSpacing: 20, verticalSpacing: 12) { + ForEach(0..<3) { row in + GridRow { + ForEach(1..<4) { column in + digitButton(row * 3 + column) + } + } + } + GridRow { + Button { + Task { await model.authenticateWithBiometrics() } + } label: { + Image(systemName: model.biometricSymbol ?? "faceid") + .font(.title) + .frame(minWidth: 64, minHeight: 64) + } + .accessibilityLabel(model.biometricSymbol == "touchid" ? "Touch ID" : "Face ID") + .disabled(!model.canEnterCode || model.biometricSymbol == nil) + .opacity(model.biometricSymbol == nil ? 0 : 1) + digitButton(0) + Button { + model.deleteDigit() + } label: { + Image(systemName: "delete.left") + .font(.title2) + .frame(minWidth: 64, minHeight: 64) + } + .accessibilityLabel(NSLocalizedString("_delete_", comment: "")) + .disabled(!model.canEnterCode || model.digits.isEmpty) + } + } + .buttonStyle(.plain) + } + + private func digitButton(_ digit: Int) -> some View { + Button { + model.append(digit) + } label: { + Text(String(digit)) + .font(.largeTitle.monospacedDigit()) + .frame(minWidth: 64, minHeight: 64) + .background(Color(uiColor: .secondarySystemBackground), in: Circle()) + } + .disabled(!model.canEnterCode) + } +} + +#if DEBUG +#Preview("Unlock") { + NCPasscodeView(model: .preview()) +} + +#Preview("Unlock – Dark") { + NCPasscodeView(model: .preview()) + .preferredColorScheme(.dark) +} + +#Preview("Create passcode") { + NCPasscodeView(model: .preview(mode: .create)) +} + +#Preview("Too many attempts") { + NCPasscodeView(model: .preview(locked: true)) +} +#endif From 78bc60313cf3bb287286abc5d560c1dddabf7b33 Mon Sep 17 00:00:00 2001 From: Marino Faggiana Date: Thu, 22 Oct 2026 16:25:50 +0200 Subject: [PATCH 02/14] feat: migrate settings to SwiftUI passcode flow Update lock settings after passcode completion and handle reset results. Add toolbar dismissal and require the current passcode when changing it. Signed-off-by: Marino Faggiana --- .../Settings/Settings/NCSettingsView.swift | 24 ++++- .../Utility/Passcode/NCPasscodeModel.swift | 2 +- .../Utility/Passcode/NCPasscodeView.swift | 90 +++++++++++-------- 3 files changed, 74 insertions(+), 42 deletions(-) diff --git a/iOSClient/Settings/Settings/NCSettingsView.swift b/iOSClient/Settings/Settings/NCSettingsView.swift index b89f0e40ad..4db49d4ecb 100644 --- a/iOSClient/Settings/Settings/NCSettingsView.swift +++ b/iOSClient/Settings/Settings/NCSettingsView.swift @@ -306,10 +306,14 @@ struct NCSettingsView: View { }) } .sheet(isPresented: $showPasscode) { - SetupPasscodeView(isLockActive: $model.isLockActive, controller: model.controller) + NCPasscodeView(mode: NCPreferences().passcode == nil ? .create : .disable, + allowCancel: true, + completion: completePasscode) } .sheet(isPresented: $showChangePasscode) { - SetupPasscodeView(isLockActive: $model.isLockActive, controller: model.controller, changePasscode: true) + NCPasscodeView(mode: .change, + allowCancel: true, + completion: completePasscode) } #if DEBUG .alert("Database export failed", isPresented: $showDatabaseExportError) { @@ -322,6 +326,22 @@ struct NCSettingsView: View { .defaultViewModifier(model) } + /// Update the lock setting only after the passcode operation has completed. + private func completePasscode(_ result: NCPasscodeModel.Result) { + switch result { + case .configured: + model.isLockActive = true + case .disabled: + model.isLockActive = false + case .reset: + (UIApplication.shared.delegate as? AppDelegate)?.resetApplication() + case .authenticated, .cancelled: + break + } + showPasscode = false + showChangePasscode = false + } + #if DEBUG private func exportDebugDatabase() { isExportingDatabase = true diff --git a/iOSClient/Utility/Passcode/NCPasscodeModel.swift b/iOSClient/Utility/Passcode/NCPasscodeModel.swift index a6ab62163a..3ba29b0822 100644 --- a/iOSClient/Utility/Passcode/NCPasscodeModel.swift +++ b/iOSClient/Utility/Passcode/NCPasscodeModel.swift @@ -64,7 +64,7 @@ final class NCPasscodeModel: ObservableObject { var biometricSymbol: String? { if isPreview { return step == .authenticate ? "faceid" : nil } - guard step == .authenticate, preferences.touchFaceID, preferences.passcode != nil else { return nil } + guard step == .authenticate, mode != .change, preferences.touchFaceID, preferences.passcode != nil else { return nil } let context = LAContext() guard context.canEvaluatePolicy(.deviceOwnerAuthenticationWithBiometrics, error: nil) else { return nil } return context.biometryType == .faceID ? "faceid" : "touchid" diff --git a/iOSClient/Utility/Passcode/NCPasscodeView.swift b/iOSClient/Utility/Passcode/NCPasscodeView.swift index 18b17c7c45..9e0b525a51 100644 --- a/iOSClient/Utility/Passcode/NCPasscodeView.swift +++ b/iOSClient/Utility/Passcode/NCPasscodeView.swift @@ -46,55 +46,64 @@ struct NCPasscodeView: View { } var body: some View { - GeometryReader { geometry in - ScrollView { - VStack(spacing: 24) { - Image(systemName: "lock.shield") - .font(.largeTitle) - .accessibilityHidden(true) - Text(NSLocalizedString(model.title, comment: "")) - .font(.title2) - .multilineTextAlignment(.center) - .accessibilityAddTraits(.isHeader) - HStack(spacing: 16) { - ForEach(0..<6) { index in - Image(systemName: index < model.digits.count ? "circle.fill" : "circle") - .font(.body) + NavigationStack { + GeometryReader { geometry in + ScrollView { + VStack(spacing: 24) { + Image(systemName: "lock.shield") + .font(.largeTitle) + .accessibilityHidden(true) + Text(NSLocalizedString(model.title, comment: "")) + .font(.title2) + .multilineTextAlignment(.center) + .accessibilityAddTraits(.isHeader) + HStack(spacing: 16) { + ForEach(0..<6) { index in + Image(systemName: index < model.digits.count ? "circle.fill" : "circle") + .font(.body) + } } - } - .accessibilityElement(children: .ignore) - .accessibilityLabel(NSLocalizedString("_passcode_digits_entered_", comment: "")) - .accessibilityValue("\(model.digits.count) / 6") + .accessibilityElement(children: .ignore) + .accessibilityLabel(NSLocalizedString("_passcode_digits_entered_", comment: "")) + .accessibilityValue("\(model.digits.count) / 6") - if let deadline = model.lockoutEnd { - TimelineView(.periodic(from: .now, by: 1)) { timeline in - VStack(spacing: 8) { - Text(NSLocalizedString("_passcode_counter_fail_", comment: "")) - Text("\(max(0, Int(deadline.timeIntervalSince(timeline.date).rounded(.up)))) " + NSLocalizedString("_seconds_", comment: "")) + if let deadline = model.lockoutEnd { + TimelineView(.periodic(from: .now, by: 1)) { timeline in + VStack(spacing: 8) { + Text(NSLocalizedString("_passcode_counter_fail_", comment: "")) + Text("\(max(0, Int(deadline.timeIntervalSince(timeline.date).rounded(.up)))) " + NSLocalizedString("_seconds_", comment: "")) + } + .multilineTextAlignment(.center) } - .multilineTextAlignment(.center) + } else { + Text(NSLocalizedString(model.message, comment: "")) + .foregroundStyle(.red) + .multilineTextAlignment(.center) + .frame(minHeight: 24) } - } else { - Text(NSLocalizedString(model.message, comment: "")) - .foregroundStyle(.red) - .multilineTextAlignment(.center) - .frame(minHeight: 24) + keypad } - keypad - if model.allowCancel { - Button(NSLocalizedString("_cancel_", comment: "")) { + .padding(24) + .frame(maxWidth: 360) + .frame(maxWidth: .infinity, minHeight: geometry.size.height) + } + } + .background(Color(uiColor: .systemBackground)) + .navigationBarTitleDisplayMode(.inline) + .toolbar { + if model.allowCancel { + ToolbarItem(placement: .cancellationAction) { + Button { model.cancel() + } label: { + Image(systemName: "xmark") } - .padding(.top, 8) + .accessibilityLabel(NSLocalizedString("_close_", comment: "")) } } - .padding(24) - .frame(maxWidth: 360) - .frame(maxWidth: .infinity, minHeight: geometry.size.height) } } - .background(Color(uiColor: .systemBackground)) - .interactiveDismissDisabled() + .interactiveDismissDisabled(!model.allowCancel) .task { await model.start() } .task(id: model.lockoutEnd) { guard let deadline = model.lockoutEnd else { return } @@ -111,7 +120,10 @@ struct NCPasscodeView: View { model.cancelBiometrics() } } - .onDisappear { model.cancelBiometrics() } + .onDisappear { + model.cancelBiometrics() + model.cancel() + } } private var keypad: some View { From 90dc3e1ab6582dd4517b9b746f92f621d0f1c5d1 Mon Sep 17 00:00:00 2001 From: Marino Faggiana Date: Thu, 22 Oct 2026 17:16:19 +0200 Subject: [PATCH 03/14] fix: authenticate E2EE actions with the shared passcode view Require authentication in Debug builds and verify the account before executing E2EE actions. Present cancellable passcode flows as sheets and handle completion during dismissal. Signed-off-by: Marino Faggiana --- .../Settings/E2EE/NCManageE2EEModel.swift | 75 +++++-------------- .../Utility/Passcode/NCPasscodeView.swift | 11 ++- 2 files changed, 25 insertions(+), 61 deletions(-) diff --git a/iOSClient/Settings/E2EE/NCManageE2EEModel.swift b/iOSClient/Settings/E2EE/NCManageE2EEModel.swift index 8493b99bc9..1aacb0371a 100644 --- a/iOSClient/Settings/E2EE/NCManageE2EEModel.swift +++ b/iOSClient/Settings/E2EE/NCManageE2EEModel.swift @@ -5,11 +5,9 @@ import UIKit import SwiftUI import NextcloudKit -import LocalAuthentication @MainActor -class NCManageE2EE: NSObject, ObservableObject, ViewOnAppearHandling, TOPasscodeViewControllerDelegate { - var passcodeType = "" +class NCManageE2EE: NSObject, ObservableObject, ViewOnAppearHandling { let preference = NCPreferences() let networkingE2EE = NCNetworkingE2EE() @@ -101,38 +99,27 @@ class NCManageE2EE: NSObject, ObservableObject, ViewOnAppearHandling, TOPasscode // MARK: - Passcode + /// Authenticate before performing the selected E2EE action, including in Debug builds. @objc func requestPasscodeType(_ passcodeType: String) { - #if DEBUG - self.passcodeType = passcodeType - correctPasscode() - return - #else - let laContext = LAContext() - var error: NSError? - let passcodeViewController = TOPasscodeViewController(passcodeType: .sixDigits, allowCancel: true) - - passcodeViewController.delegate = self - passcodeViewController.keypadButtonShowLettering = false - if NCPreferences().touchFaceID, laContext.canEvaluatePolicy(.deviceOwnerAuthenticationWithBiometrics, error: &error) { - if error == nil { - if laContext.biometryType == .faceID { - passcodeViewController.biometryType = .faceID - passcodeViewController.allowBiometricValidation = true - } else if laContext.biometryType == .touchID { - passcodeViewController.biometryType = .touchID - } - passcodeViewController.allowBiometricValidation = true - passcodeViewController.automaticallyPromptForBiometricValidation = true + guard let presenter = controller?.topMostViewController() else { return } + let account = session.account + NCPasscodeView.present(from: presenter, allowCancel: true) { [weak self] result in + guard let self else { return } + switch result { + case .authenticated: + guard self.session.account == account, + NCManageDatabase.shared.getTableAccount(account: account) != nil else { return } + self.performAuthenticatedAction(passcodeType) + case .reset: + (UIApplication.shared.delegate as? AppDelegate)?.resetApplication() + case .cancelled, .configured, .disabled: + break } } - - self.passcodeType = passcodeType - controller?.topMostViewController().present(passcodeViewController, animated: true) - #endif } - @objc func correctPasscode() { - switch self.passcodeType { + private func performAuthenticatedAction(_ passcodeType: String) { + switch passcodeType { case "startE2E": Task { do { @@ -177,32 +164,4 @@ class NCManageE2EE: NSObject, ObservableObject, ViewOnAppearHandling, TOPasscode break } } - - nonisolated func passcodeViewController(_ passcodeViewController: TOPasscodeViewController, isCorrectCode code: String) -> Bool { - if code == NCPreferences().passcode { - DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) { - self.correctPasscode() - } - return true - } else { - return false - } - } - - nonisolated func didPerformBiometricValidationRequest(in passcodeViewController: TOPasscodeViewController) { - LAContext().evaluatePolicy(.deviceOwnerAuthenticationWithBiometrics, localizedReason: NCBrandOptions.shared.brand) { success, _ in - if success { - DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) { - passcodeViewController.dismiss(animated: true) - self.correctPasscode() - } - } - } - } - - nonisolated func didTapCancel(in passcodeViewController: TOPasscodeViewController) { - Task {@MainActor in - passcodeViewController.dismiss(animated: true) - } - } } diff --git a/iOSClient/Utility/Passcode/NCPasscodeView.swift b/iOSClient/Utility/Passcode/NCPasscodeView.swift index 9e0b525a51..f3c032f414 100644 --- a/iOSClient/Utility/Passcode/NCPasscodeView.swift +++ b/iOSClient/Utility/Passcode/NCPasscodeView.swift @@ -35,11 +35,16 @@ struct NCPasscodeView: View { } let model = NCPasscodeModel(mode: mode, allowCancel: allowCancel) let controller = UIHostingController(rootView: NCPasscodeView(model: model)) - controller.modalPresentationStyle = .fullScreen - controller.isModalInPresentation = true + controller.modalPresentationStyle = allowCancel ? .pageSheet : .fullScreen + controller.isModalInPresentation = !allowCancel model.onCompletion = { [weak controller] result in - controller?.dismiss(animated: true) { + guard let controller else { return } + if controller.isBeingDismissed || controller.presentingViewController == nil { completion(result) + } else { + controller.dismiss(animated: true) { + completion(result) + } } } top.present(controller, animated: true) From f33758a89081cc599ec036877e8fbf0f32d22d9e Mon Sep 17 00:00:00 2001 From: Marino Faggiana Date: Thu, 22 Oct 2026 18:24:08 +0200 Subject: [PATCH 04/14] refactor: migrate app passcode flows to NCPasscodeView Use the shared passcode view for app unlock and enforced setup. Scope lifecycle handling to the presenting window scene and cancel optional operations when backgrounded. Signed-off-by: Marino Faggiana --- iOSClient/Main/NCMainTabBarController.swift | 9 +++--- iOSClient/SceneDelegate.swift | 23 ++++++++++----- .../Settings/Settings/NCSettingsView.swift | 2 ++ .../Utility/Passcode/NCPasscodeView.swift | 29 +++++++++++-------- 4 files changed, 39 insertions(+), 24 deletions(-) diff --git a/iOSClient/Main/NCMainTabBarController.swift b/iOSClient/Main/NCMainTabBarController.swift index cb2e77b644..6d6e09f981 100644 --- a/iOSClient/Main/NCMainTabBarController.swift +++ b/iOSClient/Main/NCMainTabBarController.swift @@ -100,10 +100,11 @@ class NCMainTabBarController: UITabBarController { previousIndex = selectedIndex if NCBrandOptions.shared.enforce_passcode_lock && NCPreferences().passcode.isEmptyOrNil { - let vc = UIHostingController(rootView: SetupPasscodeView(isLockActive: .constant(false), controller: self)) - vc.isModalInPresentation = true - - present(vc, animated: true) + NCPasscodeView.present(from: self, mode: .create) { result in + if result == .reset { + (UIApplication.shared.delegate as? AppDelegate)?.resetApplication() + } + } } } diff --git a/iOSClient/SceneDelegate.swift b/iOSClient/SceneDelegate.swift index f6850cb917..c3baba7c83 100644 --- a/iOSClient/SceneDelegate.swift +++ b/iOSClient/SceneDelegate.swift @@ -533,10 +533,21 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate { if let window = SceneManager.shared.getWindow(scene: scene), let controller = SceneManager.shared.getController(scene: scene) { - window.rootViewController = controller + if window.rootViewController !== controller { + window.rootViewController = controller + } if NCPreferences().presentPasscode { - NCPasscode.shared.presentPasscode(viewController: controller, delegate: self) { - NCPasscode.shared.enableTouchFaceID() + NCPasscodeView.present(from: controller) { [weak self, weak controller] result in + switch result { + case .authenticated, .configured: + if NCPreferences().accountRequest, let controller { + self?.requestedAccount(controller: controller) + } + case .reset: + self?.appDelegate?.resetApplication() + case .cancelled, .disabled: + break + } } } else if NCPreferences().accountRequest { requestedAccount(controller: controller) @@ -562,7 +573,7 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate { // MARK: - Extension -extension SceneDelegate: NCPasscodeDelegate { +extension SceneDelegate { func requestedAccount(controller: UIViewController?) { let tblAccounts = NCManageDatabase.shared.getAllTableAccount() if tblAccounts.count > 1, let accountRequestVC = UIStoryboard(name: "NCAccountRequest", bundle: nil).instantiateInitialViewController() as? NCAccountRequest { @@ -586,10 +597,6 @@ extension SceneDelegate: NCPasscodeDelegate { controller?.present(popup, animated: true) } } - - func passcodeReset(_ passcodeViewController: TOPasscodeViewController) { - appDelegate?.resetApplication() - } } extension SceneDelegate: NCAccountRequestDelegate { diff --git a/iOSClient/Settings/Settings/NCSettingsView.swift b/iOSClient/Settings/Settings/NCSettingsView.swift index 4db49d4ecb..20a04fec7d 100644 --- a/iOSClient/Settings/Settings/NCSettingsView.swift +++ b/iOSClient/Settings/Settings/NCSettingsView.swift @@ -308,11 +308,13 @@ struct NCSettingsView: View { .sheet(isPresented: $showPasscode) { NCPasscodeView(mode: NCPreferences().passcode == nil ? .create : .disable, allowCancel: true, + windowScene: SceneManager.shared.getWindowScene(controller: model.controller), completion: completePasscode) } .sheet(isPresented: $showChangePasscode) { NCPasscodeView(mode: .change, allowCancel: true, + windowScene: SceneManager.shared.getWindowScene(controller: model.controller), completion: completePasscode) } #if DEBUG diff --git a/iOSClient/Utility/Passcode/NCPasscodeView.swift b/iOSClient/Utility/Passcode/NCPasscodeView.swift index f3c032f414..5a757bc69b 100644 --- a/iOSClient/Utility/Passcode/NCPasscodeView.swift +++ b/iOSClient/Utility/Passcode/NCPasscodeView.swift @@ -8,20 +8,22 @@ import SwiftUI @MainActor struct NCPasscodeView: View { @StateObject private var model: NCPasscodeModel - @Environment(\.scenePhase) private var scenePhase + private weak var windowScene: UIWindowScene? - init(model: NCPasscodeModel) { + init(model: NCPasscodeModel, windowScene: UIWindowScene? = nil) { _model = StateObject(wrappedValue: model) + self.windowScene = windowScene } /// SwiftUI callers handle the result and dismiss their sheet in completion. init(mode: NCPasscodeModel.Mode = .authenticate, allowCancel: Bool = false, - completion: @escaping @MainActor (NCPasscodeModel.Result) -> Void) { + windowScene: UIWindowScene? = nil, completion: @escaping @MainActor (NCPasscodeModel.Result) -> Void) { _model = StateObject(wrappedValue: { let model = NCPasscodeModel(mode: mode, allowCancel: allowCancel) model.onCompletion = completion return model }()) + self.windowScene = windowScene } /// UIKit callers receive the result after the passcode screen has closed. @@ -34,7 +36,7 @@ struct NCPasscodeView: View { top = presented } let model = NCPasscodeModel(mode: mode, allowCancel: allowCancel) - let controller = UIHostingController(rootView: NCPasscodeView(model: model)) + let controller = UIHostingController(rootView: NCPasscodeView(model: model, windowScene: top.view.window?.windowScene)) controller.modalPresentationStyle = allowCancel ? .pageSheet : .fullScreen controller.isModalInPresentation = !allowCancel model.onCompletion = { [weak controller] result in @@ -42,12 +44,12 @@ struct NCPasscodeView: View { if controller.isBeingDismissed || controller.presentingViewController == nil { completion(result) } else { - controller.dismiss(animated: true) { + controller.dismiss(animated: allowCancel && controller.view.window?.windowScene?.activationState == .foregroundActive) { completion(result) } } } - top.present(controller, animated: true) + top.present(controller, animated: allowCancel) } var body: some View { @@ -118,12 +120,15 @@ struct NCPasscodeView: View { model.refreshProtection() } catch { return } } - .onChange(of: scenePhase) { _, phase in - if phase == .active { - model.refreshProtection() - } else if phase == .background { - model.cancelBiometrics() - } + .onReceive(NotificationCenter.default.publisher(for: UIScene.didActivateNotification)) { notification in + guard let windowScene, (notification.object as? UIScene) === windowScene else { return } + model.refreshProtection() + } + .onReceive(NotificationCenter.default.publisher(for: UIScene.didEnterBackgroundNotification)) { notification in + guard let windowScene, (notification.object as? UIScene) === windowScene else { return } + model.cancelBiometrics() + // Optional operations cancel themselves; mandatory app authentication stays open. + model.cancel() } .onDisappear { model.cancelBiometrics() From 09786dbaf9a8e7762dc00c6b65430d7bfc5b381f Mon Sep 17 00:00:00 2001 From: Marino Faggiana Date: Fri, 23 Oct 2026 10:49:57 +0200 Subject: [PATCH 05/14] refactor: migrate passcode flows to shared SwiftUI components Replace TOPasscode in the main app and Share Extension, remove the legacy implementation, and add passcode UI tests. Signed-off-by: Marino Faggiana --- AGENTS.md | 2 +- Nextcloud.xcodeproj/project.pbxproj | 229 +----- .../xcshareddata/swiftpm/Package.resolved | 11 +- REUSE.toml | 6 - Share/NCShareExtension.swift | 44 +- Share/Share-Bridging-Header.h | 1 - Tests/NextcloudUITests/PasscodeUITests.swift | 250 ++++++ iOSClient/AppDelegate.swift | 11 + .../UIAlertController+Extension.swift | 41 - iOSClient/Main/NCPasscode.swift | 196 ----- iOSClient/Nextcloud-Bridging-Header.h | 1 - iOSClient/Settings/Acknowledgements.rtf | 9 - .../Settings/Settings/NCSettingsView.swift | 2 + .../Settings/Settings/SetupPasscodeView.swift | 186 ----- iOSClient/Utility/NCAskAuthorization.swift | 3 - .../Utility/Passcode/NCPasscodeModel.swift | 6 +- .../Utility/Passcode/NCPasscodeView.swift | 7 + .../Models/TOPasscodeCircleImage.h | 57 -- .../Models/TOPasscodeCircleImage.m | 75 -- .../Models/TOPasscodeViewContentLayout.h | 100 --- .../Models/TOPasscodeViewContentLayout.m | 205 ----- ...scodeViewControllerAnimatedTransitioning.h | 60 -- ...scodeViewControllerAnimatedTransitioning.m | 120 --- .../Models/TOSettingsKeypadImage.h | 54 -- .../Models/TOSettingsKeypadImage.m | 186 ----- .../TOPasscodeViewControllerConstants.h | 71 -- .../TOPasscodeSettingsViewController.h | 108 --- .../TOPasscodeSettingsViewController.m | 630 ---------------- .../TOPasscodeViewController.h | 163 ---- .../TOPasscodeViewController.m | 711 ------------------ .../Views/Main/TOPasscodeCircleButton.h | 81 -- .../Views/Main/TOPasscodeCircleButton.m | 236 ------ .../Views/Main/TOPasscodeKeypadView.h | 101 --- .../Views/Main/TOPasscodeKeypadView.m | 432 ----------- .../Views/Main/TOPasscodeView.h | 136 ---- .../Views/Main/TOPasscodeView.m | 690 ----------------- .../Settings/TOPasscodeSettingsKeypadButton.h | 45 -- .../Settings/TOPasscodeSettingsKeypadButton.m | 103 --- .../Settings/TOPasscodeSettingsKeypadView.h | 74 -- .../Settings/TOPasscodeSettingsKeypadView.m | 395 ---------- .../Settings/TOPasscodeSettingsWarningLabel.h | 43 -- .../Settings/TOPasscodeSettingsWarningLabel.m | 154 ---- .../Views/Shared/TOPasscodeButtonLabel.h | 61 -- .../Views/Shared/TOPasscodeButtonLabel.m | 186 ----- .../Views/Shared/TOPasscodeCircleView.h | 46 -- .../Views/Shared/TOPasscodeCircleView.m | 89 --- .../Views/Shared/TOPasscodeFixedInputView.h | 54 -- .../Views/Shared/TOPasscodeFixedInputView.m | 171 ----- .../Views/Shared/TOPasscodeInputField.h | 111 --- .../Views/Shared/TOPasscodeInputField.m | 423 ----------- .../Shared/TOPasscodeVariableInputView.h | 55 -- .../Shared/TOPasscodeVariableInputView.m | 254 ------- 52 files changed, 302 insertions(+), 7183 deletions(-) create mode 100644 Tests/NextcloudUITests/PasscodeUITests.swift delete mode 100644 iOSClient/Main/NCPasscode.swift delete mode 100644 iOSClient/Settings/Settings/SetupPasscodeView.swift delete mode 100644 iOSClient/Utility/TOPasscodeViewController/Models/TOPasscodeCircleImage.h delete mode 100644 iOSClient/Utility/TOPasscodeViewController/Models/TOPasscodeCircleImage.m delete mode 100644 iOSClient/Utility/TOPasscodeViewController/Models/TOPasscodeViewContentLayout.h delete mode 100644 iOSClient/Utility/TOPasscodeViewController/Models/TOPasscodeViewContentLayout.m delete mode 100644 iOSClient/Utility/TOPasscodeViewController/Models/TOPasscodeViewControllerAnimatedTransitioning.h delete mode 100644 iOSClient/Utility/TOPasscodeViewController/Models/TOPasscodeViewControllerAnimatedTransitioning.m delete mode 100644 iOSClient/Utility/TOPasscodeViewController/Models/TOSettingsKeypadImage.h delete mode 100644 iOSClient/Utility/TOPasscodeViewController/Models/TOSettingsKeypadImage.m delete mode 100644 iOSClient/Utility/TOPasscodeViewController/Supporting/TOPasscodeViewControllerConstants.h delete mode 100644 iOSClient/Utility/TOPasscodeViewController/TOPasscodeSettingsViewController.h delete mode 100644 iOSClient/Utility/TOPasscodeViewController/TOPasscodeSettingsViewController.m delete mode 100644 iOSClient/Utility/TOPasscodeViewController/TOPasscodeViewController.h delete mode 100755 iOSClient/Utility/TOPasscodeViewController/TOPasscodeViewController.m delete mode 100644 iOSClient/Utility/TOPasscodeViewController/Views/Main/TOPasscodeCircleButton.h delete mode 100644 iOSClient/Utility/TOPasscodeViewController/Views/Main/TOPasscodeCircleButton.m delete mode 100644 iOSClient/Utility/TOPasscodeViewController/Views/Main/TOPasscodeKeypadView.h delete mode 100644 iOSClient/Utility/TOPasscodeViewController/Views/Main/TOPasscodeKeypadView.m delete mode 100644 iOSClient/Utility/TOPasscodeViewController/Views/Main/TOPasscodeView.h delete mode 100644 iOSClient/Utility/TOPasscodeViewController/Views/Main/TOPasscodeView.m delete mode 100644 iOSClient/Utility/TOPasscodeViewController/Views/Settings/TOPasscodeSettingsKeypadButton.h delete mode 100644 iOSClient/Utility/TOPasscodeViewController/Views/Settings/TOPasscodeSettingsKeypadButton.m delete mode 100644 iOSClient/Utility/TOPasscodeViewController/Views/Settings/TOPasscodeSettingsKeypadView.h delete mode 100644 iOSClient/Utility/TOPasscodeViewController/Views/Settings/TOPasscodeSettingsKeypadView.m delete mode 100644 iOSClient/Utility/TOPasscodeViewController/Views/Settings/TOPasscodeSettingsWarningLabel.h delete mode 100644 iOSClient/Utility/TOPasscodeViewController/Views/Settings/TOPasscodeSettingsWarningLabel.m delete mode 100644 iOSClient/Utility/TOPasscodeViewController/Views/Shared/TOPasscodeButtonLabel.h delete mode 100644 iOSClient/Utility/TOPasscodeViewController/Views/Shared/TOPasscodeButtonLabel.m delete mode 100644 iOSClient/Utility/TOPasscodeViewController/Views/Shared/TOPasscodeCircleView.h delete mode 100644 iOSClient/Utility/TOPasscodeViewController/Views/Shared/TOPasscodeCircleView.m delete mode 100644 iOSClient/Utility/TOPasscodeViewController/Views/Shared/TOPasscodeFixedInputView.h delete mode 100644 iOSClient/Utility/TOPasscodeViewController/Views/Shared/TOPasscodeFixedInputView.m delete mode 100644 iOSClient/Utility/TOPasscodeViewController/Views/Shared/TOPasscodeInputField.h delete mode 100644 iOSClient/Utility/TOPasscodeViewController/Views/Shared/TOPasscodeInputField.m delete mode 100644 iOSClient/Utility/TOPasscodeViewController/Views/Shared/TOPasscodeVariableInputView.h delete mode 100644 iOSClient/Utility/TOPasscodeViewController/Views/Shared/TOPasscodeVariableInputView.m diff --git a/AGENTS.md b/AGENTS.md index 0ab6360ac2..a5e778ff25 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -26,7 +26,7 @@ Other platforms like Android are irrelevant for this project. |-----------------|-----------------------------------------------------|----------------------| | `.github` | GitHub CI workflows (build & test, SwiftLint, extension-target builds) and issue templates. | Keep all three checks green: `xcode.yml` (builds the `Nextcloud` scheme and runs its tests against a Nextcloud server provisioned in CI), `lint.yml` (SwiftLint), `additional-targets.yml` (build-checks the extension schemes). CI skips draft PRs. | | `iOSClient` | Main iOS client source code. | — | -| `iOSClient/Utility/Passcode` | Shared SwiftUI passcode component, developed alongside the existing TOPasscode implementation. | Keep the existing passcode flow active until callers are migrated. | +| `iOSClient/Utility/Passcode` | Shared SwiftUI passcode UI and authentication model used by the main app and Share Extension. | Preserve existing credentials, lockout rules and app/extension reset behavior. | | `iOSClient/Supporting Files` | Translation files synced with Transifex (see `iOSClient/.tx/config`). | Only add new strings to `en.lproj` (`Localizable.strings`, plurals in `Localizable.stringsdict`); never edit the other `*.lproj` dirs — Transifex overwrites them. | | `Brand` | Branding layer: `NCBrand.swift` (app constants, colors), `Database.swift` (Realm database name and schema version), Info.plists and entitlements for all targets, launch screen and intro screens. | Bump `databaseSchemaVersion` in `Brand/Database.swift` when changing Realm models. | | `File Provider Extension` | Contains business logic for the iOS File Provider extension. | — | diff --git a/Nextcloud.xcodeproj/project.pbxproj b/Nextcloud.xcodeproj/project.pbxproj index 3037743842..760299cde2 100644 --- a/Nextcloud.xcodeproj/project.pbxproj +++ b/Nextcloud.xcodeproj/project.pbxproj @@ -174,7 +174,6 @@ F37208C22BAB63F0006B5430 /* TagListView in Frameworks */ = {isa = PBXBuildFile; productRef = F37208C12BAB63F0006B5430 /* TagListView */; }; F37208C42BAB63F0006B5430 /* Gzip in Frameworks */ = {isa = PBXBuildFile; productRef = F37208C32BAB63F0006B5430 /* Gzip */; }; F37208C82BAB63F1006B5430 /* KeychainAccess in Frameworks */ = {isa = PBXBuildFile; productRef = F37208C72BAB63F1006B5430 /* KeychainAccess */; }; - F3754A7D2CF87D600009312E /* SetupPasscodeView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F3754A7C2CF87D600009312E /* SetupPasscodeView.swift */; }; F389C9F52CEE383300049762 /* SelectAlbumView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F389C9F42CEE383300049762 /* SelectAlbumView.swift */; }; F38F71252B6BBDC300473CDC /* NCCollectionViewCommonSelectTabBar.swift in Sources */ = {isa = PBXBuildFile; fileRef = F38F71242B6BBDC300473CDC /* NCCollectionViewCommonSelectTabBar.swift */; }; F39170AD2CB82024006127BC /* FileAutoRenamer+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = F39170A82CB8201B006127BC /* FileAutoRenamer+Extensions.swift */; }; @@ -365,8 +364,6 @@ F7346E2328B0FEBA006CE2D2 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = F7346E2228B0FEBA006CE2D2 /* Assets.xcassets */; }; F734B06628E75C0100E180D5 /* TLPhotoPicker in Frameworks */ = {isa = PBXBuildFile; productRef = F734B06528E75C0100E180D5 /* TLPhotoPicker */; }; F7362A1F220C853A005101B5 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = F7362A1E220C853A005101B5 /* LaunchScreen.storyboard */; }; - F737DA9D2B7B893C0063BAFC /* NCPasscode.swift in Sources */ = {isa = PBXBuildFile; fileRef = F737DA9C2B7B893C0063BAFC /* NCPasscode.swift */; }; - F737DA9E2B7B893C0063BAFC /* NCPasscode.swift in Sources */ = {isa = PBXBuildFile; fileRef = F737DA9C2B7B893C0063BAFC /* NCPasscode.swift */; }; F7381EE1218218C9000B1560 /* NCOffline.swift in Sources */ = {isa = PBXBuildFile; fileRef = F7381EDA218218C9000B1560 /* NCOffline.swift */; }; F7381EE5218218C9000B1560 /* NCOffline.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = F7381EDE218218C9000B1560 /* NCOffline.storyboard */; }; F7386E482DA90E0F009A00F6 /* NCAppVersionManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = F7386E452DA90E02009A00F6 /* NCAppVersionManager.swift */; }; @@ -981,42 +978,6 @@ F7D496FC2EBFA541004F9823 /* NCRecommendationsCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = F75D90202D2BE26C003E740B /* NCRecommendationsCell.swift */; }; F7D496FD2EBFA6D9004F9823 /* String+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = F7A0D1342591FBC5008F8A13 /* String+Extension.swift */; }; F7D497082EBFAFD6004F9823 /* NCImageCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = F76B649B2ADFFAED00014640 /* NCImageCache.swift */; }; - F7D4BF012CA1831900A5E746 /* NCCollectionViewCommonPinchGesture.swift in Sources */ = {isa = PBXBuildFile; fileRef = F7D4BF002CA1831600A5E746 /* NCCollectionViewCommonPinchGesture.swift */; }; - F7D4BF2C2CA2E8D800A5E746 /* TOPasscodeKeypadView.m in Sources */ = {isa = PBXBuildFile; fileRef = F7D4BF102CA2E8D800A5E746 /* TOPasscodeKeypadView.m */; }; - F7D4BF2D2CA2E8D800A5E746 /* TOPasscodeSettingsKeypadView.m in Sources */ = {isa = PBXBuildFile; fileRef = F7D4BF172CA2E8D800A5E746 /* TOPasscodeSettingsKeypadView.m */; }; - F7D4BF2E2CA2E8D800A5E746 /* TOPasscodeFixedInputView.m in Sources */ = {isa = PBXBuildFile; fileRef = F7D4BF202CA2E8D800A5E746 /* TOPasscodeFixedInputView.m */; }; - F7D4BF2F2CA2E8D800A5E746 /* TOPasscodeButtonLabel.m in Sources */ = {isa = PBXBuildFile; fileRef = F7D4BF1C2CA2E8D800A5E746 /* TOPasscodeButtonLabel.m */; }; - F7D4BF302CA2E8D800A5E746 /* TOPasscodeViewControllerAnimatedTransitioning.m in Sources */ = {isa = PBXBuildFile; fileRef = F7D4BF072CA2E8D800A5E746 /* TOPasscodeViewControllerAnimatedTransitioning.m */; }; - F7D4BF312CA2E8D800A5E746 /* TOPasscodeSettingsViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = F7D4BF282CA2E8D800A5E746 /* TOPasscodeSettingsViewController.m */; }; - F7D4BF322CA2E8D800A5E746 /* TOPasscodeCircleImage.m in Sources */ = {isa = PBXBuildFile; fileRef = F7D4BF032CA2E8D800A5E746 /* TOPasscodeCircleImage.m */; }; - F7D4BF332CA2E8D800A5E746 /* TOPasscodeView.m in Sources */ = {isa = PBXBuildFile; fileRef = F7D4BF122CA2E8D800A5E746 /* TOPasscodeView.m */; }; - F7D4BF342CA2E8D800A5E746 /* TOPasscodeCircleButton.m in Sources */ = {isa = PBXBuildFile; fileRef = F7D4BF0E2CA2E8D800A5E746 /* TOPasscodeCircleButton.m */; }; - F7D4BF352CA2E8D800A5E746 /* TOPasscodeInputField.m in Sources */ = {isa = PBXBuildFile; fileRef = F7D4BF222CA2E8D800A5E746 /* TOPasscodeInputField.m */; }; - F7D4BF362CA2E8D800A5E746 /* TOSettingsKeypadImage.m in Sources */ = {isa = PBXBuildFile; fileRef = F7D4BF092CA2E8D800A5E746 /* TOSettingsKeypadImage.m */; }; - F7D4BF372CA2E8D800A5E746 /* TOPasscodeSettingsWarningLabel.m in Sources */ = {isa = PBXBuildFile; fileRef = F7D4BF192CA2E8D800A5E746 /* TOPasscodeSettingsWarningLabel.m */; }; - F7D4BF382CA2E8D800A5E746 /* TOPasscodeVariableInputView.m in Sources */ = {isa = PBXBuildFile; fileRef = F7D4BF242CA2E8D800A5E746 /* TOPasscodeVariableInputView.m */; }; - F7D4BF392CA2E8D800A5E746 /* TOPasscodeCircleView.m in Sources */ = {isa = PBXBuildFile; fileRef = F7D4BF1E2CA2E8D800A5E746 /* TOPasscodeCircleView.m */; }; - F7D4BF3A2CA2E8D800A5E746 /* TOPasscodeViewContentLayout.m in Sources */ = {isa = PBXBuildFile; fileRef = F7D4BF052CA2E8D800A5E746 /* TOPasscodeViewContentLayout.m */; }; - F7D4BF3B2CA2E8D800A5E746 /* TOPasscodeSettingsKeypadButton.m in Sources */ = {isa = PBXBuildFile; fileRef = F7D4BF152CA2E8D800A5E746 /* TOPasscodeSettingsKeypadButton.m */; }; - F7D4BF3C2CA2E8D800A5E746 /* TOPasscodeViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = F7D4BF2A2CA2E8D800A5E746 /* TOPasscodeViewController.m */; }; - F7D4BF3D2CA2E8D800A5E746 /* TOPasscodeKeypadView.m in Sources */ = {isa = PBXBuildFile; fileRef = F7D4BF102CA2E8D800A5E746 /* TOPasscodeKeypadView.m */; }; - F7D4BF3E2CA2E8D800A5E746 /* TOPasscodeSettingsKeypadView.m in Sources */ = {isa = PBXBuildFile; fileRef = F7D4BF172CA2E8D800A5E746 /* TOPasscodeSettingsKeypadView.m */; }; - F7D4BF3F2CA2E8D800A5E746 /* TOPasscodeFixedInputView.m in Sources */ = {isa = PBXBuildFile; fileRef = F7D4BF202CA2E8D800A5E746 /* TOPasscodeFixedInputView.m */; }; - F7D4BF402CA2E8D800A5E746 /* TOPasscodeButtonLabel.m in Sources */ = {isa = PBXBuildFile; fileRef = F7D4BF1C2CA2E8D800A5E746 /* TOPasscodeButtonLabel.m */; }; - F7D4BF412CA2E8D800A5E746 /* TOPasscodeViewControllerAnimatedTransitioning.m in Sources */ = {isa = PBXBuildFile; fileRef = F7D4BF072CA2E8D800A5E746 /* TOPasscodeViewControllerAnimatedTransitioning.m */; }; - F7D4BF422CA2E8D800A5E746 /* TOPasscodeSettingsViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = F7D4BF282CA2E8D800A5E746 /* TOPasscodeSettingsViewController.m */; }; - F7D4BF432CA2E8D800A5E746 /* TOPasscodeCircleImage.m in Sources */ = {isa = PBXBuildFile; fileRef = F7D4BF032CA2E8D800A5E746 /* TOPasscodeCircleImage.m */; }; - F7D4BF442CA2E8D800A5E746 /* TOPasscodeView.m in Sources */ = {isa = PBXBuildFile; fileRef = F7D4BF122CA2E8D800A5E746 /* TOPasscodeView.m */; }; - F7D4BF452CA2E8D800A5E746 /* TOPasscodeCircleButton.m in Sources */ = {isa = PBXBuildFile; fileRef = F7D4BF0E2CA2E8D800A5E746 /* TOPasscodeCircleButton.m */; }; - F7D4BF462CA2E8D800A5E746 /* TOPasscodeInputField.m in Sources */ = {isa = PBXBuildFile; fileRef = F7D4BF222CA2E8D800A5E746 /* TOPasscodeInputField.m */; }; - F7D4BF472CA2E8D800A5E746 /* TOSettingsKeypadImage.m in Sources */ = {isa = PBXBuildFile; fileRef = F7D4BF092CA2E8D800A5E746 /* TOSettingsKeypadImage.m */; }; - F7D4BF482CA2E8D800A5E746 /* TOPasscodeSettingsWarningLabel.m in Sources */ = {isa = PBXBuildFile; fileRef = F7D4BF192CA2E8D800A5E746 /* TOPasscodeSettingsWarningLabel.m */; }; - F7D4BF492CA2E8D800A5E746 /* TOPasscodeVariableInputView.m in Sources */ = {isa = PBXBuildFile; fileRef = F7D4BF242CA2E8D800A5E746 /* TOPasscodeVariableInputView.m */; }; - F7D4BF4A2CA2E8D800A5E746 /* TOPasscodeCircleView.m in Sources */ = {isa = PBXBuildFile; fileRef = F7D4BF1E2CA2E8D800A5E746 /* TOPasscodeCircleView.m */; }; - F7D4BF4B2CA2E8D800A5E746 /* TOPasscodeViewContentLayout.m in Sources */ = {isa = PBXBuildFile; fileRef = F7D4BF052CA2E8D800A5E746 /* TOPasscodeViewContentLayout.m */; }; - F7D4BF4C2CA2E8D800A5E746 /* TOPasscodeSettingsKeypadButton.m in Sources */ = {isa = PBXBuildFile; fileRef = F7D4BF152CA2E8D800A5E746 /* TOPasscodeSettingsKeypadButton.m */; }; - F7D4BF4D2CA2E8D800A5E746 /* TOPasscodeViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = F7D4BF2A2CA2E8D800A5E746 /* TOPasscodeViewController.m */; }; - F7D4BF542CA2ED9D00A5E746 /* VLCKitSPM in Frameworks */ = {isa = PBXBuildFile; productRef = F7D4BF532CA2ED9D00A5E746 /* VLCKitSPM */; }; F7D56B1A2972405500FA46C4 /* Mantis in Frameworks */ = {isa = PBXBuildFile; productRef = F7D56B192972405500FA46C4 /* Mantis */; }; F7D60CAF2C941ACB008FBFDD /* NCMediaPinchGesture.swift in Sources */ = {isa = PBXBuildFile; fileRef = F7D60CAE2C941ACB008FBFDD /* NCMediaPinchGesture.swift */; }; F7D61E932EBF1366007F865B /* UIColor+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = F70CEF5523E9C7E50007035B /* UIColor+Extension.swift */; }; @@ -1118,6 +1079,7 @@ 87768C6AB8E040219DF85898 /* NCPasscodeModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 131F8E2F43AD414E9DB53019 /* NCPasscodeModel.swift */; }; 7670DABED44E4D56AD99E6DC /* NCPasscodeView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F902B24E44354B6D9F329F12 /* NCPasscodeView.swift */; }; 9310075861174A62B1C9FED6 /* NCPasscodeView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F902B24E44354B6D9F329F12 /* NCPasscodeView.swift */; }; + D56DFFE5D5014F66A7C9FED0 /* PasscodeUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = BC6C501EF1104A6A8569D9C1 /* PasscodeUITests.swift */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -1510,7 +1472,6 @@ F36C514D2E89393C0097E5F7 /* UIView+BlurVibrancy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIView+BlurVibrancy.swift"; sourceTree = ""; }; F36E64F62B9245210085ABB5 /* NCCollectionViewCommon+SelectTabBarDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NCCollectionViewCommon+SelectTabBarDelegate.swift"; sourceTree = ""; }; F37208742BAB4AB0006B5430 /* TestConstants.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TestConstants.swift; sourceTree = ""; }; - F3754A7C2CF87D600009312E /* SetupPasscodeView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SetupPasscodeView.swift; sourceTree = ""; }; F389C9F42CEE383300049762 /* SelectAlbumView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SelectAlbumView.swift; sourceTree = ""; }; F38F71242B6BBDC300473CDC /* NCCollectionViewCommonSelectTabBar.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NCCollectionViewCommonSelectTabBar.swift; sourceTree = ""; }; F39170A82CB8201B006127BC /* FileAutoRenamer+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "FileAutoRenamer+Extensions.swift"; sourceTree = ""; }; @@ -1629,7 +1590,6 @@ F7346E2028B0FA3A006CE2D2 /* Widget-Brinding-header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Widget-Brinding-header.h"; sourceTree = ""; }; F7346E2228B0FEBA006CE2D2 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; F7362A1E220C853A005101B5 /* LaunchScreen.storyboard */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.storyboard; path = LaunchScreen.storyboard; sourceTree = ""; }; - F737DA9C2B7B893C0063BAFC /* NCPasscode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NCPasscode.swift; sourceTree = ""; }; F7381EDA218218C9000B1560 /* NCOffline.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NCOffline.swift; sourceTree = ""; }; F7381EDE218218C9000B1560 /* NCOffline.storyboard */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.storyboard; path = NCOffline.storyboard; sourceTree = ""; }; F7386E452DA90E02009A00F6 /* NCAppVersionManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NCAppVersionManager.swift; sourceTree = ""; }; @@ -2078,42 +2038,6 @@ F7CF067A2E0FF38F0063AD04 /* NCAppStateManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NCAppStateManager.swift; sourceTree = ""; }; F7CF06822E11273F0063AD04 /* NCManageDatabase+CreateMetadata.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NCManageDatabase+CreateMetadata.swift"; sourceTree = ""; }; F7D1611F23CF19E30039EBBF /* NCViewerRichWorkspace.storyboard */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; path = NCViewerRichWorkspace.storyboard; sourceTree = ""; }; - F7D4BF002CA1831600A5E746 /* NCCollectionViewCommonPinchGesture.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NCCollectionViewCommonPinchGesture.swift; sourceTree = ""; }; - F7D4BF022CA2E8D800A5E746 /* TOPasscodeCircleImage.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = TOPasscodeCircleImage.h; sourceTree = ""; }; - F7D4BF032CA2E8D800A5E746 /* TOPasscodeCircleImage.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = TOPasscodeCircleImage.m; sourceTree = ""; }; - F7D4BF042CA2E8D800A5E746 /* TOPasscodeViewContentLayout.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = TOPasscodeViewContentLayout.h; sourceTree = ""; }; - F7D4BF052CA2E8D800A5E746 /* TOPasscodeViewContentLayout.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = TOPasscodeViewContentLayout.m; sourceTree = ""; }; - F7D4BF062CA2E8D800A5E746 /* TOPasscodeViewControllerAnimatedTransitioning.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = TOPasscodeViewControllerAnimatedTransitioning.h; sourceTree = ""; }; - F7D4BF072CA2E8D800A5E746 /* TOPasscodeViewControllerAnimatedTransitioning.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = TOPasscodeViewControllerAnimatedTransitioning.m; sourceTree = ""; }; - F7D4BF082CA2E8D800A5E746 /* TOSettingsKeypadImage.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = TOSettingsKeypadImage.h; sourceTree = ""; }; - F7D4BF092CA2E8D800A5E746 /* TOSettingsKeypadImage.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = TOSettingsKeypadImage.m; sourceTree = ""; }; - F7D4BF0B2CA2E8D800A5E746 /* TOPasscodeViewControllerConstants.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = TOPasscodeViewControllerConstants.h; sourceTree = ""; }; - F7D4BF0D2CA2E8D800A5E746 /* TOPasscodeCircleButton.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = TOPasscodeCircleButton.h; sourceTree = ""; }; - F7D4BF0E2CA2E8D800A5E746 /* TOPasscodeCircleButton.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = TOPasscodeCircleButton.m; sourceTree = ""; }; - F7D4BF0F2CA2E8D800A5E746 /* TOPasscodeKeypadView.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = TOPasscodeKeypadView.h; sourceTree = ""; }; - F7D4BF102CA2E8D800A5E746 /* TOPasscodeKeypadView.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = TOPasscodeKeypadView.m; sourceTree = ""; }; - F7D4BF112CA2E8D800A5E746 /* TOPasscodeView.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = TOPasscodeView.h; sourceTree = ""; }; - F7D4BF122CA2E8D800A5E746 /* TOPasscodeView.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = TOPasscodeView.m; sourceTree = ""; }; - F7D4BF142CA2E8D800A5E746 /* TOPasscodeSettingsKeypadButton.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = TOPasscodeSettingsKeypadButton.h; sourceTree = ""; }; - F7D4BF152CA2E8D800A5E746 /* TOPasscodeSettingsKeypadButton.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = TOPasscodeSettingsKeypadButton.m; sourceTree = ""; }; - F7D4BF162CA2E8D800A5E746 /* TOPasscodeSettingsKeypadView.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = TOPasscodeSettingsKeypadView.h; sourceTree = ""; }; - F7D4BF172CA2E8D800A5E746 /* TOPasscodeSettingsKeypadView.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = TOPasscodeSettingsKeypadView.m; sourceTree = ""; }; - F7D4BF182CA2E8D800A5E746 /* TOPasscodeSettingsWarningLabel.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = TOPasscodeSettingsWarningLabel.h; sourceTree = ""; }; - F7D4BF192CA2E8D800A5E746 /* TOPasscodeSettingsWarningLabel.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = TOPasscodeSettingsWarningLabel.m; sourceTree = ""; }; - F7D4BF1B2CA2E8D800A5E746 /* TOPasscodeButtonLabel.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = TOPasscodeButtonLabel.h; sourceTree = ""; }; - F7D4BF1C2CA2E8D800A5E746 /* TOPasscodeButtonLabel.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = TOPasscodeButtonLabel.m; sourceTree = ""; }; - F7D4BF1D2CA2E8D800A5E746 /* TOPasscodeCircleView.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = TOPasscodeCircleView.h; sourceTree = ""; }; - F7D4BF1E2CA2E8D800A5E746 /* TOPasscodeCircleView.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = TOPasscodeCircleView.m; sourceTree = ""; }; - F7D4BF1F2CA2E8D800A5E746 /* TOPasscodeFixedInputView.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = TOPasscodeFixedInputView.h; sourceTree = ""; }; - F7D4BF202CA2E8D800A5E746 /* TOPasscodeFixedInputView.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = TOPasscodeFixedInputView.m; sourceTree = ""; }; - F7D4BF212CA2E8D800A5E746 /* TOPasscodeInputField.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = TOPasscodeInputField.h; sourceTree = ""; }; - F7D4BF222CA2E8D800A5E746 /* TOPasscodeInputField.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = TOPasscodeInputField.m; sourceTree = ""; }; - F7D4BF232CA2E8D800A5E746 /* TOPasscodeVariableInputView.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = TOPasscodeVariableInputView.h; sourceTree = ""; }; - F7D4BF242CA2E8D800A5E746 /* TOPasscodeVariableInputView.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = TOPasscodeVariableInputView.m; sourceTree = ""; }; - F7D4BF272CA2E8D800A5E746 /* TOPasscodeSettingsViewController.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = TOPasscodeSettingsViewController.h; sourceTree = ""; }; - F7D4BF282CA2E8D800A5E746 /* TOPasscodeSettingsViewController.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = TOPasscodeSettingsViewController.m; sourceTree = ""; }; - F7D4BF292CA2E8D800A5E746 /* TOPasscodeViewController.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = TOPasscodeViewController.h; sourceTree = ""; }; - F7D4BF2A2CA2E8D800A5E746 /* TOPasscodeViewController.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = TOPasscodeViewController.m; sourceTree = ""; }; F7D532461F5D4123006568B1 /* is */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = is; path = is.lproj/Localizable.strings; sourceTree = ""; }; F7D5324D1F5D4137006568B1 /* nl */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = nl; path = nl.lproj/Localizable.strings; sourceTree = ""; }; F7D532541F5D4155006568B1 /* sk-SK */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "sk-SK"; path = "sk-SK.lproj/Localizable.strings"; sourceTree = ""; }; @@ -2167,6 +2091,7 @@ F7FFFC9D2FB300440015441E /* NCAssistantSharedTextStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NCAssistantSharedTextStore.swift; sourceTree = ""; }; 131F8E2F43AD414E9DB53019 /* NCPasscodeModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NCPasscodeModel.swift; sourceTree = ""; }; F902B24E44354B6D9F329F12 /* NCPasscodeView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NCPasscodeView.swift; sourceTree = ""; }; + BC6C501EF1104A6A8569D9C1 /* PasscodeUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PasscodeUITests.swift; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFileSystemSynchronizedRootGroup section */ @@ -2277,7 +2202,6 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( - F7D4BF542CA2ED9D00A5E746 /* VLCKitSPM in Frameworks */, F7D56B1A2972405500FA46C4 /* Mantis in Frameworks */, F7ED547C25EEA65400956C55 /* QRCodeReader in Frameworks */, F788ECC7263AAAFA00ADC67F /* MarkdownKit in Frameworks */, @@ -2449,6 +2373,7 @@ isa = PBXGroup; children = ( F3374A952D6744A4002A38F9 /* BaseUIXCTestCase.swift */, + BC6C501EF1104A6A8569D9C1 /* PasscodeUITests.swift */, AABD0C882D5F631600F009E6 /* Extensions */, AA3C85E92D36BBDE00F74F12 /* UITestBackend */, AA8E03DB2D2FBAAD00E7E89C /* DownloadLimitUITests.swift */, @@ -2587,7 +2512,6 @@ F7A03E342D427308007AA677 /* NCMainNavigationController.swift */, F794E13C2BBBFF2E003693D7 /* NCMainTabBarController.swift */, F75A64A3307DFB9E0059B78E /* NCMainTabBarController+Sidebar.swift */, - F737DA9C2B7B893C0063BAFC /* NCPasscode.swift */, F77444F7222816D5000D5EB0 /* NCPickerViewController.swift */, F75A64A1307DFB870059B78E /* NCSidebarView.swift */, ); @@ -3143,7 +3067,6 @@ F7CCAB502ECF315F00F8E68B /* NCCollectionViewCommon+SyncMetadata.swift */, F721C5092FB6F9AA00207DA9 /* NCCollectionViewCommon+TransitionSourceBlink.swift */, F76995F32F9A4AC000291FA7 /* NCCollectionViewCommon+UIEditMenuInteractionDelegate.swift */, - F7D4BF002CA1831600A5E746 /* NCCollectionViewCommonPinchGesture.swift */, F38F71242B6BBDC300473CDC /* NCCollectionViewCommonSelectTabBar.swift */, F7C1EEA425053A9C00866ACC /* NCCollectionViewDataSource.swift */, F78ACD50219046AC0088454D /* Section Header Footer */, @@ -3234,7 +3157,6 @@ children = ( F76882112C0DD1E7001CF441 /* NCSettingsModel.swift */, F768820C2C0DD1E7001CF441 /* NCSettingsView.swift */, - F3754A7C2CF87D600009312E /* SetupPasscodeView.swift */, ); path = Settings; sourceTree = ""; @@ -3647,7 +3569,6 @@ F3E173BE2C9B1057006D177A /* ScreenAwakeManager */, F71F6D062B6A6A5E00F1EB15 /* ThreadSafeArray.swift */, F7245923289BB50B00474787 /* ThreadSafeDictionary.swift */, - F7D4BF2B2CA2E8D800A5E746 /* TOPasscodeViewController */, ); path = Utility; sourceTree = ""; @@ -3737,96 +3658,6 @@ path = RichWorkspace; sourceTree = ""; }; - F7D4BF0A2CA2E8D800A5E746 /* Models */ = { - isa = PBXGroup; - children = ( - F7D4BF022CA2E8D800A5E746 /* TOPasscodeCircleImage.h */, - F7D4BF032CA2E8D800A5E746 /* TOPasscodeCircleImage.m */, - F7D4BF042CA2E8D800A5E746 /* TOPasscodeViewContentLayout.h */, - F7D4BF052CA2E8D800A5E746 /* TOPasscodeViewContentLayout.m */, - F7D4BF062CA2E8D800A5E746 /* TOPasscodeViewControllerAnimatedTransitioning.h */, - F7D4BF072CA2E8D800A5E746 /* TOPasscodeViewControllerAnimatedTransitioning.m */, - F7D4BF082CA2E8D800A5E746 /* TOSettingsKeypadImage.h */, - F7D4BF092CA2E8D800A5E746 /* TOSettingsKeypadImage.m */, - ); - path = Models; - sourceTree = ""; - }; - F7D4BF0C2CA2E8D800A5E746 /* Supporting */ = { - isa = PBXGroup; - children = ( - F7D4BF0B2CA2E8D800A5E746 /* TOPasscodeViewControllerConstants.h */, - ); - path = Supporting; - sourceTree = ""; - }; - F7D4BF132CA2E8D800A5E746 /* Main */ = { - isa = PBXGroup; - children = ( - F7D4BF0D2CA2E8D800A5E746 /* TOPasscodeCircleButton.h */, - F7D4BF0E2CA2E8D800A5E746 /* TOPasscodeCircleButton.m */, - F7D4BF0F2CA2E8D800A5E746 /* TOPasscodeKeypadView.h */, - F7D4BF102CA2E8D800A5E746 /* TOPasscodeKeypadView.m */, - F7D4BF112CA2E8D800A5E746 /* TOPasscodeView.h */, - F7D4BF122CA2E8D800A5E746 /* TOPasscodeView.m */, - ); - path = Main; - sourceTree = ""; - }; - F7D4BF1A2CA2E8D800A5E746 /* Settings */ = { - isa = PBXGroup; - children = ( - F7D4BF142CA2E8D800A5E746 /* TOPasscodeSettingsKeypadButton.h */, - F7D4BF152CA2E8D800A5E746 /* TOPasscodeSettingsKeypadButton.m */, - F7D4BF162CA2E8D800A5E746 /* TOPasscodeSettingsKeypadView.h */, - F7D4BF172CA2E8D800A5E746 /* TOPasscodeSettingsKeypadView.m */, - F7D4BF182CA2E8D800A5E746 /* TOPasscodeSettingsWarningLabel.h */, - F7D4BF192CA2E8D800A5E746 /* TOPasscodeSettingsWarningLabel.m */, - ); - path = Settings; - sourceTree = ""; - }; - F7D4BF252CA2E8D800A5E746 /* Shared */ = { - isa = PBXGroup; - children = ( - F7D4BF1B2CA2E8D800A5E746 /* TOPasscodeButtonLabel.h */, - F7D4BF1C2CA2E8D800A5E746 /* TOPasscodeButtonLabel.m */, - F7D4BF1D2CA2E8D800A5E746 /* TOPasscodeCircleView.h */, - F7D4BF1E2CA2E8D800A5E746 /* TOPasscodeCircleView.m */, - F7D4BF1F2CA2E8D800A5E746 /* TOPasscodeFixedInputView.h */, - F7D4BF202CA2E8D800A5E746 /* TOPasscodeFixedInputView.m */, - F7D4BF212CA2E8D800A5E746 /* TOPasscodeInputField.h */, - F7D4BF222CA2E8D800A5E746 /* TOPasscodeInputField.m */, - F7D4BF232CA2E8D800A5E746 /* TOPasscodeVariableInputView.h */, - F7D4BF242CA2E8D800A5E746 /* TOPasscodeVariableInputView.m */, - ); - path = Shared; - sourceTree = ""; - }; - F7D4BF262CA2E8D800A5E746 /* Views */ = { - isa = PBXGroup; - children = ( - F7D4BF132CA2E8D800A5E746 /* Main */, - F7D4BF1A2CA2E8D800A5E746 /* Settings */, - F7D4BF252CA2E8D800A5E746 /* Shared */, - ); - path = Views; - sourceTree = ""; - }; - F7D4BF2B2CA2E8D800A5E746 /* TOPasscodeViewController */ = { - isa = PBXGroup; - children = ( - F7D4BF0A2CA2E8D800A5E746 /* Models */, - F7D4BF0C2CA2E8D800A5E746 /* Supporting */, - F7D4BF262CA2E8D800A5E746 /* Views */, - F7D4BF272CA2E8D800A5E746 /* TOPasscodeSettingsViewController.h */, - F7D4BF282CA2E8D800A5E746 /* TOPasscodeSettingsViewController.m */, - F7D4BF292CA2E8D800A5E746 /* TOPasscodeViewController.h */, - F7D4BF2A2CA2E8D800A5E746 /* TOPasscodeViewController.m */, - ); - path = TOPasscodeViewController; - sourceTree = ""; - }; F7DFB7E9219C5A0500680748 /* Create */ = { isa = PBXGroup; children = ( @@ -4314,7 +4145,6 @@ F7160A812BE933390034DCB3 /* RealmSwift */, F33EE6E02BF4BDA500CA1A51 /* NIOSSL */, F33EE6EF2BF4C0FF00CA1A51 /* NIO */, - F7D4BF532CA2ED9D00A5E746 /* VLCKitSPM */, F3374AEF2D78B01B002A38F9 /* BitCollections */, F3374AF12D78B01B002A38F9 /* Collections */, F3374AF32D78B01B002A38F9 /* DequeModule */, @@ -4545,7 +4375,6 @@ F3A0479C2BD268B500658E7B /* XCRemoteSwiftPackageReference "PopupView" */, F33EE6DF2BF4BDA500CA1A51 /* XCRemoteSwiftPackageReference "swift-nio-ssl" */, F33EE6EE2BF4C0FF00CA1A51 /* XCRemoteSwiftPackageReference "swift-nio" */, - F7D4BF4E2CA2ECCB00A5E746 /* XCRemoteSwiftPackageReference "vlckit-spm" */, F3374AEE2D78B01B002A38F9 /* XCRemoteSwiftPackageReference "swift-collections" */, F70557B52ED44E2700135623 /* XCRemoteSwiftPackageReference "LucidBanner" */, F76BED72306CE862002E1442 /* XCRemoteSwiftPackageReference "GRDB" */, @@ -4902,6 +4731,7 @@ AAA7BC2E2D3E39F1008F1A22 /* CapabilitiesResponse.swift in Sources */, AA3C85F22D394B3A00F74F12 /* DownloadLimitResponse.swift in Sources */, F3374A962D6744A4002A38F9 /* BaseUIXCTestCase.swift in Sources */, + D56DFFE5D5014F66A7C9FED0 /* PasscodeUITests.swift in Sources */, F37208812BAB5979006B5430 /* TestConstants.swift in Sources */, F33D303E2D8B129600531D64 /* AutoUploadUITests.swift in Sources */, ); @@ -5007,32 +4837,15 @@ F711A4DF2AF92CAE00095DD8 /* NCUtility+Date.swift in Sources */, F78295311F962EFA00A572F5 /* NCEndToEndEncryption.m in Sources */, F7C30DFE291BD0B80017149B /* NCNetworkingE2EEDelete.swift in Sources */, - F7D4BF2C2CA2E8D800A5E746 /* TOPasscodeKeypadView.m in Sources */, F7C630802FFE4F8000257EEB /* UIWindowScene+Extension.swift in Sources */, - F7D4BF2D2CA2E8D800A5E746 /* TOPasscodeSettingsKeypadView.m in Sources */, - F7D4BF2E2CA2E8D800A5E746 /* TOPasscodeFixedInputView.m in Sources */, F7D61EA92EBF1694007F865B /* NCManageDatabase+TableCapabilities.swift in Sources */, - F7D4BF2F2CA2E8D800A5E746 /* TOPasscodeButtonLabel.m in Sources */, - F7D4BF302CA2E8D800A5E746 /* TOPasscodeViewControllerAnimatedTransitioning.m in Sources */, - F7D4BF312CA2E8D800A5E746 /* TOPasscodeSettingsViewController.m in Sources */, F71916122E2901FB00E13E96 /* NCNetworking+Upload.swift in Sources */, F73BC74F2F23811E003170C2 /* WarningBannerView.swift in Sources */, - F7D4BF322CA2E8D800A5E746 /* TOPasscodeCircleImage.m in Sources */, F7CAFE192F168F6000DB35A5 /* NCDebouncer.swift in Sources */, - F7D4BF332CA2E8D800A5E746 /* TOPasscodeView.m in Sources */, F77E8C252E79717D00EAE68F /* NCManageDatabase+LivePhoto.swift in Sources */, - F7D4BF342CA2E8D800A5E746 /* TOPasscodeCircleButton.m in Sources */, - F7D4BF352CA2E8D800A5E746 /* TOPasscodeInputField.m in Sources */, - F7D4BF362CA2E8D800A5E746 /* TOSettingsKeypadImage.m in Sources */, - F7D4BF372CA2E8D800A5E746 /* TOPasscodeSettingsWarningLabel.m in Sources */, - F7D4BF382CA2E8D800A5E746 /* TOPasscodeVariableInputView.m in Sources */, - F7D4BF392CA2E8D800A5E746 /* TOPasscodeCircleView.m in Sources */, - F7D4BF3A2CA2E8D800A5E746 /* TOPasscodeViewContentLayout.m in Sources */, F79792482F5EECE100FE9544 /* Font+Extension.swift in Sources */, F751760B307F53680057C9B0 /* NCMediaViewerTransitionSource.swift in Sources */, F7D61EBF2EBF1878007F865B /* NCManageDatabase+Tag.swift in Sources */, - F7D4BF3B2CA2E8D800A5E746 /* TOPasscodeSettingsKeypadButton.m in Sources */, - F7D4BF3C2CA2E8D800A5E746 /* TOPasscodeViewController.m in Sources */, F74AF3A5247FB6AE00AC767B /* NCUtilityFileSystem.swift in Sources */, AF1A9B6527D0CC0500F17A9E /* UIAlertController+Extension.swift in Sources */, AF22B206277B4E4C00DAB0CC /* NCCreateFormUploadConflict.swift in Sources */, @@ -5074,7 +4887,6 @@ F757CC8529E7F88B00F31428 /* NCManageDatabase+Groupfolders.swift in Sources */, F768823A2C0DD230001CF441 /* NCPreferences.swift in Sources */, A1E2EE010000000000000004 /* NCEndToEndKeySet.swift in Sources */, - F737DA9E2B7B893C0063BAFC /* NCPasscode.swift in Sources */, 87768C6AB8E040219DF85898 /* NCPasscodeModel.swift in Sources */, 9310075861174A62B1C9FED6 /* NCPasscodeView.swift in Sources */, F7C30DF7291BC0D30017149B /* NCNetworkingE2EEUpload.swift in Sources */, @@ -5250,7 +5062,6 @@ F768822C2C0DD1E7001CF441 /* NCPreferences.swift in Sources */, A1E2EE010000000000000007 /* NCEndToEndKeySet.swift in Sources */, F7CAFE1D2F17A35F00DB35A5 /* ProgressQuantizer.swift in Sources */, - F3754A7D2CF87D600009312E /* SetupPasscodeView.swift in Sources */, F73EF7D72B0226080087E6E9 /* NCManageDatabase+Tip.swift in Sources */, F3374A842D64AC31002A38F9 /* AssistantLabelStyle.swift in Sources */, F74BAE172C7E2F4E0028D4FA /* FileProviderDomain.swift in Sources */, @@ -5348,30 +5159,13 @@ F77A6B30301112C5007103E6 /* NCMediaSectionHeader.swift in Sources */, F794E13D2BBBFF2E003693D7 /* NCMainTabBarController.swift in Sources */, F7CBC1252BAC8B0000EC1D55 /* NCSectionFirstHeaderEmptyData.swift in Sources */, - F7D4BF3D2CA2E8D800A5E746 /* TOPasscodeKeypadView.m in Sources */, - F7D4BF3E2CA2E8D800A5E746 /* TOPasscodeSettingsKeypadView.m in Sources */, F7132C722D085AD200B42D6A /* NCTermOfServiceModel.swift in Sources */, F7132C732D085AD200B42D6A /* NCTermOfServiceView.swift in Sources */, F7A08A7C3017441D00470AD3 /* NCMedia+SelectDelegate.swift in Sources */, - F7D4BF3F2CA2E8D800A5E746 /* TOPasscodeFixedInputView.m in Sources */, - F7D4BF402CA2E8D800A5E746 /* TOPasscodeButtonLabel.m in Sources */, F70898692EDDB51700EF85BD /* NCSelectOpen+SelectDelegate.swift in Sources */, - F7D4BF412CA2E8D800A5E746 /* TOPasscodeViewControllerAnimatedTransitioning.m in Sources */, - F7D4BF422CA2E8D800A5E746 /* TOPasscodeSettingsViewController.m in Sources */, - F7D4BF432CA2E8D800A5E746 /* TOPasscodeCircleImage.m in Sources */, F78026102E9CFA3700B63436 /* NCTransfersView.swift in Sources */, - F7D4BF442CA2E8D800A5E746 /* TOPasscodeView.m in Sources */, - F7D4BF452CA2E8D800A5E746 /* TOPasscodeCircleButton.m in Sources */, - F7D4BF462CA2E8D800A5E746 /* TOPasscodeInputField.m in Sources */, - F7D4BF472CA2E8D800A5E746 /* TOSettingsKeypadImage.m in Sources */, - F7D4BF482CA2E8D800A5E746 /* TOPasscodeSettingsWarningLabel.m in Sources */, F3A0B1F12EC76FE800F10B82 /* NCUserStatusModel.swift in Sources */, F3A0B1F22EC76FE800F10B82 /* NCUserStatusView.swift in Sources */, - F7D4BF492CA2E8D800A5E746 /* TOPasscodeVariableInputView.m in Sources */, - F7D4BF4A2CA2E8D800A5E746 /* TOPasscodeCircleView.m in Sources */, - F7D4BF4B2CA2E8D800A5E746 /* TOPasscodeViewContentLayout.m in Sources */, - F7D4BF4C2CA2E8D800A5E746 /* TOPasscodeSettingsKeypadButton.m in Sources */, - F7D4BF4D2CA2E8D800A5E746 /* TOPasscodeViewController.m in Sources */, AAE330042D2ED20200B04903 /* NCShareNavigationTitleSetting.swift in Sources */, F71CFA672F2A07DB007A3AE9 /* NCMediaNetwork.swift in Sources */, F7DF7B422F1A36C100514020 /* HelperBanner.swift in Sources */, @@ -5445,7 +5239,6 @@ F71F6D072B6A6A5E00F1EB15 /* ThreadSafeArray.swift in Sources */, F761856C29E98543006EB3B0 /* NCIntroCollectionViewCell.swift in Sources */, F75DD765290ABB25002EB562 /* Intent.intentdefinition in Sources */, - F7D4BF012CA1831900A5E746 /* NCCollectionViewCommonPinchGesture.swift in Sources */, F74B6D952A7E239A00F03C5F /* NCManageDatabase+Chunk.swift in Sources */, F702F2F725EE5CED008F8E80 /* NCLogin.swift in Sources */, F75D90212D2BE26F003E740B /* NCRecommendationsCell.swift in Sources */, @@ -5590,7 +5383,6 @@ F79792472F5EECE100FE9544 /* Font+Extension.swift in Sources */, F768822A2C0DD1E7001CF441 /* NCSettingsModel.swift in Sources */, F7BCE48E301091EA00876F4D /* NCMediaDatePickerViewController.swift in Sources */, - F737DA9D2B7B893C0063BAFC /* NCPasscode.swift in Sources */, 9F0E3B428AB44A6480190C2D /* NCPasscodeModel.swift in Sources */, 7670DABED44E4D56AD99E6DC /* NCPasscodeView.swift in Sources */, F77C97392953131000FDDD09 /* NCCameraRoll.swift in Sources */, @@ -7319,14 +7111,6 @@ minimumVersion = 1.0.0; }; }; - F7D4BF4E2CA2ECCB00A5E746 /* XCRemoteSwiftPackageReference "vlckit-spm" */ = { - isa = XCRemoteSwiftPackageReference; - repositoryURL = "https://github.com/tylerjonesio/vlckit-spm"; - requirement = { - kind = upToNextMajorVersion; - minimumVersion = 3.5.1; - }; - }; F7D56B182972405400FA46C4 /* XCRemoteSwiftPackageReference "Mantis" */ = { isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/marinofaggiana/Mantis"; @@ -7729,11 +7513,6 @@ package = F783034028B511D200B84583 /* XCRemoteSwiftPackageReference "NextcloudKit" */; productName = NextcloudKit; }; - F7D4BF532CA2ED9D00A5E746 /* VLCKitSPM */ = { - isa = XCSwiftPackageProductDependency; - package = F7D4BF4E2CA2ECCB00A5E746 /* XCRemoteSwiftPackageReference "vlckit-spm" */; - productName = VLCKitSPM; - }; F7D56B192972405500FA46C4 /* Mantis */ = { isa = XCSwiftPackageProductDependency; package = F7D56B182972405400FA46C4 /* XCRemoteSwiftPackageReference "Mantis" */; diff --git a/Nextcloud.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/Nextcloud.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index 87db5d5f5e..7b5e49729a 100644 --- a/Nextcloud.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/Nextcloud.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -1,5 +1,5 @@ { - "originHash" : "7880536aedfe5f5b326df402ac666a6126ac1b325106ab3eb7b6c78a6ef725ff", + "originHash" : "e8140d7bd4476541d44ce4bf03cb73841973e05cd3c93172bf90ff7d869ccd81", "pins" : [ { "identity" : "abseil-cpp-binary", @@ -360,15 +360,6 @@ "revision" : "91d7bca65721928af30c6a2bb1a1f517f7862964", "version" : "2.1.21" } - }, - { - "identity" : "vlckit-spm", - "kind" : "remoteSourceControl", - "location" : "https://github.com/tylerjonesio/vlckit-spm", - "state" : { - "revision" : "e932bbd488872fdb74f6654d28c2f291eae03daf", - "version" : "3.6.0" - } } ], "version" : 3 diff --git a/REUSE.toml b/REUSE.toml index 5504703419..976a6d6713 100644 --- a/REUSE.toml +++ b/REUSE.toml @@ -61,12 +61,6 @@ precedence = "aggregate" SPDX-FileCopyrightText = "Nextcloud GmbH" SPDX-License-Identifier = "LicenseRef-NextcloudTrademarks" -[[annotations]] -path = ["iOSClient/Utility/TOPasscodeViewController/**"] -precedence = "aggregate" -SPDX-FileCopyrightText = "2017 Timothy Oliver" -SPDX-License-Identifier = "MIT" - [[annotations]] path = ["iOSClient/Utility/NYMnemonic/**"] precedence = "aggregate" diff --git a/Share/NCShareExtension.swift b/Share/NCShareExtension.swift index 65cf8eb0b7..8506639df6 100644 --- a/Share/NCShareExtension.swift +++ b/Share/NCShareExtension.swift @@ -53,6 +53,8 @@ class NCShareExtension: UIViewController { var banner: LucidBanner? var sceneIdentifier: String = UUID().uuidString private var isPresentingNoAccountAlert = false + private var isPresentingPasscode = false + private var isPasscodeAuthenticated = false // MARK: - View Life Cycle @@ -110,11 +112,8 @@ class NCShareExtension: UIViewController { return } - if NCPreferences().presentPasscode { - NCPasscode.shared.presentPasscode(viewController: self, delegate: self) { - NCPasscode.shared.enableTouchFaceID() - } - } + self.isPasscodeAuthenticated = false + self.requestPasscodeIfNeeded() } // Verify version @@ -165,11 +164,7 @@ class NCShareExtension: UIViewController { } } - if NCPreferences().presentPasscode { - NCPasscode.shared.presentPasscode(viewController: self, delegate: self) { - NCPasscode.shared.enableTouchFaceID() - } - } + self.requestPasscodeIfNeeded() self.collectionView.reloadData() } @@ -189,7 +184,7 @@ class NCShareExtension: UIViewController { } private func validateAccount() -> Bool { - guard !NCPasscode.shared.isPasscodeReset else { + guard !NCPasscodeModel.isPasscodeReset else { return showNoAccountAlert() } @@ -575,18 +570,21 @@ extension NCShareExtension { } } -extension NCShareExtension: NCPasscodeDelegate { - func passcodeReset(_ passcodeViewController: TOPasscodeViewController) { - DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { - passcodeViewController.dismiss(animated: false) - self.cancel(with: .noAccount) - } - } - - func evaluatePolicy(_ passcodeViewController: TOPasscodeViewController, isCorrectCode: Bool) { - if !isCorrectCode { - DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { - passcodeViewController.dismiss(animated: false) +extension NCShareExtension { + /// Require authentication once per foreground session without reopening after dismissal. + private func requestPasscodeIfNeeded() { + guard NCPreferences().presentPasscode, + !isPasscodeAuthenticated, + !isPresentingPasscode else { return } + isPresentingPasscode = true + NCPasscodeView.present(from: self) { [weak self] result in + guard let self else { return } + self.isPresentingPasscode = false + switch result { + case .authenticated: + self.isPasscodeAuthenticated = true + _ = self.validateAccount() + case .cancelled, .reset, .configured, .disabled: self.cancel(with: .noAccount) } } diff --git a/Share/Share-Bridging-Header.h b/Share/Share-Bridging-Header.h index dd577bf8ef..68997c812e 100644 --- a/Share/Share-Bridging-Header.h +++ b/Share/Share-Bridging-Header.h @@ -6,4 +6,3 @@ #import "NCEndToEndEncryption.h" #import "UIImage+animatedGIF.h" -#import "TOPasscodeViewController.h" diff --git a/Tests/NextcloudUITests/PasscodeUITests.swift b/Tests/NextcloudUITests/PasscodeUITests.swift new file mode 100644 index 0000000000..c480630482 --- /dev/null +++ b/Tests/NextcloudUITests/PasscodeUITests.swift @@ -0,0 +1,250 @@ +// SPDX-FileCopyrightText: Nextcloud GmbH +// SPDX-FileCopyrightText: 2026 Marino Faggiana +// SPDX-License-Identifier: GPL-3.0-or-later + +import XCTest + +/// Exercises real settings and app-lock flows against the standard UI-test server. +@MainActor +final class PasscodeUITests: BaseUIXCTestCase { + private let originalCode = "135790" + private let replacementCode = "246801" + private let wrongCode = "000000" + private var currentCode: String? + private let languageArguments = ["-AppleLanguages", "(en)", "-AppleLocale", "en_US"] + + private var title: XCUIElement { app.staticTexts["passcode.title"] } + private var closeButton: XCUIElement { app.buttons["passcode.close"] } + private var lockout: XCUIElement { app.staticTexts["passcode.lockout.remaining"] } + private var lockSetting: XCUIElement { app.buttons["settings.passcode.toggle"] } + private var changeSetting: XCUIElement { app.buttons["settings.passcode.change"] } + + override func setUp() async throws { + try await super.setUp() + continueAfterFailure = false + addUIInterruptionMonitor(withDescription: "Allow Notifications", for: "Allow") + addUIInterruptionMonitor(withDescription: "Save Password", for: "Not Now") + app = XCUIApplication() + app.launchArguments = ["UI_TESTING", "UI_TESTING_PASSCODE"] + languageArguments + app.launch() + try await logIn() + openSettings() + XCTAssertFalse(changeSetting.exists, "Each test must start with app lock disabled.") + } + + override func tearDown() async throws { + // Avoid leaving the simulator locked if an assertion interrupted a test. + if let currentCode, app != nil { + app.activate() + if lockout.exists { + _ = lockout.waitForNonExistence(timeout: 65) + } + if title.exists && !closeButton.exists { + clearPartialCode() + enter(currentCode) + } + if closeButton.exists { + closeButton.tap() + } + if !lockSetting.exists { + openSettings() + } + if changeSetting.exists { + lockSetting.tap() + enter(currentCode) + _ = title.waitForNonExistence(timeout: TestConstants.controlExistenceTimeout) + } + } + app?.terminate() + try await super.tearDown() + } + + func testCreateChangeAndDisablePasscode() { + createPasscode() + changeSetting.tap() + assertTitle("Enter Passcode") + enter(originalCode) + assertTitle("Enter a new passcode") + enter(replacementCode) + assertTitle("Confirm new passcode") + enter(replacementCode) + currentCode = replacementCode + XCTAssertTrue(title.waitForNonExistence(timeout: TestConstants.controlExistenceTimeout)) + + lockSetting.tap() + assertTitle("Enter Passcode") + enter(originalCode) + XCTAssertTrue(app.staticTexts["passcode.error"].waitForExistence(timeout: 5)) + XCTAssertEqual(app.staticTexts["passcode.error"].label, "Incorrect passcode. Try again.") + enter(replacementCode) + XCTAssertTrue(title.waitForNonExistence(timeout: TestConstants.controlExistenceTimeout)) + XCTAssertFalse(changeSetting.exists) + currentCode = nil + } + + /// Regression for the reported brute-force bypass when disabling app lock. + func testDisableLockoutSurvivesReopeningBackgroundAndRelaunch() { + createPasscode() + lockSetting.tap() + enter(wrongCode) + closeButton.tap() + XCTAssertTrue(title.waitForNonExistence(timeout: 5)) + lockSetting.tap() + enter(wrongCode) + closeButton.tap() + XCTAssertTrue(title.waitForNonExistence(timeout: 5)) + lockSetting.tap() + enter(wrongCode) + assertLockedOut() + let deadline = Date().addingTimeInterval(TimeInterval(remainingSeconds())) + + // Reopening must neither enable the keypad nor restart the countdown. + closeButton.tap() + XCTAssertTrue(title.waitForNonExistence(timeout: 5)) + lockSetting.tap() + assertLockedOut() + waitForCountdown(toDropBelow: 56) + assertOriginalDeadline(deadline) + + // Background cancels the optional request; foreground requires the mandatory lock. + backgroundAndActivate() + assertTitle("Enter Passcode") + XCTAssertFalse(closeButton.exists) + assertLockedOut() + assertOriginalDeadline(deadline) + + // Relaunch without setup flags: credentials and the deadline must persist in Keychain. + app.terminate() + app.launchArguments = languageArguments + app.launch() + assertTitle("Enter Passcode") + XCTAssertFalse(closeButton.exists) + assertLockedOut() + assertOriginalDeadline(deadline) + + XCTAssertTrue(lockout.waitForNonExistence(timeout: max(0, deadline.timeIntervalSinceNow) + 5)) + enter(originalCode) + XCTAssertTrue(title.waitForNonExistence(timeout: TestConstants.controlExistenceTimeout)) + } + + /// Changing the code must enforce the same persisted protection as disabling it. + func testChangePasscodeCannotResetFailedAttemptsByReopening() { + createPasscode() + for attempt in 1...3 { + changeSetting.tap() + assertTitle("Enter Passcode") + enter(wrongCode) + if attempt < 3 { + closeButton.tap() + XCTAssertTrue(title.waitForNonExistence(timeout: 5)) + } + } + assertLockedOut() + let deadline = Date().addingTimeInterval(TimeInterval(remainingSeconds())) + closeButton.tap() + XCTAssertTrue(title.waitForNonExistence(timeout: 5)) + changeSetting.tap() + assertLockedOut() + assertOriginalDeadline(deadline) + } + + func testBackgroundCannotBypassMandatoryAppLock() { + createPasscode() + backgroundAndActivate() + for _ in 0..<2 { + assertTitle("Enter Passcode") + XCTAssertFalse(closeButton.exists) + XCTAssertFalse(lockSetting.isHittable, "Protected settings must remain inaccessible.") + let start = app.coordinate(withNormalizedOffset: CGVector(dx: 0.5, dy: 0.25)) + let end = app.coordinate(withNormalizedOffset: CGVector(dx: 0.5, dy: 0.8)) + start.press(forDuration: 0.1, thenDragTo: end) + XCTAssertTrue(title.exists, "Dragging must not dismiss the mandatory lock.") + backgroundAndActivate() + } + enter(originalCode) + XCTAssertTrue(title.waitForNonExistence(timeout: TestConstants.controlExistenceTimeout)) + XCTAssertTrue(lockSetting.waitForExistence(timeout: 5), "Unlocking must preserve the Settings screen.") + } + + private func createPasscode() { + lockSetting.tap() + assertTitle("Enter a new passcode") + enter(originalCode) + assertTitle("Confirm new passcode") + enter(originalCode) + currentCode = originalCode + XCTAssertTrue(title.waitForNonExistence(timeout: TestConstants.controlExistenceTimeout)) + XCTAssertTrue(changeSetting.waitForExistence(timeout: 5)) + } + + private func openSettings() { + let sidebar = app.buttons["openSidebar"].firstMatch + XCTAssertTrue(sidebar.waitForExistence(timeout: TestConstants.controlExistenceTimeout)) + sidebar.tap() + let settings = app.buttons["sidebarSettings"] + XCTAssertTrue(settings.waitForExistence(timeout: 5)) + settings.tap() + XCTAssertTrue(lockSetting.waitForExistence(timeout: 5)) + } + + private func enter(_ code: String) { + for digit in code { + let button = app.buttons["passcode.digit.\(digit)"] + XCTAssertTrue(button.waitForExistence(timeout: 5)) + XCTAssertTrue(button.isEnabled) + button.tap() + } + } + + private func clearPartialCode() { + let delete = app.buttons["passcode.delete"] + for _ in 0..<6 where delete.exists && delete.isEnabled { + delete.tap() + } + } + + private func assertTitle(_ expected: String) { + XCTAssertTrue(title.waitForExistence(timeout: TestConstants.controlExistenceTimeout)) + XCTAssertEqual(title.label, expected) + } + + private func backgroundAndActivate() { + XCUIDevice.shared.press(.home) + let background = NSPredicate { [app] _, _ in + MainActor.assumeIsolated { app?.state != .runningForeground } + } + XCTAssertEqual(XCTWaiter.wait(for: [XCTNSPredicateExpectation(predicate: background, object: nil)], timeout: 5), .completed) + app.activate() + XCTAssertTrue(app.wait(for: .runningForeground, timeout: 5)) + } + + private func assertLockedOut() { + XCTAssertTrue(lockout.waitForExistence(timeout: 5)) + XCTAssertFalse(app.buttons["passcode.digit.1"].isEnabled) + } + + private func remainingSeconds() -> Int { + guard let first = lockout.label.split(separator: " ").first, let seconds = Int(first) else { + XCTFail("Missing lockout countdown: \(lockout.label)") + return 0 + } + return seconds + } + + private func waitForCountdown(toDropBelow seconds: Int) { + let countdown = NSPredicate { [weak self] _, _ in + MainActor.assumeIsolated { + guard let self else { return false } + return self.remainingSeconds() < seconds + } + } + XCTAssertEqual(XCTWaiter.wait(for: [XCTNSPredicateExpectation(predicate: countdown, object: nil)], timeout: 10), .completed) + } + + private func assertOriginalDeadline(_ deadline: Date) { + let expected = Int(deadline.timeIntervalSinceNow.rounded(.up)) + XCTAssertGreaterThan(remainingSeconds(), 0) + XCTAssertLessThanOrEqual(remainingSeconds(), expected + 2, "Background, reopening or relaunch must not restart the lockout.") + XCTAssertGreaterThanOrEqual(remainingSeconds(), max(0, expected - 2), "The persisted deadline must not be shortened.") + } +} diff --git a/iOSClient/AppDelegate.swift b/iOSClient/AppDelegate.swift index 1f755c14dc..1b1cdb374f 100644 --- a/iOSClient/AppDelegate.swift +++ b/iOSClient/AppDelegate.swift @@ -32,6 +32,17 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD var pushSubscriptionTask: Task? func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { +#if DEBUG + // Give security UI tests a repeatable starting state on their dedicated simulator. + if isUiTestingEnabled && ProcessInfo.processInfo.arguments.contains("UI_TESTING_PASSCODE") { + let preferences = NCPreferences() + preferences.passcode = nil + preferences.clearPasscodeFailures() + preferences.touchFaceID = false + preferences.requestPasscodeAtStart = true + preferences.resetAppCounterFail = false + } +#endif if isUiTestingEnabled { Task { await NCAccount().deleteAllAccounts() diff --git a/iOSClient/Extensions/UIAlertController+Extension.swift b/iOSClient/Extensions/UIAlertController+Extension.swift index 6b8a628966..500f3973eb 100644 --- a/iOSClient/Extensions/UIAlertController+Extension.swift +++ b/iOSClient/Extensions/UIAlertController+Extension.swift @@ -331,47 +331,6 @@ extension UIAlertController { } } - @MainActor - static func failedPasscode(presenter: UIViewController, completion: (() -> Void)? = nil) { - let preferences = NCPreferences() - let deadline: Date - - if let pending = preferences.passcodeLockoutEnd { - guard pending > Date() else { - endPasscodeLockout(completion: completion) - return - } - - deadline = pending - } else { - deadline = Date().addingTimeInterval(TimeInterval(NCBrandOptions.shared.passcodeSecondsFail)) - preferences.passcodeLockoutEnd = deadline - } - - let alertController = UIAlertController(title: NSLocalizedString("_passcode_counter_fail_", comment: ""), message: nil, preferredStyle: .alert) - presenter.present(alertController, animated: true) - - alertController.message = "\(Int(deadline.timeIntervalSinceNow.rounded(.up))) " + NSLocalizedString("_seconds_", comment: "") - - _ = Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true) { timer in - let seconds = Int(deadline.timeIntervalSinceNow.rounded(.up)) - - if seconds > 0 { - alertController.message = "\(seconds) " + NSLocalizedString("_seconds_", comment: "") - } else { - timer.invalidate() - alertController.dismiss(animated: true) - endPasscodeLockout(completion: completion) - } - } - } - - private static func endPasscodeLockout(completion: (() -> Void)?) { - NCPreferences().clearPasscodeFailures() - - completion?() - } - /// Presents a localized confirmation alert and asynchronously returns the user's choice. /// /// - Parameters: diff --git a/iOSClient/Main/NCPasscode.swift b/iOSClient/Main/NCPasscode.swift deleted file mode 100644 index df3bb4e846..0000000000 --- a/iOSClient/Main/NCPasscode.swift +++ /dev/null @@ -1,196 +0,0 @@ -// SPDX-FileCopyrightText: Nextcloud GmbH -// SPDX-FileCopyrightText: 2024 Marino Faggiana -// SPDX-License-Identifier: GPL-3.0-or-later - -import UIKit -import LocalAuthentication - -public protocol NCPasscodeDelegate: AnyObject { - func evaluatePolicy(_ passcodeViewController: TOPasscodeViewController, isCorrectCode: Bool) - func passcodeReset(_ passcodeViewController: TOPasscodeViewController) - func requestedAccount(controller: UIViewController?) -} - -public extension NCPasscodeDelegate { - func evaluatePolicy(_ passcodeViewController: TOPasscodeViewController, isCorrectCode: Bool) {} - func passcodeReset(_ passcodeViewController: TOPasscodeViewController) {} - func requestedAccount(controller: UIViewController?) {} -} - -class NCPasscode: NSObject, TOPasscodeViewControllerDelegate { - public static let shared: NCPasscode = { - let instance = NCPasscode() - return instance - }() - var isPasscodeReset: Bool { - let passcodeCounterFailReset = NCPreferences().passcodeCounterFailReset - return NCPreferences().resetAppCounterFail && passcodeCounterFailReset >= NCBrandOptions.shared.resetAppPasscodeAttempts - } - - var isPasscodeCounterFail: Bool { - let passcodeCounterFail = NCPreferences().passcodeCounterFail - return passcodeCounterFail >= 3 - } - - var passcodeViewController: TOPasscodeViewController! - var delegate: NCPasscodeDelegate? - var viewController: UIViewController? - - override init() { - super.init() - - NotificationCenter.default.addObserver(self, - selector: #selector(applicationDidBecomeActive), - name: UIApplication.didBecomeActiveNotification, - object: nil) - } - - func presentPasscode(viewController: UIViewController, delegate: NCPasscodeDelegate?, completion: @escaping () -> Void) { - if viewController.presentedViewController is TOPasscodeViewController { return } - - var error: NSError? - self.delegate = delegate - self.viewController = viewController - - passcodeViewController = TOPasscodeViewController(passcodeType: .sixDigits, allowCancel: false) - passcodeViewController.delegate = self - passcodeViewController.keypadButtonShowLettering = false - if NCPreferences().touchFaceID, LAContext().canEvaluatePolicy(.deviceOwnerAuthenticationWithBiometrics, error: &error) { - if error == nil { - if LAContext().biometryType == .faceID { - passcodeViewController.biometryType = .faceID - } else if LAContext().biometryType == .touchID { - passcodeViewController.biometryType = .touchID - } - passcodeViewController.allowBiometricValidation = true - passcodeViewController.automaticallyPromptForBiometricValidation = false - } - } - viewController.presentedViewController?.dismiss(animated: false) - viewController.present(passcodeViewController, animated: true, completion: { - // `present` always runs on main thread so this assumption is correct. - MainActor.assumeIsolated { - self.presentTooManyFailedAttemptsAlertIfNeeded(passcodeViewController: self.passcodeViewController) - } - completion() - }) - } - - func enableTouchFaceID() { - guard NCPreferences().touchFaceID, - NCPreferences().presentPasscode, - !isPasscodeCounterFail, - let passcodeViewController - else { return } - - DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { - LAContext().evaluatePolicy(.deviceOwnerAuthenticationWithBiometrics, localizedReason: NCBrandOptions.shared.brand) { success, evaluateError in - if success { - DispatchQueue.main.async { - passcodeViewController.dismiss(animated: true) { - NCPreferences().passcodeCounterFail = 0 - NCPreferences().passcodeCounterFailReset = 0 - self.delegate?.evaluatePolicy(passcodeViewController, isCorrectCode: true) - if NCPreferences().accountRequest { - self.delegate?.requestedAccount(controller: self.viewController) - } - } - } - } else { - if let error = evaluateError { - switch error._code { - case LAError.userFallback.rawValue, LAError.authenticationFailed.rawValue: - NCPreferences().passcodeCounterFail += 1 - NCPreferences().passcodeCounterFailReset += 1 - - // The biometry reply arrives off the main thread. - DispatchQueue.main.async { - self.presentTooManyFailedAttemptsAlertIfNeeded(passcodeViewController: passcodeViewController) - } - case LAError.biometryLockout.rawValue: - LAContext().evaluatePolicy(LAPolicy.deviceOwnerAuthentication, localizedReason: NSLocalizedString("_deviceOwnerAuthentication_", comment: ""), reply: { success, _ in - if success { - DispatchQueue.main.async { - NCPreferences().passcodeCounterFail = 0 - self.enableTouchFaceID() - } - } - }) - default: - break - } - } - } - } - } - } - - func didInputCorrectPasscode(in passcodeViewController: TOPasscodeViewController) { - DispatchQueue.main.async { - passcodeViewController.dismiss(animated: true) { - NCPreferences().passcodeCounterFail = 0 - NCPreferences().passcodeCounterFailReset = 0 - if NCPreferences().accountRequest { - self.delegate?.requestedAccount(controller: self.viewController) - } - } - } - } - - func passcodeViewController(_ passcodeViewController: TOPasscodeViewController, isCorrectCode code: String) -> Bool { - if code == NCPreferences().passcode { - self.delegate?.evaluatePolicy(passcodeViewController, isCorrectCode: true) - return true - } else { - NCPreferences().passcodeCounterFail += 1 - NCPreferences().passcodeCounterFailReset += 1 - - // Keypad taps are delivered on the main thread. - MainActor.assumeIsolated { - presentTooManyFailedAttemptsAlertIfNeeded(passcodeViewController: passcodeViewController) - } - - self.delegate?.evaluatePolicy(passcodeViewController, isCorrectCode: false) - return false - } - } - - func didPerformBiometricValidationRequest(in passcodeViewController: TOPasscodeViewController) { - enableTouchFaceID() - } - - @MainActor - private func presentTooManyFailedAttemptsAlertIfNeeded(passcodeViewController: TOPasscodeViewController) { - guard passcodeViewController.presentedViewController == nil else { return } - - // A lockout can elapse while the app is closed; clear it before deciding whether to cover the keypad. - if let lockoutEnd = NCPreferences().passcodeLockoutEnd, lockoutEnd <= Date() { - NCPreferences().clearPasscodeFailures() - } - - if isPasscodeReset { - passcodeViewController.setContentHidden(true, animated: true) - - let alertController = UIAlertController(title: NSLocalizedString("_reset_wrong_passcode_", comment: ""), message: nil, preferredStyle: .alert) - passcodeViewController.present(alertController, animated: true) - - delegate?.passcodeReset(passcodeViewController) - } else if isPasscodeCounterFail { - passcodeViewController.setContentHidden(true, animated: true) - - UIAlertController.failedPasscode(presenter: passcodeViewController) { - passcodeViewController.setContentHidden(false, animated: true) - self.enableTouchFaceID() - } - } - } - - @MainActor - @objc private func applicationDidBecomeActive() { - guard let passcodeViewController, - passcodeViewController.view.window != nil - else { return } - - presentTooManyFailedAttemptsAlertIfNeeded(passcodeViewController: passcodeViewController) - } -} diff --git a/iOSClient/Nextcloud-Bridging-Header.h b/iOSClient/Nextcloud-Bridging-Header.h index 465ea9cd9a..fe04e1a63f 100644 --- a/iOSClient/Nextcloud-Bridging-Header.h +++ b/iOSClient/Nextcloud-Bridging-Header.h @@ -6,4 +6,3 @@ #import "NYMnemonic.h" #import "UIImage+animatedGIF.h" #import "NCPushNotificationEncryption.h" -#import "TOPasscodeViewController.h" diff --git a/iOSClient/Settings/Acknowledgements.rtf b/iOSClient/Settings/Acknowledgements.rtf index 739f9a05e5..4a7b3713d8 100644 --- a/iOSClient/Settings/Acknowledgements.rtf +++ b/iOSClient/Settings/Acknowledgements.rtf @@ -179,15 +179,6 @@ Copyright (c) Fabrizio Brancati\ __________________________________\ \ -\f1\b TOPasscodeViewController -\f0\b0 \ -\ -MIT License\ -\ -Copyright (c) Tim Oliver\ -__________________________________\ -\ - \f1\b EasyTipView \f0\b0 \ \ diff --git a/iOSClient/Settings/Settings/NCSettingsView.swift b/iOSClient/Settings/Settings/NCSettingsView.swift index 20a04fec7d..fabc2ae45c 100644 --- a/iOSClient/Settings/Settings/NCSettingsView.swift +++ b/iOSClient/Settings/Settings/NCSettingsView.swift @@ -53,6 +53,7 @@ struct NCSettingsView: View { } }) .tint(Color(NCBrandColor.shared.textColor)) + .accessibilityIdentifier("settings.passcode.toggle") .disabled(NCBrandOptions.shared.enforce_passcode_lock) }, header: { Text(NSLocalizedString("_privacy_", comment: "")) @@ -77,6 +78,7 @@ struct NCSettingsView: View { .tint(Color(NCBrandColor.shared.textColor)) } }) + .accessibilityIdentifier("settings.passcode.change") // Enable Touch ID Toggle(NSLocalizedString("_enable_touch_face_id_", comment: ""), isOn: $model.enableTouchFaceID) .font(.body) diff --git a/iOSClient/Settings/Settings/SetupPasscodeView.swift b/iOSClient/Settings/Settings/SetupPasscodeView.swift deleted file mode 100644 index b04c7be5cd..0000000000 --- a/iOSClient/Settings/Settings/SetupPasscodeView.swift +++ /dev/null @@ -1,186 +0,0 @@ -// SPDX-FileCopyrightText: Nextcloud GmbH -// SPDX-FileCopyrightText: 2026 Milen Pivchev -// SPDX-FileCopyrightText: 2024 Marino Faggiana -// SPDX-License-Identifier: GPL-3.0-or-later - -import Foundation -import UIKit -import SwiftUI -import LocalAuthentication -import PopupView - -struct SetupPasscodeView: UIViewControllerRepresentable { - var isPasscodeReset: Bool { - let passcodeCounterFailReset = NCPreferences().passcodeCounterFailReset - return NCPreferences().resetAppCounterFail && passcodeCounterFailReset >= NCBrandOptions.shared.resetAppPasscodeAttempts - } - - var isPasscodeCounterFail: Bool { - let passcodeCounterFail = NCPreferences().passcodeCounterFail - return passcodeCounterFail > 0 && passcodeCounterFail >= 3 - } - - @Binding var isLockActive: Bool - weak var controller: NCMainTabBarController? - var changePasscode: Bool = false - - func makeUIViewController(context: Context) -> UIViewController { - let laContext = LAContext() - var error: NSError? - let viewController: UIViewController - - if !NCPreferences().passcode.isEmptyOrNil, !changePasscode { - let passcodeVC = TOPasscodeViewController(passcodeType: .sixDigits, allowCancel: true) - passcodeVC.keypadButtonShowLettering = false - - if NCPreferences().touchFaceID, laContext.canEvaluatePolicy(.deviceOwnerAuthenticationWithBiometrics, error: &error), error == nil { - switch laContext.biometryType { - case .faceID: - passcodeVC.biometryType = .faceID - case .touchID: - passcodeVC.biometryType = .touchID - default: - print("No Biometric support") - } - passcodeVC.allowBiometricValidation = true - passcodeVC.automaticallyPromptForBiometricValidation = true - } - - passcodeVC.delegate = context.coordinator - viewController = passcodeVC - } else { - let passcodeSettingsVC = TOPasscodeSettingsViewController() - passcodeSettingsVC.hideOptionsButton = true - passcodeSettingsVC.requireCurrentPasscode = changePasscode - passcodeSettingsVC.passcodeType = .sixDigits - passcodeSettingsVC.delegate = context.coordinator - viewController = passcodeSettingsVC - } - - return PasscodeContainerViewController(child: viewController) - } - - func updateUIViewController(_ uiViewController: UIViewController, context: Context) { - // Update the view controller if needed - } - - func makeCoordinator() -> Coordinator { - Coordinator(self) - } - - class Coordinator: NSObject, @MainActor TOPasscodeSettingsViewControllerDelegate, @MainActor TOPasscodeViewControllerDelegate { - var parent: SetupPasscodeView - init(_ parent: SetupPasscodeView) { - self.parent = parent - } - - func didPerformBiometricValidationRequest(in passcodeViewController: TOPasscodeViewController) { - let context = LAContext() - var error: NSError? - - if context.canEvaluatePolicy(.deviceOwnerAuthenticationWithBiometrics, error: &error) { - context.evaluatePolicy(.deviceOwnerAuthenticationWithBiometrics, localizedReason: NCBrandOptions.shared.brand) { success, evaluateError in - DispatchQueue.main.async { - if success { - DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { [self] in - parent.isLockActive = false - NCPreferences().passcode = nil - - passcodeViewController.dismiss(animated: true) - } - } else if evaluateError != nil { - NCPreferences().passcodeCounterFail += 1 - NCPreferences().passcodeCounterFailReset += 1 - } - } - } - } - } - - // This triggers only for "Change passcode" option - @MainActor - func passcodeSettingsViewController(_ passcodeSettingsViewController: TOPasscodeSettingsViewController, didAttemptCurrentPasscode passcode: String) -> Bool { - if passcode == NCPreferences().passcode { - return true - } else { - NCPreferences().passcodeCounterFail += 1 - NCPreferences().passcodeCounterFailReset += 1 - } - - if parent.isPasscodeCounterFail { - UIAlertController.failedPasscode(presenter: passcodeSettingsViewController) - } - - return false - } - - func passcodeSettingsViewController(_ passcodeSettingsViewController: TOPasscodeSettingsViewController, didChangeToNewPasscode passcode: String, of type: TOPasscodeType) { - NCPreferences().passcode = passcode - parent.isLockActive = true - passcodeSettingsViewController.dismiss(animated: true) - } - - func didTapCancel(in passcodeViewController: TOPasscodeViewController) { - passcodeViewController.dismiss(animated: true) - } - - // This triggers upon pressing "Lock: On" - @MainActor - func passcodeViewController(_ passcodeViewController: TOPasscodeViewController, isCorrectCode passcode: String) -> Bool { - if passcode == NCPreferences().passcode { - parent.isLockActive = false - NCPreferences().passcode = nil - return true - } - - NCPreferences().passcodeCounterFail += 1 - NCPreferences().passcodeCounterFailReset += 1 - - if parent.isPasscodeCounterFail { - UIAlertController.failedPasscode(presenter: passcodeViewController) - } - - return false - } - } -} - -private final class PasscodeContainerViewController: UIViewController { - private let child: UIViewController - - init(child: UIViewController) { - self.child = child - super.init(nibName: nil, bundle: nil) - } - - required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - override func viewDidLoad() { - super.viewDidLoad() - - addChild(child) - child.view.frame = view.bounds - child.view.autoresizingMask = [.flexibleWidth, .flexibleHeight] - view.addSubview(child.view) - child.didMove(toParent: self) - - NotificationCenter.default.addObserver(self, - selector: #selector(presentFailedPasscodeIfNeeded), - name: UIApplication.didBecomeActiveNotification, - object: nil) - } - - override func viewDidAppear(_ animated: Bool) { - super.viewDidAppear(animated) - - presentFailedPasscodeIfNeeded() - } - - @objc private func presentFailedPasscodeIfNeeded() { - guard presentedViewController == nil, NCPreferences().passcodeCounterFail >= 3 else { return } - - UIAlertController.failedPasscode(presenter: self) - } -} diff --git a/iOSClient/Utility/NCAskAuthorization.swift b/iOSClient/Utility/NCAskAuthorization.swift index a58f97c3d8..62596ded33 100644 --- a/iOSClient/Utility/NCAskAuthorization.swift +++ b/iOSClient/Utility/NCAskAuthorization.swift @@ -70,9 +70,6 @@ class NCAskAuthorization: NSObject { self.isRequesting = true PHPhotoLibrary.requestAuthorization { allowed in self.isRequesting = false -#if !EXTENSION - // DispatchQueue.main.async { NCPasscode.shared.hidePrivacyProtectionWindow() } -#endif if allowed == PHAuthorizationStatus.authorized { completion(true) diff --git a/iOSClient/Utility/Passcode/NCPasscodeModel.swift b/iOSClient/Utility/Passcode/NCPasscodeModel.swift index 3ba29b0822..9a7febc268 100644 --- a/iOSClient/Utility/Passcode/NCPasscodeModel.swift +++ b/iOSClient/Utility/Passcode/NCPasscodeModel.swift @@ -30,6 +30,10 @@ final class NCPasscodeModel: ObservableObject { @Published private var step: Step var onCompletion: (@MainActor (NCPasscodeModel.Result) -> Void)? + static var isPasscodeReset: Bool { + NCPreferences().resetAppCounterFail && NCPreferences().passcodeCounterFailReset >= NCBrandOptions.shared.resetAppPasscodeAttempts + } + private var isPreview = false private var newPasscode = "" private var biometricContext: LAContext? @@ -89,7 +93,7 @@ final class NCPasscodeModel: ObservableObject { if let deadline = preferences.passcodeLockoutEnd, deadline <= Date() { preferences.clearPasscodeFailures() } - if preferences.resetAppCounterFail && preferences.passcodeCounterFailReset >= NCBrandOptions.shared.resetAppPasscodeAttempts { + if Self.isPasscodeReset { finish(.reset) return } diff --git a/iOSClient/Utility/Passcode/NCPasscodeView.swift b/iOSClient/Utility/Passcode/NCPasscodeView.swift index 5a757bc69b..d1137e310c 100644 --- a/iOSClient/Utility/Passcode/NCPasscodeView.swift +++ b/iOSClient/Utility/Passcode/NCPasscodeView.swift @@ -62,6 +62,7 @@ struct NCPasscodeView: View { .accessibilityHidden(true) Text(NSLocalizedString(model.title, comment: "")) .font(.title2) + .accessibilityIdentifier("passcode.title") .multilineTextAlignment(.center) .accessibilityAddTraits(.isHeader) HStack(spacing: 16) { @@ -73,17 +74,20 @@ struct NCPasscodeView: View { .accessibilityElement(children: .ignore) .accessibilityLabel(NSLocalizedString("_passcode_digits_entered_", comment: "")) .accessibilityValue("\(model.digits.count) / 6") + .accessibilityIdentifier("passcode.progress") if let deadline = model.lockoutEnd { TimelineView(.periodic(from: .now, by: 1)) { timeline in VStack(spacing: 8) { Text(NSLocalizedString("_passcode_counter_fail_", comment: "")) Text("\(max(0, Int(deadline.timeIntervalSince(timeline.date).rounded(.up)))) " + NSLocalizedString("_seconds_", comment: "")) + .accessibilityIdentifier("passcode.lockout.remaining") } .multilineTextAlignment(.center) } } else { Text(NSLocalizedString(model.message, comment: "")) + .accessibilityIdentifier("passcode.error") .foregroundStyle(.red) .multilineTextAlignment(.center) .frame(minHeight: 24) @@ -106,6 +110,7 @@ struct NCPasscodeView: View { Image(systemName: "xmark") } .accessibilityLabel(NSLocalizedString("_close_", comment: "")) + .accessibilityIdentifier("passcode.close") } } } @@ -165,6 +170,7 @@ struct NCPasscodeView: View { .frame(minWidth: 64, minHeight: 64) } .accessibilityLabel(NSLocalizedString("_delete_", comment: "")) + .accessibilityIdentifier("passcode.delete") .disabled(!model.canEnterCode || model.digits.isEmpty) } } @@ -180,6 +186,7 @@ struct NCPasscodeView: View { .frame(minWidth: 64, minHeight: 64) .background(Color(uiColor: .secondarySystemBackground), in: Circle()) } + .accessibilityIdentifier("passcode.digit.\(digit)") .disabled(!model.canEnterCode) } } diff --git a/iOSClient/Utility/TOPasscodeViewController/Models/TOPasscodeCircleImage.h b/iOSClient/Utility/TOPasscodeViewController/Models/TOPasscodeCircleImage.h deleted file mode 100644 index 6000f1fca6..0000000000 --- a/iOSClient/Utility/TOPasscodeViewController/Models/TOPasscodeCircleImage.h +++ /dev/null @@ -1,57 +0,0 @@ -// -// TOPasscodeCircleImage.h -// -// Copyright 2017 Timothy Oliver. All rights reserved. -// -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files (the "Software"), to -// deal in the Software without restriction, including without limitation the -// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or -// sell copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: -// -// The above copyright notice and this permission notice shall be included in -// all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS -// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, -// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR -// IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - -#import - -NS_ASSUME_NONNULL_BEGIN - -/** - A subclass of `UIImage` that can procedurally generate both hollow and full circle graphics at any size. - These are used for the 'normal' and 'tapped' states of the passcode circle buttons. - */ -@interface TOPasscodeCircleImage : UIImage - -/** - Generates and returns a `UIImage` of a filled circle at the specified size. - - @param size The diameter of the final circle image - @param inset An inset value that will shrink the size of the circle. This is so it can be overlaid on a hollow circle - without interfering with the anti-aliasing on the outer border. - @param padding External padding around the circle to ensure it won't be clipped by the edge of the layer. - Setting this value will increase the dimensions of the final `UIImage`. - @param antialias Whether the circle boundary will be antialiased (Since antialiasing is unnecessary if this circle will overlay another.) - */ -+ (UIImage *)circleImageOfSize:(CGFloat)size inset:(CGFloat)inset padding:(CGFloat)padding antialias:(BOOL)antialias; - -/** - Generates and returns a `UIImage` of a hollow circle at the specified size. - - @param size The diameter of the final circle image - @param strokeWidth The thickness, in points, of the stroke making up the circle image. - @param padding External padding around the circle to ensure it won't be clipped by the edge of the layer. - Setting this value will increase the dimensions of the final `UIImage`. - */ -+ (UIImage *)hollowCircleImageOfSize:(CGFloat)size strokeWidth:(CGFloat)strokeWidth padding:(CGFloat)padding; - -@end - -NS_ASSUME_NONNULL_END diff --git a/iOSClient/Utility/TOPasscodeViewController/Models/TOPasscodeCircleImage.m b/iOSClient/Utility/TOPasscodeViewController/Models/TOPasscodeCircleImage.m deleted file mode 100644 index 4288a33092..0000000000 --- a/iOSClient/Utility/TOPasscodeViewController/Models/TOPasscodeCircleImage.m +++ /dev/null @@ -1,75 +0,0 @@ -// -// TOPasscodeCircleImage.m -// -// Copyright 2017 Timothy Oliver. All rights reserved. -// -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files (the "Software"), to -// deal in the Software without restriction, including without limitation the -// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or -// sell copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: -// -// The above copyright notice and this permission notice shall be included in -// all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS -// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, -// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR -// IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - -#import "TOPasscodeCircleImage.h" - -@implementation TOPasscodeCircleImage - -+ (UIImage *)circleImageOfSize:(CGFloat)size inset:(CGFloat)inset padding:(CGFloat)padding antialias:(BOOL)antialias -{ - UIImage *image = nil; - CGSize imageSize = (CGSize){size + (padding * 2), size + (padding * 2)}; - - UIGraphicsBeginImageContextWithOptions(imageSize, NO, 0.0f); - { - CGContextRef context = UIGraphicsGetCurrentContext(); - - if (!antialias) { - CGContextSetShouldAntialias(context, NO); - } - - CGRect rect = (CGRect){padding + inset, padding + inset, size - (inset * 2), size - (inset * 2)}; - UIBezierPath* ovalPath = [UIBezierPath bezierPathWithOvalInRect:rect]; - [[UIColor blackColor] setFill]; - [ovalPath fill]; - - image = UIGraphicsGetImageFromCurrentImageContext(); - } - UIGraphicsEndImageContext(); - - return [image imageWithRenderingMode:UIImageRenderingModeAlwaysTemplate]; -} - -+ (UIImage *)hollowCircleImageOfSize:(CGFloat)size strokeWidth:(CGFloat)strokeWidth padding:(CGFloat)padding -{ - UIImage *image = nil; - CGSize canvasSize = (CGSize){size + (padding * 2), size + (padding * 2)}; - CGSize circleSize = (CGSize){size, size}; - - UIGraphicsBeginImageContextWithOptions(canvasSize, NO, 0.0f); - { - CGRect circleRect = (CGRect){{padding, padding}, circleSize}; - circleRect = CGRectInset(circleRect, (strokeWidth * 0.5f), (strokeWidth * 0.5f)); - - UIBezierPath *path = [UIBezierPath bezierPathWithOvalInRect:circleRect]; - [[UIColor blackColor] setStroke]; - path.lineWidth = strokeWidth; - [path stroke]; - - image = UIGraphicsGetImageFromCurrentImageContext(); - } - UIGraphicsEndImageContext(); - - return [image imageWithRenderingMode:UIImageRenderingModeAlwaysTemplate]; -} - -@end diff --git a/iOSClient/Utility/TOPasscodeViewController/Models/TOPasscodeViewContentLayout.h b/iOSClient/Utility/TOPasscodeViewController/Models/TOPasscodeViewContentLayout.h deleted file mode 100644 index 97191fd6ae..0000000000 --- a/iOSClient/Utility/TOPasscodeViewController/Models/TOPasscodeViewContentLayout.h +++ /dev/null @@ -1,100 +0,0 @@ -// -// TOPasscodeViewContentLayout.h -// -// Copyright 2017 Timothy Oliver. All rights reserved. -// -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files (the "Software"), to -// deal in the Software without restriction, including without limitation the -// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or -// sell copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: -// -// The above copyright notice and this permission notice shall be included in -// all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS -// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, -// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR -// IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - -#import -#import - -/** - Depending on the width of the application window, all of the content views in - the passcode view need to be resized in order to fit in the available space. - - This means that not only does the spacing and sizing of views need to be changed, but - image assets need to be regenerated and font sizes need to change as well. - - This class assumes there will be three major screen sizes, and provides layout - sizes, spacing, and styles in order to resize the passcode view for each one. - - The three screen styles it supports are: - - * Small Screens - iPhone 5/ or iPad 9.7" in 1/4 split screen mode - * Medium Screens - iPhone 6/ or iPad 12.9" in 1/4 split screen mode - * Large Screens - iPhone 6 Plus and all iPads when not in split screen mode. - - */ -@interface TOPasscodeViewContentLayout : NSObject - -/* The width of the PIN view in which this layout object is sizing the content to fit. */ -@property (nonatomic, assign) CGFloat viewWidth; - -/* Extra padding at the bottom in order to shift the content slightly up */ -@property (nonatomic, assign) CGFloat bottomPadding; - -/* The title view at the very top */ -@property (nonatomic, assign) CGFloat titleViewBottomSpacing; // Space from the bottom of the title view to the title label - -/* The Title Label Explaining the Passcode View */ -@property (nonatomic, assign) CGFloat titleLabelBottomSpacing; // Space from the title label to the input view -@property (nonatomic, assign) CGFloat subtitleLabelBottomSpacing; // Space from the subtitle label to the input view - -@property (nonatomic, strong) UIFont *titleLabelFont; // The font of the title label -@property (nonatomic, strong) UIFont *subtitleLabelFont; // The font of the subtitle label - -/* Title Label properties when the view is laid out horizontally */ -@property (nonatomic, assign) CGFloat titleHorizontalLayoutWidth; // When laid out horizontally, the width of the title view -@property (nonatomic, assign) CGFloat titleHorizontalLayoutSpacing; // The amount of spacing between the title label and the passcode keypad -@property (nonatomic, assign) CGFloat titleViewHorizontalBottomSpacing; // Space from the bottom of the title view when iPhone is horizontal -@property (nonatomic, assign) CGFloat titleLabelHorizontalBottomSpacing; // Spacing from the title label to input view in horizontal mode -@property (nonatomic, assign) CGFloat subtitleLabelHorizontalBottomSpacing; // Spacing from the subtitle label to input view in horizontal mode - -/* Circle Row Configuration */ -@property (nonatomic, assign) CGFloat circleRowDiameter; // The diameter of each circle representing a PIN number -@property (nonatomic, assign) CGFloat circleRowSpacing; // The spacing between each circle -@property (nonatomic, assign) CGFloat circleRowBottomSpacing; // Space between the view used to indicate input - -/* Text Field Configuration */ -@property (nonatomic, assign) CGFloat textFieldBorderThickness; // The thickness of the border stroke -@property (nonatomic, assign) CGFloat textFieldBorderRadius; // The corner radius of the border -@property (nonatomic, assign) CGFloat textFieldCircleDiameter; // The size of the circles in the passcode field -@property (nonatomic, assign) CGFloat textFieldCircleSpacing; // The amount of spacing between each circle -@property (nonatomic, assign) CGSize textFieldBorderPadding; // The amount of padding between the circles and the border -@property (nonatomic, assign) NSInteger textFieldNumericCharacterLength; // The amount of circles to have in this field when set to numeric -@property (nonatomic, assign) NSInteger textFieldAlphanumericCharacterLength; // The amount of circles to have in this field when set to alphanumeric -@property (nonatomic, assign) CGFloat submitButtonFontSize; // The font size of the 'OK' button -@property (nonatomic, assign) CGFloat submitButtonSpacing; // The spacing of the 'OK' button from the input - -/* Circle Button Shape and Layout */ -@property (nonatomic, assign) CGFloat circleButtonDiameter; // The size of each PIN button -@property (nonatomic, assign) CGSize circleButtonSpacing; // The vertical/horizontal spacing between buttons -@property (nonatomic, assign) CGFloat circleButtonStrokeWidth; // The thickness of the border line - -/* Circle Button Label */ -@property (nonatomic, strong) UIFont *circleButtonTitleLabelFont; // The font used for the '1' number labels -@property (nonatomic, strong) UIFont *circleButtonLetteringLabelFont; // The font used for the 'ABC' labels -@property (nonatomic, assign) CGFloat circleButtonLabelSpacing; // The vertical spacing between the number and lettering labels -@property (nonatomic, assign) CGFloat circleButtonLetteringSpacing; // The spacing between the 'ABC' characters - -/* Default layout configurations for the various sizes */ -+ (TOPasscodeViewContentLayout *)defaultScreenContentLayout; /* Default layout values. Designed for iPhone 6 Plus and above. */ -+ (TOPasscodeViewContentLayout *)mediumScreenContentLayout; /* For medium screen sizes, like iPhone 6, or 1/4 view on iPad Pro. */ -+ (TOPasscodeViewContentLayout *)smallScreenContentLayout; /* For the smallest screens, like iPhone SE, and 1/4 on standard size iPads/ */ - -@end diff --git a/iOSClient/Utility/TOPasscodeViewController/Models/TOPasscodeViewContentLayout.m b/iOSClient/Utility/TOPasscodeViewController/Models/TOPasscodeViewContentLayout.m deleted file mode 100644 index 5e8029133c..0000000000 --- a/iOSClient/Utility/TOPasscodeViewController/Models/TOPasscodeViewContentLayout.m +++ /dev/null @@ -1,205 +0,0 @@ -// -// TOPasscodeViewContentLayout.m -// -// Copyright 2017 Timothy Oliver. All rights reserved. -// -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files (the "Software"), to -// deal in the Software without restriction, including without limitation the -// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or -// sell copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: -// -// The above copyright notice and this permission notice shall be included in -// all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS -// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, -// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR -// IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - -#import "TOPasscodeViewContentLayout.h" - -@implementation TOPasscodeViewContentLayout - -+ (TOPasscodeViewContentLayout *)defaultScreenContentLayout -{ - TOPasscodeViewContentLayout *contentLayout = [[TOPasscodeViewContentLayout alloc] init]; - - /* Width of the PIN View */ - contentLayout.viewWidth = 414.0f; - - /* Bottom Padding */ - contentLayout.bottomPadding = 25.0f; - - /* Title View Constraints */ - contentLayout.titleViewBottomSpacing = 34.0f; - - /* The Title Label Explaining the PIN View */ - contentLayout.titleLabelBottomSpacing = 34.0f; - contentLayout.titleLabelFont = [UIFont systemFontOfSize: 22.0f]; - - /* Horizontal title constraints */ - contentLayout.titleHorizontalLayoutWidth = 250.0f; - contentLayout.titleHorizontalLayoutSpacing = 35.0f; - contentLayout.titleViewHorizontalBottomSpacing = 20.0f; - contentLayout.titleLabelHorizontalBottomSpacing = 20.0f; - - /* Circle Row Configuration */ - contentLayout.circleRowDiameter = 15.5f; - contentLayout.circleRowSpacing = 30.0f; - contentLayout.circleRowBottomSpacing = 35.0f; - - /* The Subtitle Label */ - contentLayout.subtitleLabelFont = [UIFont systemFontOfSize: 17.0f]; - contentLayout.subtitleLabelBottomSpacing = 40.0f; - contentLayout.subtitleLabelHorizontalBottomSpacing = 40.0f; - - /* Text Field Input Configuration */ - contentLayout.textFieldBorderThickness = 1.5f; - contentLayout.textFieldBorderRadius = 5.0f; - contentLayout.textFieldCircleDiameter = 10.0f; - contentLayout.textFieldCircleSpacing = 6.0f; - contentLayout.textFieldBorderPadding = (CGSize){10, 10}; - contentLayout.textFieldNumericCharacterLength = 10; - contentLayout.textFieldAlphanumericCharacterLength = 15; - contentLayout.submitButtonFontSize = 17.0f; - contentLayout.submitButtonSpacing = 4.0f; - - /* Circle Button Shape and Layout */ - contentLayout.circleButtonDiameter = 90.0f; - contentLayout.circleButtonSpacing = (CGSize){25.0f, 16.0f}; - contentLayout.circleButtonStrokeWidth = 1.5f; - - /* Circle Button Label */ - contentLayout.circleButtonTitleLabelFont = [UIFont systemFontOfSize:37.5f weight:UIFontWeightThin]; - contentLayout.circleButtonLetteringLabelFont = [UIFont systemFontOfSize:9.0f weight:UIFontWeightThin]; - contentLayout.circleButtonLabelSpacing = 6.0f; - contentLayout.circleButtonLetteringSpacing = 3.0f; - - return contentLayout; -} - -+ (TOPasscodeViewContentLayout *)mediumScreenContentLayout -{ - TOPasscodeViewContentLayout *contentLayout = [[TOPasscodeViewContentLayout alloc] init]; - - /* Width of the PIN View */ - contentLayout.viewWidth = 375.0f; - - /* Bottom Padding */ - contentLayout.bottomPadding = 17.0f; - - /* Title View Constraints */ - contentLayout.titleViewBottomSpacing = 20.0f; - - /* The Title Label Explaining the PIN View */ - contentLayout.titleLabelFont = [UIFont systemFontOfSize: 19.0f]; - contentLayout.titleLabelBottomSpacing = 23.0f; - - /* Horizontal title constraints */ - contentLayout.titleHorizontalLayoutWidth = 185.0f; - contentLayout.titleHorizontalLayoutSpacing = 16.0f; - contentLayout.titleViewHorizontalBottomSpacing = 18.0f; - contentLayout.titleLabelHorizontalBottomSpacing = 18.0f; - - /* Circle Row Configuration */ - contentLayout.circleRowDiameter = 13.5f; - contentLayout.circleRowSpacing = 26.0f; - contentLayout.circleRowBottomSpacing = 21.0f; - - /* The Subtitle Label */ - contentLayout.subtitleLabelFont = [UIFont systemFontOfSize: 14.0f]; - contentLayout.subtitleLabelHorizontalBottomSpacing = 20.0f; - contentLayout.subtitleLabelBottomSpacing = 20.0f; - - /* Submit Button */ - contentLayout.submitButtonFontSize = 16.0f; - contentLayout.submitButtonSpacing = 4.0f; - - /* Circle Button Shape and Layout */ - contentLayout.circleButtonDiameter = 80.0f; - contentLayout.circleButtonSpacing = (CGSize){28.0f, 15.0f}; - contentLayout.circleButtonStrokeWidth = 1.5f; - - /* Text Field Input Configuration */ - contentLayout.textFieldBorderThickness = 1.5f; - contentLayout.textFieldBorderRadius = 5.0f; - contentLayout.textFieldCircleDiameter = 9.0f; - contentLayout.textFieldCircleSpacing = 5.0f; - contentLayout.textFieldBorderPadding = (CGSize){10, 10}; - contentLayout.textFieldNumericCharacterLength = 10; - contentLayout.textFieldAlphanumericCharacterLength = 15; - - /* Circle Button Label */ - contentLayout.circleButtonTitleLabelFont = [UIFont systemFontOfSize:36.5f weight:UIFontWeightThin]; - contentLayout.circleButtonLetteringLabelFont = [UIFont systemFontOfSize:8.5f weight:UIFontWeightThin]; - contentLayout.circleButtonLabelSpacing = 5.0f; - contentLayout.circleButtonLetteringSpacing = 2.5f; - - return contentLayout; -} - -+ (TOPasscodeViewContentLayout *)smallScreenContentLayout -{ - TOPasscodeViewContentLayout *contentLayout = [[TOPasscodeViewContentLayout alloc] init]; - - /* Width of the PIN View */ - contentLayout.viewWidth = 320.0f; - - /* Bottom Padding */ - contentLayout.bottomPadding = 12.0f; - - /* Title View Constraints */ - contentLayout.titleViewBottomSpacing = 15.0f; - - /* The Title Label Explaining the PIN View */ - contentLayout.titleLabelFont = [UIFont systemFontOfSize: 16.0f]; - contentLayout.titleLabelBottomSpacing = 19.0f; - - /* Horizontal title constraints */ - contentLayout.titleHorizontalLayoutWidth = 185.0f; - contentLayout.titleHorizontalLayoutSpacing = 5.0f; - contentLayout.titleViewHorizontalBottomSpacing = 18.0f; - contentLayout.titleLabelHorizontalBottomSpacing = 18.0f; - - /* Circle Row Configuration */ - contentLayout.circleRowDiameter = 12.5f; - contentLayout.circleRowSpacing = 22.0f; - contentLayout.circleRowBottomSpacing = 19.0f; - - /* The Subtitle Label */ - contentLayout.subtitleLabelFont = [UIFont systemFontOfSize: 12.0f]; - contentLayout.subtitleLabelHorizontalBottomSpacing = 22.0f; - contentLayout.subtitleLabelBottomSpacing = 19.0f; - - /* Text Field Input Configuration */ - contentLayout.textFieldBorderThickness = 1.5f; - contentLayout.textFieldBorderRadius = 5.0f; - contentLayout.textFieldCircleDiameter = 8.0f; - contentLayout.textFieldCircleSpacing = 4.0f; - contentLayout.textFieldBorderPadding = (CGSize){8, 8}; - contentLayout.textFieldNumericCharacterLength = 10; - contentLayout.textFieldAlphanumericCharacterLength = 15; - - /* Submit Button */ - contentLayout.submitButtonFontSize = 15.0f; - contentLayout.submitButtonSpacing = 3.0f; - - /* Circle Button Shape and Layout */ - contentLayout.circleButtonDiameter = 70.0f; - contentLayout.circleButtonSpacing = (CGSize){20.0f, 8.5f}; - contentLayout.circleButtonStrokeWidth = 1.5f; - - /* Circle Button Label */ - contentLayout.circleButtonTitleLabelFont = [UIFont systemFontOfSize:35.0f weight:UIFontWeightThin]; - contentLayout.circleButtonLetteringLabelFont = [UIFont systemFontOfSize:9.0f weight:UIFontWeightThin]; - contentLayout.circleButtonLabelSpacing = 4.5f; - contentLayout.circleButtonLetteringSpacing = 2.0f; - - return contentLayout; -} - -@end diff --git a/iOSClient/Utility/TOPasscodeViewController/Models/TOPasscodeViewControllerAnimatedTransitioning.h b/iOSClient/Utility/TOPasscodeViewController/Models/TOPasscodeViewControllerAnimatedTransitioning.h deleted file mode 100644 index 23147a4481..0000000000 --- a/iOSClient/Utility/TOPasscodeViewController/Models/TOPasscodeViewControllerAnimatedTransitioning.h +++ /dev/null @@ -1,60 +0,0 @@ -// -// TOPasscodeViewControllerAnimatedTransitioning.h -// -// Copyright 2017 Timothy Oliver. All rights reserved. -// -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files (the "Software"), to -// deal in the Software without restriction, including without limitation the -// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or -// sell copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: -// -// The above copyright notice and this permission notice shall be included in -// all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS -// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, -// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR -// IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - -#import -#import - -@class TOPasscodeViewController; - -NS_ASSUME_NONNULL_BEGIN - -/** - An class conforming to `UIViewControllerAnimatedTransitioning` that handles the custom animation - that plays when the passcode view controller is presented on the user's screen. - */ -@interface TOPasscodeViewControllerAnimatedTransitioning : NSObject - -/** The parent passcode view controller that this object will be controlling */ -@property (nonatomic, weak, readonly) TOPasscodeViewController *passcodeViewController; - -/** Whether the controller is being presented or dismissed. The animation is played in reverse when dismissing. */ -@property (nonatomic, assign) BOOL dismissing; - -/** If the correct passcode was successfully entered, this property can be set to YES. When the view controller - is dismissing, the keypad view will also play a zooming out animation to give added context to the dismissal. */ -@property (nonatomic, assign) BOOL success; - -/** - Creates a new instanc of `TOPasscodeViewControllerAnimatedTransitioning` that will control the provided passcode - view controller. - - @param passcodeViewController The passcode view controller in which this object will coordinate the animation upon. - @param dismissing Whether the animation is played to present the view controller, or dismiss it. - @param success Whether the object needs to play an additional zooming animation denoting the passcode was successfully entered. - */ -- (instancetype)initWithPasscodeViewController:(TOPasscodeViewController *)passcodeViewController - dismissing:(BOOL)dismissing - success:(BOOL)success; - -@end - -NS_ASSUME_NONNULL_END diff --git a/iOSClient/Utility/TOPasscodeViewController/Models/TOPasscodeViewControllerAnimatedTransitioning.m b/iOSClient/Utility/TOPasscodeViewController/Models/TOPasscodeViewControllerAnimatedTransitioning.m deleted file mode 100644 index fe5f4c2e4d..0000000000 --- a/iOSClient/Utility/TOPasscodeViewController/Models/TOPasscodeViewControllerAnimatedTransitioning.m +++ /dev/null @@ -1,120 +0,0 @@ -// -// TOPasscodeViewControllerAnimatedTransitioning.m -// -// Copyright 2017 Timothy Oliver. All rights reserved. -// -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files (the "Software"), to -// deal in the Software without restriction, including without limitation the -// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or -// sell copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: -// -// The above copyright notice and this permission notice shall be included in -// all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS -// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, -// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR -// IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - -#import "TOPasscodeViewControllerAnimatedTransitioning.h" -#import "TOPasscodeViewController.h" -#import "TOPasscodeView.h" - -@interface TOPasscodeViewControllerAnimatedTransitioning () -@property (nonatomic, weak) TOPasscodeViewController *passcodeViewController; -@end - -@implementation TOPasscodeViewControllerAnimatedTransitioning - -- (instancetype)initWithPasscodeViewController:(TOPasscodeViewController *)passcodeViewController dismissing:(BOOL)dismissing success:(BOOL)success -{ - if (self = [super init]) { - _passcodeViewController = passcodeViewController; - _dismissing = dismissing; - _success = success; - } - - return self; -} - -- (NSTimeInterval)transitionDuration:(nullable id )transitionContext -{ - return 0.35f; -} - -- (void)animateTransition:(id )transitionContext -{ - BOOL isPhone = [UIDevice currentDevice].userInterfaceIdiom == UIUserInterfaceIdiomPhone; - UIView *containerView = transitionContext.containerView; - UIVisualEffectView *backgroundEffectView = self.passcodeViewController.backgroundEffectView; - UIView *backgroundView = self.passcodeViewController.backgroundView; - UIVisualEffect *backgroundEffect = backgroundEffectView.effect; - TOPasscodeView *passcodeView = self.passcodeViewController.passcodeView; - - // Set the initial properties when presenting - if (!self.dismissing) { - backgroundEffectView.effect = nil; - backgroundView.alpha = 0.0f; - - self.passcodeViewController.view.frame = containerView.bounds; - [containerView addSubview:self.passcodeViewController.view]; - } - else { - UIViewController *baseController = [transitionContext viewControllerForKey:UITransitionContextToViewControllerKey]; - if (baseController.view.superview == nil) { - [containerView insertSubview:baseController.view atIndex:0]; - } - } - - CGFloat alpha = self.dismissing ? 1.0f : 0.0f; - passcodeView.contentAlpha = alpha; - - // Animate the accessory views - if (isPhone) { - self.passcodeViewController.leftAccessoryButton.alpha = alpha; - self.passcodeViewController.rightAccessoryButton.alpha = alpha; - self.passcodeViewController.cancelButton.alpha = alpha; - self.passcodeViewController.biometricButton.alpha = alpha; - } - - id animationBlock = ^{ - backgroundEffectView.effect = self.dismissing ? nil : backgroundEffect; - backgroundView.alpha = self.dismissing ? 0.0f : 1.0f; - - CGFloat toAlpha = self.dismissing ? 0.0f : 1.0f; - passcodeView.contentAlpha = toAlpha; - if (isPhone) { - self.passcodeViewController.leftAccessoryButton.alpha = toAlpha; - self.passcodeViewController.rightAccessoryButton.alpha = toAlpha; - self.passcodeViewController.cancelButton.alpha = toAlpha; - self.passcodeViewController.biometricButton.alpha = toAlpha; - } - }; - - id completedBlock = ^(BOOL completed) { - backgroundEffectView.effect = backgroundEffect; - [transitionContext completeTransition:completed]; - }; - - // If we're animating out from a successful passcode, play a zooming out animation - // to give some more context - if (self.success && self.dismissing) { - CABasicAnimation *animation = [CABasicAnimation animationWithKeyPath:@"transform"]; - animation.duration = [self transitionDuration:transitionContext]; - animation.toValue = [NSValue valueWithCATransform3D:CATransform3DMakeScale(0.9f, 0.9f, 1)]; - animation.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseInEaseOut]; - [passcodeView.layer addAnimation:animation forKey:@"transform"]; - } - - [UIView animateWithDuration:[self transitionDuration:transitionContext] - delay:0.0f - options:UIViewAnimationOptionAllowUserInteraction - animations:animationBlock - completion:completedBlock]; -} - -@end diff --git a/iOSClient/Utility/TOPasscodeViewController/Models/TOSettingsKeypadImage.h b/iOSClient/Utility/TOPasscodeViewController/Models/TOSettingsKeypadImage.h deleted file mode 100644 index 51d2828aae..0000000000 --- a/iOSClient/Utility/TOPasscodeViewController/Models/TOSettingsKeypadImage.h +++ /dev/null @@ -1,54 +0,0 @@ -// -// TOSettingsKeypadImage.h -// -// Copyright 2017 Timothy Oliver. All rights reserved. -// -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files (the "Software"), to -// deal in the Software without restriction, including without limitation the -// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or -// sell copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: -// -// The above copyright notice and this permission notice shall be included in -// all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS -// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, -// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR -// IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - -#import - -NS_ASSUME_NONNULL_BEGIN - -/** - A subclass of `UIImage` that procedurally generates images for `TOPasscodeSettingsKeypadView`. - This includes background images for each keypad button, and a delete icon for the bottom right corner - of the keypad. - */ -@interface TOSettingsKeypadImage : UIImage - -/** - Generates and returns an image of button background with a raised border in a pseudo-skeuomorphic style. - - @param radius The rounded radius of the button image's corners - @param foregroundColor The fill color of the primary section of the button - @param edgeColor The color of the raised border edge along the bottom. - @param thickness The size of the border running along the bottom - */ -+ (UIImage *)buttonImageWithCornerRadius:(CGFloat)radius - foregroundColor:(UIColor *)foregroundColor - edgeColor:(UIColor *)edgeColor - edgeThickness:(CGFloat)thickness; - -/** -Generates and returns a tintable delete icon. - */ -+ (UIImage *)deleteIcon; - -@end - -NS_ASSUME_NONNULL_END diff --git a/iOSClient/Utility/TOPasscodeViewController/Models/TOSettingsKeypadImage.m b/iOSClient/Utility/TOPasscodeViewController/Models/TOSettingsKeypadImage.m deleted file mode 100644 index e361081bd3..0000000000 --- a/iOSClient/Utility/TOPasscodeViewController/Models/TOSettingsKeypadImage.m +++ /dev/null @@ -1,186 +0,0 @@ -// -// TOSettingsKeypadImage.m -// TOPasscodeViewControllerExample -// -// Created by Tim Oliver on 6/20/17. -// Copyright © 2017 Timothy Oliver. All rights reserved. -// - -#import "TOSettingsKeypadImage.h" - -#define TOP_LEFT(X, Y) CGPointMake(rect.origin.x + X * limitedRadius, rect.origin.y + Y * limitedRadius) -#define TOP_RIGHT(X, Y) CGPointMake(rect.origin.x + rect.size.width - X * limitedRadius, rect.origin.y + Y * limitedRadius) -#define BOTTOM_RIGHT(X, Y) CGPointMake(rect.origin.x + rect.size.width - X * limitedRadius, rect.origin.y + rect.size.height - Y * limitedRadius) -#define BOTTOM_LEFT(X, Y) CGPointMake(rect.origin.x + X * limitedRadius, rect.origin.y + rect.size.height - Y * limitedRadius) - -@implementation TOSettingsKeypadImage - -+ (UIImage *)buttonImageWithCornerRadius:(CGFloat)radius - foregroundColor:(UIColor *)foregroundColor - edgeColor:(UIColor *)edgeColor - edgeThickness:(CGFloat)thickness -{ - CGFloat width = (radius * 2.0f) + 1.0f; - CGFloat height = width + thickness; - - CGRect frame = (CGRect){CGPointZero, {width, height}}; - - UIImage *image = nil; - UIGraphicsBeginImageContextWithOptions(frame.size, NO, 0.0f); - { - CGContextRef context = UIGraphicsGetCurrentContext(); - - NSShadow* shadow = [[NSShadow alloc] init]; - shadow.shadowColor = edgeColor; - shadow.shadowOffset = CGSizeMake(0, thickness); - shadow.shadowBlurRadius = 0; - - CGRect buttonFrame = frame; - buttonFrame.size.height -= thickness; - - CGContextSaveGState(context); - { - CGContextSetShadowWithColor(context, shadow.shadowOffset, shadow.shadowBlurRadius, [shadow.shadowColor CGColor]); - UIBezierPath *buttonPath = [[self class] bezierPathWithContinuousRoundedRect:buttonFrame cornerRadius:radius];//bezierPathWithRoundedRect:buttonFrame cornerRadius:radius]; - [foregroundColor setFill]; - [buttonPath fill]; - } - CGContextRestoreGState(context); - - image = UIGraphicsGetImageFromCurrentImageContext(); - } - UIGraphicsEndImageContext(); - - UIEdgeInsets insets = UIEdgeInsetsMake(radius, radius, radius + thickness, radius); - image = [image resizableImageWithCapInsets:insets]; - - return image; -} - -+ (UIImage *)deleteIcon -{ - UIImage *image = nil; - - CGRect frame = CGRectMake(0, 0, 40.0f, 21.0f); - UIGraphicsBeginImageContextWithOptions(frame.size, NO, 0.0f); - { - // DeleteIcon - { - // Border Drawing - UIBezierPath* borderPath = [UIBezierPath bezierPath]; - [borderPath moveToPoint: CGPointMake(25.73, 1.5)]; - [borderPath addLineToPoint: CGPointMake(25.9, 1.53)]; - [borderPath addCurveToPoint: CGPointMake(28.34, 3.46) controlPoint1: CGPointMake(27.03, 1.86) controlPoint2: CGPointMake(27.93, 2.56)]; - [borderPath addCurveToPoint: CGPointMake(28.67, 6.56) controlPoint1: CGPointMake(28.67, 4.28) controlPoint2: CGPointMake(28.67, 5.04)]; - [borderPath addLineToPoint: CGPointMake(28.64, 14.23)]; - [borderPath addCurveToPoint: CGPointMake(28.35, 17.05) controlPoint1: CGPointMake(28.64, 15.76) controlPoint2: CGPointMake(28.64, 16.37)]; - [borderPath addLineToPoint: CGPointMake(28.31, 17.19)]; - [borderPath addCurveToPoint: CGPointMake(25.86, 19.11) controlPoint1: CGPointMake(27.89, 18.08) controlPoint2: CGPointMake(27, 18.79)]; - [borderPath addCurveToPoint: CGPointMake(21.4, 19.37) controlPoint1: CGPointMake(24.82, 19.37) controlPoint2: CGPointMake(23.34, 19.37)]; - [borderPath addLineToPoint: CGPointMake(11.51, 19.37)]; - [borderPath addCurveToPoint: CGPointMake(9.9, 19.07) controlPoint1: CGPointMake(11.51, 19.37) controlPoint2: CGPointMake(10.41, 19.3)]; - [borderPath addCurveToPoint: CGPointMake(7.38, 17.06) controlPoint1: CGPointMake(9.09, 18.68) controlPoint2: CGPointMake(8.52, 18.14)]; - [borderPath addLineToPoint: CGPointMake(3.92, 13.81)]; - [borderPath addCurveToPoint: CGPointMake(1.87, 11.55) controlPoint1: CGPointMake(2.78, 12.73) controlPoint2: CGPointMake(2.21, 12.19)]; - [borderPath addLineToPoint: CGPointMake(1.79, 11.43)]; - [borderPath addCurveToPoint: CGPointMake(1.82, 9.06) controlPoint1: CGPointMake(1.36, 10.57) controlPoint2: CGPointMake(1.4, 9.92)]; - [borderPath addCurveToPoint: CGPointMake(3.96, 6.68) controlPoint1: CGPointMake(2.25, 8.29) controlPoint2: CGPointMake(2.82, 7.76)]; - [borderPath addLineToPoint: CGPointMake(7.21, 3.61)]; - [borderPath addCurveToPoint: CGPointMake(9.61, 1.67) controlPoint1: CGPointMake(8.35, 2.54) controlPoint2: CGPointMake(8.92, 2)]; - [borderPath addLineToPoint: CGPointMake(9.73, 1.6)]; - [borderPath addCurveToPoint: CGPointMake(11.41, 1.31) controlPoint1: CGPointMake(10.26, 1.37) controlPoint2: CGPointMake(10.84, 1.27)]; - [borderPath addLineToPoint: CGPointMake(21.44, 1.27)]; - [borderPath addCurveToPoint: CGPointMake(25.73, 1.5) controlPoint1: CGPointMake(23.38, 1.27) controlPoint2: CGPointMake(24.85, 1.27)]; - [borderPath closePath]; - [UIColor.blackColor setStroke]; - borderPath.lineWidth = 2.5; - [borderPath stroke]; - - - //// Cross Drawing - UIBezierPath* crossPath = [UIBezierPath bezierPath]; - [crossPath moveToPoint: CGPointMake(15.22, 5.9)]; - [crossPath addCurveToPoint: CGPointMake(15.21, 5.88) controlPoint1: CGPointMake(15.27, 5.95) controlPoint2: CGPointMake(15.21, 5.88)]; - [crossPath addLineToPoint: CGPointMake(15.22, 5.9)]; - [crossPath closePath]; - [crossPath moveToPoint: CGPointMake(16.18, 10.28)]; - [crossPath addCurveToPoint: CGPointMake(16.19, 10.26) controlPoint1: CGPointMake(16.22, 10.29) controlPoint2: CGPointMake(16.2, 10.28)]; - [crossPath addLineToPoint: CGPointMake(16.18, 10.28)]; - [crossPath closePath]; - [crossPath moveToPoint: CGPointMake(14.52, 5.35)]; - [crossPath addCurveToPoint: CGPointMake(15.21, 5.88) controlPoint1: CGPointMake(14.75, 5.46) controlPoint2: CGPointMake(14.93, 5.62)]; - [crossPath addCurveToPoint: CGPointMake(15.38, 6.05) controlPoint1: CGPointMake(15.26, 5.94) controlPoint2: CGPointMake(15.32, 5.99)]; - [crossPath addCurveToPoint: CGPointMake(15.43, 6.09) controlPoint1: CGPointMake(15.42, 6.09) controlPoint2: CGPointMake(15.43, 6.09)]; - [crossPath addCurveToPoint: CGPointMake(15.38, 6.05) controlPoint1: CGPointMake(15.21, 5.88) controlPoint2: CGPointMake(15.27, 5.95)]; - [crossPath addCurveToPoint: CGPointMake(17.97, 8.55) controlPoint1: CGPointMake(15.94, 6.59) controlPoint2: CGPointMake(17.66, 8.25)]; - [crossPath addCurveToPoint: CGPointMake(17.97, 8.55) controlPoint1: CGPointMake(17.91, 8.61) controlPoint2: CGPointMake(17.94, 8.58)]; - [crossPath addCurveToPoint: CGPointMake(21.36, 5.39) controlPoint1: CGPointMake(20.95, 5.68) controlPoint2: CGPointMake(21.14, 5.5)]; - [crossPath addCurveToPoint: CGPointMake(22.67, 5.58) controlPoint1: CGPointMake(21.83, 5.17) controlPoint2: CGPointMake(22.34, 5.26)]; - [crossPath addCurveToPoint: CGPointMake(22.98, 6.89) controlPoint1: CGPointMake(23.09, 5.99) controlPoint2: CGPointMake(23.18, 6.47)]; - [crossPath addCurveToPoint: CGPointMake(22.28, 7.68) controlPoint1: CGPointMake(22.84, 7.14) controlPoint2: CGPointMake(22.65, 7.32)]; - [crossPath addCurveToPoint: CGPointMake(19.68, 10.19) controlPoint1: CGPointMake(22.28, 7.68) controlPoint2: CGPointMake(20.88, 9.03)]; - [crossPath addCurveToPoint: CGPointMake(22.97, 13.47) controlPoint1: CGPointMake(22.66, 13.06) controlPoint2: CGPointMake(22.85, 13.25)]; - [crossPath addCurveToPoint: CGPointMake(22.76, 14.79) controlPoint1: CGPointMake(23.21, 13.95) controlPoint2: CGPointMake(23.11, 14.46)]; - [crossPath addCurveToPoint: CGPointMake(21.35, 15.1) controlPoint1: CGPointMake(22.33, 15.22) controlPoint2: CGPointMake(21.8, 15.31)]; - [crossPath addCurveToPoint: CGPointMake(20.48, 14.4) controlPoint1: CGPointMake(21.07, 14.97) controlPoint2: CGPointMake(20.87, 14.78)]; - [crossPath addCurveToPoint: CGPointMake(17.89, 11.91) controlPoint1: CGPointMake(20.48, 14.4) controlPoint2: CGPointMake(19.08, 13.05)]; - [crossPath addCurveToPoint: CGPointMake(14.5, 15.06) controlPoint1: CGPointMake(14.91, 14.78) controlPoint2: CGPointMake(14.73, 14.95)]; - [crossPath addCurveToPoint: CGPointMake(13.2, 14.87) controlPoint1: CGPointMake(14.04, 15.28) controlPoint2: CGPointMake(13.53, 15.19)]; - [crossPath addCurveToPoint: CGPointMake(12.89, 13.57) controlPoint1: CGPointMake(12.78, 14.47) controlPoint2: CGPointMake(12.69, 13.98)]; - [crossPath addCurveToPoint: CGPointMake(13.42, 12.93) controlPoint1: CGPointMake(13, 13.35) controlPoint2: CGPointMake(13.15, 13.19)]; - [crossPath addCurveToPoint: CGPointMake(13.59, 12.77) controlPoint1: CGPointMake(13.47, 12.88) controlPoint2: CGPointMake(13.53, 12.83)]; - [crossPath addCurveToPoint: CGPointMake(16.19, 10.26) controlPoint1: CGPointMake(14.12, 12.25) controlPoint2: CGPointMake(15.78, 10.66)]; - [crossPath addCurveToPoint: CGPointMake(12.89, 6.98) controlPoint1: CGPointMake(13.21, 7.39) controlPoint2: CGPointMake(13.01, 7.2)]; - [crossPath addCurveToPoint: CGPointMake(12.77, 6.63) controlPoint1: CGPointMake(12.82, 6.84) controlPoint2: CGPointMake(12.79, 6.73)]; - [crossPath addCurveToPoint: CGPointMake(13.1, 5.66) controlPoint1: CGPointMake(12.72, 6.28) controlPoint2: CGPointMake(12.83, 5.92)]; - [crossPath addCurveToPoint: CGPointMake(14.52, 5.35) controlPoint1: CGPointMake(13.54, 5.24) controlPoint2: CGPointMake(14.07, 5.15)]; - [crossPath closePath]; - [UIColor.blackColor setFill]; - [crossPath fill]; - } - - image = UIGraphicsGetImageFromCurrentImageContext(); - } - UIGraphicsEndImageContext(); - - return [image imageWithRenderingMode:UIImageRenderingModeAlwaysTemplate]; -} - -/** - Creates a bezier path with the iOS 7 squircle shape. - - A HUGE thanks to the folks at PaintCode for open-sourcing this - https://www.paintcodeapp.com/news/code-for-ios-7-rounded-rectangles - */ -+ (UIBezierPath *)bezierPathWithContinuousRoundedRect:(CGRect)rect cornerRadius:(CGFloat)radius -{ - UIBezierPath* path = UIBezierPath.bezierPath; - CGFloat limit = MIN(rect.size.width, rect.size.height) / 2 / 1.52866483; - CGFloat limitedRadius = MIN(radius, limit); - - [path moveToPoint: TOP_LEFT(1.52866483, 0.00000000)]; - [path addLineToPoint: TOP_RIGHT(1.52866471, 0.00000000)]; - [path addCurveToPoint: TOP_RIGHT(0.66993427, 0.06549600) controlPoint1: TOP_RIGHT(1.08849323, 0.00000000) controlPoint2: TOP_RIGHT(0.86840689, 0.00000000)]; - [path addLineToPoint: TOP_RIGHT(0.63149399, 0.07491100)]; - [path addCurveToPoint: TOP_RIGHT(0.07491176, 0.63149399) controlPoint1: TOP_RIGHT(0.37282392, 0.16905899) controlPoint2: TOP_RIGHT(0.16906013, 0.37282401)]; - [path addCurveToPoint: TOP_RIGHT(0.00000000, 1.52866483) controlPoint1: TOP_RIGHT(0.00000000, 0.86840701) controlPoint2: TOP_RIGHT(0.00000000, 1.08849299)]; - [path addLineToPoint: BOTTOM_RIGHT(0.00000000, 1.52866471)]; - [path addCurveToPoint: BOTTOM_RIGHT(0.06549569, 0.66993493) controlPoint1: BOTTOM_RIGHT(0.00000000, 1.08849323) controlPoint2: BOTTOM_RIGHT(0.00000000, 0.86840689)]; - [path addLineToPoint: BOTTOM_RIGHT(0.07491111, 0.63149399)]; - [path addCurveToPoint: BOTTOM_RIGHT(0.63149399, 0.07491111) controlPoint1: BOTTOM_RIGHT(0.16905883, 0.37282392) controlPoint2: BOTTOM_RIGHT(0.37282392, 0.16905883)]; - [path addCurveToPoint: BOTTOM_RIGHT(1.52866471, 0.00000000) controlPoint1: BOTTOM_RIGHT(0.86840689, 0.00000000) controlPoint2: BOTTOM_RIGHT(1.08849323, 0.00000000)]; - [path addLineToPoint: BOTTOM_LEFT(1.52866483, 0.00000000)]; - [path addCurveToPoint: BOTTOM_LEFT(0.66993397, 0.06549569) controlPoint1: BOTTOM_LEFT(1.08849299, 0.00000000) controlPoint2: BOTTOM_LEFT(0.86840701, 0.00000000)]; - [path addLineToPoint: BOTTOM_LEFT(0.63149399, 0.07491111)]; - [path addCurveToPoint: BOTTOM_LEFT(0.07491100, 0.63149399) controlPoint1: BOTTOM_LEFT(0.37282401, 0.16905883) controlPoint2: BOTTOM_LEFT(0.16906001, 0.37282392)]; - [path addCurveToPoint: BOTTOM_LEFT(0.00000000, 1.52866471) controlPoint1: BOTTOM_LEFT(0.00000000, 0.86840689) controlPoint2: BOTTOM_LEFT(0.00000000, 1.08849323)]; - [path addLineToPoint: TOP_LEFT(0.00000000, 1.52866483)]; - [path addCurveToPoint: TOP_LEFT(0.06549600, 0.66993397) controlPoint1: TOP_LEFT(0.00000000, 1.08849299) controlPoint2: TOP_LEFT(0.00000000, 0.86840701)]; - [path addLineToPoint: TOP_LEFT(0.07491100, 0.63149399)]; - [path addCurveToPoint: TOP_LEFT(0.63149399, 0.07491100) controlPoint1: TOP_LEFT(0.16906001, 0.37282401) controlPoint2: TOP_LEFT(0.37282401, 0.16906001)]; - [path addCurveToPoint: TOP_LEFT(1.52866483, 0.00000000) controlPoint1: TOP_LEFT(0.86840701, 0.00000000) controlPoint2: TOP_LEFT(1.08849299, 0.00000000)]; - [path closePath]; - return path; -} - -@end diff --git a/iOSClient/Utility/TOPasscodeViewController/Supporting/TOPasscodeViewControllerConstants.h b/iOSClient/Utility/TOPasscodeViewController/Supporting/TOPasscodeViewControllerConstants.h deleted file mode 100644 index 3447370059..0000000000 --- a/iOSClient/Utility/TOPasscodeViewController/Supporting/TOPasscodeViewControllerConstants.h +++ /dev/null @@ -1,71 +0,0 @@ -// -// TOPasscodeViewControllerConstants.h -// -// Copyright 2017 Timothy Oliver. All rights reserved. -// -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files (the "Software"), to -// deal in the Software without restriction, including without limitation the -// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or -// sell copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: -// -// The above copyright notice and this permission notice shall be included in -// all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS -// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, -// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR -// IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - -/* The visual style of the asscode view controller */ -typedef NS_ENUM(NSInteger, TOPasscodeViewStyle) { - TOPasscodeViewStyleTranslucentDark, - TOPasscodeViewStyleTranslucentLight, - TOPasscodeViewStyleOpaqueDark, - TOPasscodeViewStyleOpaqueLight -}; - -/* The visual style of the passcode settings view controller. */ -typedef NS_ENUM(NSInteger, TOPasscodeSettingsViewStyle) { - TOPasscodeSettingsViewStyleLight, - TOPasscodeSettingsViewStyleDark -}; - -/* Depending on the amount of horizontal space, the sizing of the elements */ -typedef NS_ENUM(NSInteger, TOPasscodeViewContentSize) { - TOPasscodeViewContentSizeDefault = 414, // Default, 414 points and above (6 Plus, all remaining iPad sizes) - TOPasscodeViewContentSizeMedium = 375, // Greater or equal to 375 points: iPhone 6 / iPad Pro 1/4 split mode - TOPasscodeViewContentSizeSmall = 320 // Greater or equal to 320 points: iPhone SE / iPad 1/4 split mode -}; - -/* The types of passcodes that may be used. */ -typedef NS_ENUM(NSInteger, TOPasscodeType) { - TOPasscodeTypeFourDigits, // 4 Numbers - TOPasscodeTypeSixDigits, // 6 Numbers - TOPasscodeTypeCustomNumeric, // Any length of numbers - TOPasscodeTypeCustomAlphanumeric // Any length of characters -}; - -/* The type of biometrics this controller can handle */ -typedef NS_ENUM(NSInteger, TOPasscodeBiometryType) { - TOPasscodeBiometryTypeTouchID, - TOPasscodeBiometryTypeFaceID -}; - -static inline BOOL TOPasscodeViewStyleIsTranslucent(TOPasscodeViewStyle style) { - return style <= TOPasscodeViewStyleTranslucentLight; -} - -static inline BOOL TOPasscodeViewStyleIsDark(TOPasscodeViewStyle style) { - return style < TOPasscodeViewStyleTranslucentLight || style == TOPasscodeViewStyleOpaqueDark; -} - -static inline NSString *TOPasscodeBiometryTitleForType(TOPasscodeBiometryType type) { - switch (type) { - case TOPasscodeBiometryTypeFaceID: return NSLocalizedString(@"Face ID", @""); - default: return NSLocalizedString(@"Touch ID", @""); - } -} diff --git a/iOSClient/Utility/TOPasscodeViewController/TOPasscodeSettingsViewController.h b/iOSClient/Utility/TOPasscodeViewController/TOPasscodeSettingsViewController.h deleted file mode 100644 index 656e617dc7..0000000000 --- a/iOSClient/Utility/TOPasscodeViewController/TOPasscodeSettingsViewController.h +++ /dev/null @@ -1,108 +0,0 @@ -// -// TOPasscodeSettingsViewController.h -// -// Copyright 2017 Timothy Oliver. All rights reserved. -// -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files (the "Software"), to -// deal in the Software without restriction, including without limitation the -// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or -// sell copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: -// -// The above copyright notice and this permission notice shall be included in -// all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS -// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, -// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR -// IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - -#import -#import "TOPasscodeViewControllerConstants.h" - -@class TOPasscodeSettingsViewController; - -typedef NS_ENUM(NSInteger, TOPasscodeSettingsViewState) { - TOPasscodeSettingsViewStateEnterCurrentPasscode, - TOPasscodeSettingsViewStateEnterNewPasscode, - TOPasscodeSettingsViewStateConfirmNewPasscode -}; - -NS_ASSUME_NONNULL_BEGIN - -/** - A delegate object in charge of validating and recording the passcodes entered by the user. - */ -@protocol TOPasscodeSettingsViewControllerDelegate - -@optional - -/** Called when the user was prompted to input their current passcode. - Return YES if passcode was right and NO otherwise. - - Returning NO will cause a warning label to appear - */ -- (BOOL)passcodeSettingsViewController:(TOPasscodeSettingsViewController *)passcodeSettingsViewController - didAttemptCurrentPasscode:(NSString *)passcode; - -/** Called when the user has successfully set a new passcode. At this point, you should save over - the old passcode with the new one. */ -- (void)passcodeSettingsViewController:(TOPasscodeSettingsViewController *)passcodeSettingsViewController - didChangeToNewPasscode:(NSString *)passcode ofType:(TOPasscodeType)type; - -@end - -// ---------------------------------------------------------------------- - -/** - A standard system-styled view controller that users can use to change the passcode - that they will need to enter for the main passcode view controller. - - This controller allows requiring the user to enter their previous passcode in first, - and has passcode validation by requiring them to enter the new passcode twice. - */ - -@interface TOPasscodeSettingsViewController : UIViewController - -/** Delegate event for controlling and responding to the behavior of this controller */ -@property (nonatomic, weak, nullable) id delegate; - -/** The current state of the controller (confirming old passcode or creating a new one) */ -@property (nonatomic, assign) TOPasscodeSettingsViewState state; - -/** The input type of the passcode */ -@property (nonatomic, assign) TOPasscodeType passcodeType; - -/** The number of incorrect passcode attempts the user has made. Use this property to decide when to disable input. */ -@property (nonatomic, assign) NSInteger failedPasscodeAttemptCount; - -/** Before setting a new passcode, show a UI to validate the existing passcode. (Default is NO) */ -@property (nonatomic, assign) BOOL requireCurrentPasscode; - -/** If set, the view controller will disable input until this date time has been reached */ -@property (nonatomic, strong, nullable) NSDate *disabledInputDate; - -/** Hide the button Options (Default is NO) */ -@property (nonatomic, assign) BOOL hideOptionsButton; - -/* - Create a new instance with the desird light or dark style - - @param style The visual style of the view controller - */ -- (instancetype)init; - -/* - Changes the passcode type and animates if required - - @param passcodeType Change the type of passcode to enter. - @param animated Play a crossfade animation. - */ -- (void)setPasscodeType:(TOPasscodeType)passcodeType animated:(BOOL)animated; - -@end - -NS_ASSUME_NONNULL_END diff --git a/iOSClient/Utility/TOPasscodeViewController/TOPasscodeSettingsViewController.m b/iOSClient/Utility/TOPasscodeViewController/TOPasscodeSettingsViewController.m deleted file mode 100644 index 707f2567dc..0000000000 --- a/iOSClient/Utility/TOPasscodeViewController/TOPasscodeSettingsViewController.m +++ /dev/null @@ -1,630 +0,0 @@ -// -// TOPasscodeSettingsViewController.m -// -// Copyright 2017 Timothy Oliver. All rights reserved. -// -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files (the "Software"), to -// deal in the Software without restriction, including without limitation the -// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or -// sell copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: -// -// The above copyright notice and this permission notice shall be included in -// all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS -// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, -// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR -// IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - -#import "TOPasscodeSettingsViewController.h" -#import "TOPasscodeInputField.h" -#import "TOPasscodeSettingsKeypadView.h" -#import "TOPasscodeSettingsWarningLabel.h" - -const CGFloat kTOPasscodeSettingsLabelInputSpacing = 15.0f; -const CGFloat kTOPasscodeSettingsOptionsButtonOffset = 15.0f; -const CGFloat kTOPasscodeKeypadMaxSizeRatio = 0.40f; -const CGFloat kTOPasscodeKeypadMinHeight = 185.0f; // was 165 -const CGFloat kTOPasscodeKeypadMaxHeight = 330.0f; - -@interface TOPasscodeSettingsViewController () - -@property (nonatomic, copy) NSString *potentialPasscode; - -/* Layout Calculations */ -@property (nonatomic, assign) CGFloat verticalMidPoint; -@property (nonatomic, assign) CGRect keyboardFrame; -@property (nonatomic, readonly) CGRect contentOverlapFrame; // Either the keypad or the system keyboard - -/* Views */ -@property (nonatomic, strong) UIView *containerView; -@property (nonatomic, strong) UILabel *titleLabel; -@property (nonatomic, strong) UILabel *errorLabel; -@property (nonatomic, strong) UIButton *optionsButton; -@property (nonatomic, strong) TOPasscodeInputField *inputField; -@property (nonatomic, strong) TOPasscodeSettingsKeypadView *keypadView; -@property (nonatomic, strong) TOPasscodeSettingsWarningLabel *warningLabel; - -/* Bar Items */ -@property (nonatomic, strong) UIBarButtonItem *nextBarButtonItem; -@property (nonatomic, strong) UIBarButtonItem *doneBarButtonItem; - -/* Style */ -@property (nonatomic, assign) TOPasscodeSettingsViewStyle style; - -@end - -@implementation TOPasscodeSettingsViewController - -#pragma mark - Object Creation - - -- (instancetype)init -{ - if (self = [self initWithNibName:nil bundle:nil]) { - [self setUp]; - } - - return self; -} - -- (instancetype)initWithNibName:(NSString *)nibNameOrNil bundle:(NSBundle *)nibBundleOrNil -{ - if (self = [super initWithNibName:nibNameOrNil bundle:nibBundleOrNil]) { - [self setUp]; - } - - return self; -} - -- (void)setUp -{ - if (@available(iOS 13.0, *)) { - if ([self.traitCollection userInterfaceStyle] == UIUserInterfaceStyleDark) { - self.style = TOPasscodeSettingsViewStyleDark; - } else { - self.style = TOPasscodeSettingsViewStyleLight; - } - } else { - self.style = TOPasscodeSettingsViewStyleLight; - } - - [self applyThemeForStyle:_style]; - - _failedPasscodeAttemptCount = 0; - [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(keyboardWillChangeFrame:) name:UIKeyboardWillChangeFrameNotification object:nil]; -} - -- (void)dealloc -{ - [[NSNotificationCenter defaultCenter] removeObserver:self name:UIKeyboardWillChangeFrameNotification object:nil]; -} - -#pragma mark - View Set-up - - -- (void)viewDidLoad { - [super viewDidLoad]; - - __weak typeof(self) weakSelf = self; - - self.title = NSLocalizedString(@"Enter Passcode", @""); - - // Create container view - self.containerView = [[UIView alloc] initWithFrame:CGRectZero]; - self.containerView.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleTopMargin - | UIViewAutoresizingFlexibleBottomMargin; - [self.view addSubview:self.containerView]; - - // Create title label - self.titleLabel = [[UILabel alloc] initWithFrame:CGRectZero]; - self.titleLabel.font = [UIFont systemFontOfSize:17.0f]; - self.titleLabel.textAlignment = NSTextAlignmentCenter; - self.titleLabel.textColor = [UIColor blackColor]; - self.titleLabel.text = @"Enter your passcode"; - self.titleLabel.autoresizingMask = UIViewAutoresizingFlexibleLeftMargin | UIViewAutoresizingFlexibleRightMargin; - [self.titleLabel sizeToFit]; - [self.containerView addSubview:self.titleLabel]; - - // Create number view - self.inputField = [[TOPasscodeInputField alloc] init]; - self.inputField.tintColor = [UIColor blackColor]; - self.inputField.autoresizingMask = UIViewAutoresizingFlexibleLeftMargin | UIViewAutoresizingFlexibleRightMargin; - self.inputField.passcodeCompletedHandler = ^(NSString *passcode) { [weakSelf inputViewDidCompletePasscode:passcode]; }; - [self.inputField sizeToFit]; - [self.containerView addSubview:self.inputField]; - - // Create keypad view - self.keypadView = [[TOPasscodeSettingsKeypadView alloc] initWithFrame:CGRectZero]; - self.keypadView.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleTopMargin; - [self.view addSubview:self.keypadView]; - - // Create warning label view - self.warningLabel = [[TOPasscodeSettingsWarningLabel alloc] initWithFrame:CGRectZero]; - self.warningLabel.autoresizingMask = UIViewAutoresizingFlexibleLeftMargin | UIViewAutoresizingFlexibleRightMargin; - self.warningLabel.hidden = YES; - [self.warningLabel sizeToFit]; - [self.containerView addSubview:self.warningLabel]; - - // Create error label view - self.errorLabel = [[UILabel alloc] initWithFrame:CGRectZero]; - self.errorLabel.text = NSLocalizedString(@"Passcodes didn't match. Try again.", @""); - self.errorLabel.textAlignment = NSTextAlignmentCenter; - self.errorLabel.font = [UIFont systemFontOfSize:15.0f]; - self.errorLabel.numberOfLines = 0; - self.errorLabel.hidden = YES; - [self.errorLabel sizeToFit]; - [self.containerView addSubview:self.errorLabel]; - - // Create Options button - self.optionsButton = [UIButton buttonWithType:UIButtonTypeSystem]; - [self.optionsButton setTitle:NSLocalizedString(@"Passcode Options", @"") forState:UIControlStateNormal]; - self.optionsButton.titleLabel.font = [UIFont systemFontOfSize:15.0f]; - [self.optionsButton sizeToFit]; - self.optionsButton.hidden = _hideOptionsButton; - [self.optionsButton addTarget:self action:@selector(optionsCodeButtonTapped:) forControlEvents:UIControlEventTouchUpInside]; - [self.view addSubview:self.optionsButton]; - - // Add callbacks for the keypad view - self.keypadView.numberButtonTappedHandler = ^(NSInteger number) { - NSString *numberString = [NSString stringWithFormat:@"%ld", (long)number]; - [weakSelf.inputField appendPasscodeCharacters:numberString animated:NO]; - }; - - self.keypadView.deleteButtonTappedHandler = ^{ [weakSelf.inputField deletePasscodeCharactersOfCount:1 animated:NO]; }; - - // Set height of the container view (This will never change) - CGRect frame = self.containerView.frame; - frame.size.width = self.view.bounds.size.width; - frame.size.height = CGRectGetHeight(self.titleLabel.frame) + CGRectGetHeight(self.inputField.frame) - + CGRectGetHeight(self.warningLabel.frame) + (kTOPasscodeSettingsLabelInputSpacing * 2.0f); - self.containerView.frame = CGRectIntegral(frame); - - //Work out the vertical offset of the container view assuming the warning label doesn't count - self.verticalMidPoint = CGRectGetHeight(self.titleLabel.frame) + CGRectGetHeight(self.inputField.frame) - + kTOPasscodeSettingsLabelInputSpacing; - self.verticalMidPoint *= 0.5f; - - // Bar button items - self.nextBarButtonItem = [[UIBarButtonItem alloc] initWithTitle:NSLocalizedString(@"Next", @"") style:UIBarButtonItemStylePlain target:self action:@selector(nextButtonTapped:)]; - self.doneBarButtonItem = [[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemDone target:self action:@selector(doneButtonTapped:)]; - - // Apply light/dark mode - [self applyThemeForStyle:self.style]; -} - -- (void)viewWillAppear:(BOOL)animated -{ - [super viewWillAppear:animated]; - - self.state = self.requireCurrentPasscode ? TOPasscodeSettingsViewStateEnterCurrentPasscode : TOPasscodeSettingsViewStateEnterNewPasscode; - [self updateContentForState:self.state type:self.passcodeType animated:NO]; -} - -#pragma mark - View Update - - -- (void)updateContentForState:(TOPasscodeSettingsViewState)state type:(TOPasscodeType)type animated:(BOOL)animated -{ - BOOL variableSizePasscode = (type >= TOPasscodeTypeCustomNumeric); - - // Update the visibility of the options button - if (_hideOptionsButton) { - self.optionsButton.hidden = YES; - } else { - self.optionsButton.hidden = !(state == TOPasscodeSettingsViewStateEnterNewPasscode); - } - - // Clear the input view - self.inputField.passcode = nil; - - // Disable the input view - self.inputField.enabled = NO; - - //Update the warning label - [self updateWarningLabelForState:state]; - - // Change the input view if needed - if (!variableSizePasscode) { - self.inputField.style = TOPasscodeInputFieldStyleFixed; - self.inputField.fixedInputView.length = (self.passcodeType == TOPasscodeTypeSixDigits) ? 6 : 4; - } - else { - self.inputField.style = TOPasscodeInputFieldStyleVariable; - } - - // Update text depending on state - switch (state) { - case TOPasscodeSettingsViewStateEnterCurrentPasscode: - self.titleLabel.text = NSLocalizedString(@"Enter your passcode", @""); - self.navigationItem.rightBarButtonItem = variableSizePasscode ? self.nextBarButtonItem : nil; - if (@available(iOS 9.0, *)) { - self.inputField.returnKeyType = UIReturnKeyContinue; - } - else { - self.inputField.returnKeyType = UIReturnKeyNext; - } - break; - case TOPasscodeSettingsViewStateEnterNewPasscode: - self.titleLabel.text = NSLocalizedString(@"Enter a new passcode", @""); - self.navigationItem.rightBarButtonItem = variableSizePasscode ? self.nextBarButtonItem : nil; - if (@available(iOS 9.0, *)) { - self.inputField.returnKeyType = UIReturnKeyContinue; - } - else { - self.inputField.returnKeyType = UIReturnKeyNext; - } - break; - case TOPasscodeSettingsViewStateConfirmNewPasscode: - self.titleLabel.text = NSLocalizedString(@"Confirm new passcode", @""); - self.navigationItem.rightBarButtonItem = variableSizePasscode ? self.doneBarButtonItem : nil; - self.inputField.returnKeyType = UIReturnKeyDone; - break; - } - - CGRect frame = CGRectZero; - - // Reload the 'Done' button - [self.inputField reloadInputViews]; - - // Resize text label to fit new text - [self.titleLabel sizeToFit]; - frame = self.titleLabel.frame; - frame.origin.x = (CGRectGetWidth(self.containerView.frame) - CGRectGetWidth(frame)) * 0.5f; - self.titleLabel.frame = CGRectIntegral(frame); - - // Resize passcode view - [self.inputField sizeToFit]; - frame = self.inputField.frame; - frame.origin.x = (CGRectGetWidth(self.containerView.frame) - CGRectGetWidth(frame)) * 0.5f; - self.inputField.frame = CGRectIntegral(frame); - - // If we're the alphanumeric type, present the keyboard - if (type == TOPasscodeTypeCustomAlphanumeric) { - self.inputField.enabled = YES; - [self.inputField becomeFirstResponder]; - } - else { - if (self.inputField.isFirstResponder) { - [self.inputField resignFirstResponder]; - } - } - - // If not animated, force a blanket re-layout - if (!animated) { - [self viewDidLayoutSubviews]; - return; - } - - // If animated, perform the animation - [UIView animateWithDuration:0.3f animations:^{ - [self viewDidLayoutSubviews]; - }]; -} - -- (void)updateWarningLabelForState:(TOPasscodeSettingsViewState)state -{ - BOOL confirmingPasscode = state == TOPasscodeSettingsViewStateEnterCurrentPasscode; - - // Update the warning label - self.warningLabel.hidden = !(confirmingPasscode && self.failedPasscodeAttemptCount > 0); - self.warningLabel.numberOfWarnings = self.failedPasscodeAttemptCount; - - CGRect frame = self.warningLabel.frame; - frame.origin.x = (CGRectGetWidth(self.view.frame) - frame.size.width) * 0.5f; - self.warningLabel.frame = frame; -} - -- (void)transitionToState:(TOPasscodeSettingsViewState)state animated:(BOOL)animated -{ - // Preserve the current view state - UIView *snapshot = nil; - - BOOL reverseDirection = state < self.state; - - // If animated, take a snapshot of the current container view - if (animated) { - snapshot = [self.containerView snapshotViewAfterScreenUpdates:NO]; - snapshot.frame = self.containerView.frame; - [self.view addSubview:snapshot]; - } - - self.errorLabel.hidden = YES; - - // Update the layout for the new state - self.state = state; - - // Cancel out now if we're not animating - if (!animated) { - return; - } - - // Place the live container off screen to the right - CGFloat multiplier = reverseDirection ? -1.0f : 1.0f; - self.containerView.frame = CGRectOffset(self.containerView.frame, self.view.frame.size.width * multiplier, 0.0f); - - // Update the options button alpha depending on transition state - self.optionsButton.hidden = _hideOptionsButton; - self.optionsButton.alpha = (state == TOPasscodeSettingsViewStateEnterNewPasscode) ? 0.0f : 1.0f; - - // Perform an animation where the snapshot slides off, and the new container slides in - id animationBlock = ^{ - snapshot.frame = CGRectOffset(snapshot.frame, -self.view.frame.size.width * multiplier, 0.0f); - self.containerView.frame = CGRectOffset(self.containerView.frame, -self.view.frame.size.width * multiplier, 0.0f); - self.optionsButton.alpha = (state == TOPasscodeSettingsViewStateEnterNewPasscode) ? 1.0f : 0.0f; - }; - - // Clean up by removing the snapshot view - id completionBlock = ^(BOOL complete) { - [snapshot removeFromSuperview]; - }; - - // Perform the animation - [UIView animateWithDuration:0.4f - delay:0.0f - usingSpringWithDamping:1.0f - initialSpringVelocity:0.7f - options:0 - animations:animationBlock - completion:completionBlock]; -} - -- (void)viewDidLayoutSubviews -{ - [super viewDidLayoutSubviews]; - - CGSize viewSize = self.view.bounds.size; - - // Layout the keypad view - CGRect frame = self.keypadView.frame; - frame.size.height = viewSize.height * kTOPasscodeKeypadMaxSizeRatio; - frame.size.height = MAX(frame.size.height, kTOPasscodeKeypadMinHeight); - frame.size.height = MIN(frame.size.height, kTOPasscodeKeypadMaxHeight); - frame.size.width = viewSize.width; - frame.origin.y = viewSize.height; - if (self.passcodeType != TOPasscodeTypeCustomAlphanumeric) { - frame.origin.y -= frame.size.height; - } - - self.keypadView.frame = CGRectIntegral(frame); - - BOOL horizontalLayout = frame.size.height < kTOPasscodeKeypadMinHeight + FLT_EPSILON; - BOOL animated = ([self.view.layer animationForKey:@"bounds.size"] != nil); - [self.keypadView setButtonLabelHorizontalLayout:horizontalLayout animated:animated]; - - CGFloat topContentHeight = self.view.safeAreaInsets.top; - - // Layout the container view - frame = self.containerView.frame; - frame.origin.y = (((viewSize.height - (topContentHeight + self.contentOverlapFrame.size.height))) * 0.5f) - self.verticalMidPoint; - frame.origin.y += topContentHeight; - self.containerView.frame = CGRectIntegral(frame); - - // Layout the passcode options button - frame = self.optionsButton.frame; - frame.origin.y = CGRectGetMinY(self.contentOverlapFrame) - kTOPasscodeSettingsOptionsButtonOffset - CGRectGetHeight(frame); - frame.origin.x = (CGRectGetWidth(self.view.frame) - CGRectGetWidth(frame)) * 0.5f; - self.optionsButton.frame = frame; - - // Set frame of title label - frame = self.titleLabel.frame; - frame.origin.x = (CGRectGetWidth(self.view.frame) - CGRectGetWidth(frame)) * 0.5f; - self.titleLabel.frame = CGRectIntegral(frame); - - // Set frame of number pad - frame = self.inputField.frame; - frame.origin.x = (CGRectGetWidth(self.view.frame) - CGRectGetWidth(frame)) * 0.5f; - frame.origin.y = (CGRectGetHeight(self.titleLabel.frame) + kTOPasscodeSettingsLabelInputSpacing); - self.inputField.frame = CGRectIntegral(frame); - - // Set the frame for the warning view - frame = self.warningLabel.frame; - frame.origin.x = (CGRectGetWidth(self.view.frame) - CGRectGetWidth(frame)) * 0.5f; - frame.origin.y = CGRectGetMaxY(self.inputField.frame) + kTOPasscodeSettingsLabelInputSpacing; - self.warningLabel.frame = CGRectIntegral(frame); - - // Set the frame of the error view - frame = self.errorLabel.frame; - frame.size = [self.errorLabel sizeThatFits:CGSizeMake(300.0f, CGFLOAT_MAX)]; - frame.origin.y = CGRectGetMaxY(self.inputField.frame) + kTOPasscodeSettingsLabelInputSpacing; - frame.origin.x = (CGRectGetWidth(self.containerView.frame) - CGRectGetWidth(frame)) * 0.5f; - self.errorLabel.frame = CGRectIntegral(frame); -} - -- (void)applyThemeForStyle:(TOPasscodeSettingsViewStyle)style -{ - BOOL isDark = (style == TOPasscodeSettingsViewStyleDark); - - // Set background color - UIColor *backgroundColor; - if (isDark) { - backgroundColor = [UIColor colorWithWhite:0.15f alpha:1.0f]; - } - else { - backgroundColor = [UIColor colorWithRed:235.0f/255.0f green:235.0f/255.0f blue:241.0f/255.0f alpha:1.0f]; - } - self.view.backgroundColor = backgroundColor; - - // Set the style of the keypad view - self.keypadView.style = style; - - // Set the color for the input content - UIColor *inputColor = isDark ? [UIColor whiteColor] : [UIColor blackColor]; - - // Set the label style - self.titleLabel.textColor = inputColor; - - // Set the number input tint - self.inputField.tintColor = inputColor; - - // Set the tint color of the incorrect warning label - UIColor *warningColor = nil; - if (isDark) { - warningColor = [UIColor colorWithRed:214.0f/255.0f green:63.0f/255.0f blue:63.0f/255.0f alpha:1.0f]; - } - else { - warningColor = [UIColor colorWithRed:214.0f/255.0f green:63.0f/255.0f blue:63.0f/255.0f alpha:1.0f]; - } -} - -#pragma mark - Data Management - -- (void)inputViewDidCompletePasscode:(NSString *)passcode -{ - switch (self.state) { - case TOPasscodeSettingsViewStateEnterCurrentPasscode: - [self validateCurrentPasscodeAttemptWithPasscode:passcode]; - break; - case TOPasscodeSettingsViewStateEnterNewPasscode: - [self didReceiveNewPasscode:passcode]; - break; - case TOPasscodeSettingsViewStateConfirmNewPasscode: - [self confirmNewPasscode:passcode]; - break; - } -} - -- (void)validateCurrentPasscodeAttemptWithPasscode:(NSString *)passcode -{ - if (![self.delegate respondsToSelector:@selector(passcodeSettingsViewController:didAttemptCurrentPasscode:)]) { - return; - } - - BOOL correct = [self.delegate passcodeSettingsViewController:self didAttemptCurrentPasscode:passcode]; - if (!correct) { - [self.inputField resetPasscodeAnimated:YES playImpact:YES]; - self.failedPasscodeAttemptCount++; - } - else { - [self transitionToState:TOPasscodeSettingsViewStateEnterNewPasscode animated:YES]; - } -} - -- (void)didReceiveNewPasscode:(NSString *)passcode -{ - self.potentialPasscode = passcode; - [self transitionToState:TOPasscodeSettingsViewStateConfirmNewPasscode animated:YES]; -} - -- (void)confirmNewPasscode:(NSString *)passcode -{ - if (![passcode isEqualToString:self.potentialPasscode]) { - [self transitionToState:TOPasscodeSettingsViewStateEnterNewPasscode animated:YES]; - self.errorLabel.hidden = NO; - return; - } - - if (![self.delegate respondsToSelector:@selector(passcodeSettingsViewController:didChangeToNewPasscode:ofType:)]) { - return; - } - - [self.delegate passcodeSettingsViewController:self didChangeToNewPasscode:self.potentialPasscode ofType:self.passcodeType]; -} - -#pragma mark - System Keyboard Handling - -- (void)keyboardWillChangeFrame:(NSNotification *)notification -{ - self.keyboardFrame = [notification.userInfo[UIKeyboardFrameEndUserInfoKey] CGRectValue]; - [self viewDidLayoutSubviews]; -} - -- (CGRect)contentOverlapFrame -{ - if (self.passcodeType < TOPasscodeTypeCustomAlphanumeric) { - return self.keypadView.frame; - } - - // Work out where our view is in relation to the screen - UIWindow *window = self.view.window; - CGRect viewFrame = [self.view.superview convertRect:self.view.frame toView:window]; - - CGFloat overlap = CGRectGetMaxY(viewFrame) - CGRectGetMinY(self.keyboardFrame); - - CGRect overlapFrame = self.keyboardFrame; - overlapFrame.origin.y = MIN(viewFrame.size.height - overlap, viewFrame.size.height); - overlapFrame.size.height = MAX(overlap, 0.0f); - return overlapFrame; -} - -#pragma mark - Button Callbacks - - -- (void)optionsCodeButtonTapped:(id)sender -{ - UIAlertController *alertController = [UIAlertController alertControllerWithTitle:nil message:nil preferredStyle:UIAlertControllerStyleActionSheet]; - UIAlertActionStyle style = UIAlertActionStyleDefault; - - __weak typeof(self) weakSelf = self; - - NSArray *types = @[@(TOPasscodeTypeFourDigits), - @(TOPasscodeTypeSixDigits), - @(TOPasscodeTypeCustomNumeric), - @(TOPasscodeTypeCustomAlphanumeric) - ]; - - - NSArray *titles = @[NSLocalizedString(@"4-Digit Numeric Code", @""), - NSLocalizedString(@"6-Digit Numeric Code", @""), - NSLocalizedString(@"Custom Numeric Code", @""), - NSLocalizedString(@"Custom Alphanumeric Code", @"")]; - - // Add all the buttons - for (NSInteger i = 0; i < types.count; i++) { - TOPasscodeType type = [types[i] integerValue]; - if (type == self.passcodeType) { continue; } - - id handler = ^(UIAlertAction *action) { - [weakSelf setPasscodeType:type]; - }; - [alertController addAction:[UIAlertAction actionWithTitle:titles[i] style:style handler:handler]]; - } - - // Cancel button - [alertController addAction:[UIAlertAction actionWithTitle:NSLocalizedString(@"Cancel", @"") style:UIAlertActionStyleCancel handler:nil]]; - - alertController.modalPresentationStyle = UIModalPresentationPopover; - alertController.popoverPresentationController.sourceView = self.optionsButton; - alertController.popoverPresentationController.sourceRect = self.optionsButton.bounds; - alertController.popoverPresentationController.permittedArrowDirections = UIPopoverArrowDirectionDown | UIPopoverArrowDirectionUp; - [self presentViewController:alertController animated:YES completion:nil]; -} - -- (void)nextButtonTapped:(id)sender -{ - [self inputViewDidCompletePasscode:self.inputField.passcode]; -} - -- (void)doneButtonTapped:(id)sender -{ - [self inputViewDidCompletePasscode:self.inputField.passcode]; -} - -#pragma mark - Accessors - -- (void)setPasscodeType:(TOPasscodeType)passcodeType -{ - [self setPasscodeType:passcodeType animated:NO]; -} - -- (void)setPasscodeType:(TOPasscodeType)passcodeType animated:(BOOL)animated -{ - if (_passcodeType == passcodeType) { return; } - _passcodeType = passcodeType; - - [self updateContentForState:self.state type:_passcodeType animated:animated]; -} - -- (void)setState:(TOPasscodeSettingsViewState)state -{ - if (_state == state) { return; } - _state = state; - - [self updateContentForState:_state type:self.passcodeType animated:NO]; -} - -- (void)setFailedPasscodeAttemptCount:(NSInteger)failedPasscodeAttemptCount -{ - if (_failedPasscodeAttemptCount == failedPasscodeAttemptCount) { return; } - _failedPasscodeAttemptCount = failedPasscodeAttemptCount; - [self updateWarningLabelForState:self.state]; -} - -@end diff --git a/iOSClient/Utility/TOPasscodeViewController/TOPasscodeViewController.h b/iOSClient/Utility/TOPasscodeViewController/TOPasscodeViewController.h deleted file mode 100644 index f4f6a7023d..0000000000 --- a/iOSClient/Utility/TOPasscodeViewController/TOPasscodeViewController.h +++ /dev/null @@ -1,163 +0,0 @@ -// -// TOPasscodeViewController.h -// -// Copyright 2017 Timothy Oliver. All rights reserved. -// -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files (the "Software"), to -// deal in the Software without restriction, including without limitation the -// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or -// sell copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: -// -// The above copyright notice and this permission notice shall be included in -// all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS -// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, -// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR -// IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - -#import -#import "TOPasscodeViewControllerConstants.h" -#import "TOPasscodeSettingsViewController.h" -#import "TOPasscodeView.h" - -NS_ASSUME_NONNULL_BEGIN - -@class TOPasscodeViewController; - -/** - A delegate object in charge of validating the passcodes that the user has entered into the passcode - view controller. - */ -@protocol TOPasscodeViewControllerDelegate - -@optional - -/** - Return YES if the user entered the expected PIN code. Return NO if it was incorrect. - (For security reasons, it is safer to fetch the saved PIN code only when this method is called, and - then discard it immediately. This is why the view controller does not directly store it.) -*/ -- (BOOL)passcodeViewController:(TOPasscodeViewController *)passcodeViewController isCorrectCode:(NSString *)code; - -/** The user tapped the 'Cancel' button. Any dismissing of confidential content should be done in here. */ -- (void)didTapCancelInPasscodeViewController:(TOPasscodeViewController *)passcodeViewController; - -/** The user successfully entered the correct code, as validated by `isCorrectCode:` */ -- (void)didInputCorrectPasscodeInPasscodeViewController:(TOPasscodeViewController *)passcodeViewController; - -/** When available, the user tapped the 'Touch ID' button, or the view controller itself automatically initiated - the Touch ID request on display. This method is where you should implement your - own Touch ID validation logic. For security reasons, this controller does not implement the Touch ID logic itself. */ - -- (void)didPerformBiometricValidationRequestInPasscodeViewController:(TOPasscodeViewController *)passcodeViewController; - -/** Called when the pin view was resized as a result of the view controller being resized. - You can use this to resize your custom header view if necessary. - */ -- (void)passcodeViewController:(TOPasscodeViewController *)passcodeViewController didResizePasscodeViewToWidth:(CGFloat)width; - -@end - - -/** - A view controller that displays an interface for entering a user passcode. - It may be presented modally over another view controller, requiring the user to enter - the passcode correctly before they are able to proceed inside the application. - */ -@interface TOPasscodeViewController : UIViewController - -/** A delegate object, in charge of verifying the PIN code entered by the user */ -@property (nonatomic, weak, nullable) id delegate; - -/** The type of passcode that is expected to be entered. */ -@property (nonatomic, readonly) TOPasscodeType passcodeType; - -/** Will show a 'Touch ID' or 'Face ID' (depending on `biometricType`) button if the user is allowed to log in that way. (Default is NO) */ -@property (nonatomic, assign) BOOL allowBiometricValidation; - -/** Will handle delete button press as delete last symbol (Default is YES) */ -@property (nonatomic, assign) BOOL handleDeletePress; - -/** Set the type of biometrics for this device to update the title of the biometrics button properly. */ -@property (nonatomic, assign) TOPasscodeBiometryType biometryType; - -/** If biometrics are available, automatically ask for it upon presentation (Default is NO) */ -@property (nonatomic, assign) BOOL automaticallyPromptForBiometricValidation; - -/** Optionally change the color of the title text label. */ -@property (nonatomic, strong, nullable) UIColor *titleLabelColor; - -/** Optionally change the tint color of the UI element that indicates input progress (eg the row of circles) */ -@property (nonatomic, strong, nullable) UIColor *inputProgressViewTintColor; - -/** Optionally enable or disable showing the lettering label of all keypad circle buttons. **/ -@property (nonatomic, assign) BOOL keypadButtonShowLettering; - -/** If the style isn't translucent, changes the tint color of the keypad circle button outlines. */ -@property (nonatomic, strong, nullable) UIColor *keypadButtonBackgroundTintColor; - -/** The color of the text elements in each keypad button */ -@property (nonatomic, strong, nullable) UIColor *keypadButtonTextColor; - -/** Optionally, the text color of the keypad button text when tapped. Animates back to the base color. */ -@property (nonatomic, strong, nullable) UIColor *keypadButtonHighlightedTextColor; - -/** The tint button of the accessory button views at the bottom of the keypad (ie 'Cance' etc) */ -@property (nonatomic, strong, nullable) UIColor *accessoryButtonTintColor; - -/** Controls the transluceny of the PIN background when the style has been set to translucent. */ -@property (nonatomic, readonly) UIVisualEffectView *backgroundEffectView; - -/** Opaque, background view when the style is opaque */ -@property (nonatomic, readonly) UIView *backgroundView; - -/** The keypad and accessory views that are displayed in the center of this view */ -@property (nonatomic, readonly) TOPasscodeView *passcodeView; - -/** The Touch ID button, visible if biometrics is enabled and `leftAccessoryButton` is nil. */ -@property (nonatomic, readonly) UIButton *biometricButton; - -/** The Cancel, visible if `rightAccessoryButton` is nil. */ -@property (nonatomic, readonly) UIButton *cancelButton; - -/** The left accessory button. Setting this will override the 'Touch ID' button. */ -@property (nonatomic, strong, nullable) UIButton *leftAccessoryButton; - -/** The right accessory button. Setting this will override the 'Cancel' button. */ -@property (nonatomic, strong, nullable) UIButton *rightAccessoryButton; - -@property (nonatomic, assign) CGFloat accessoryButtonsVerticalInset; - -/** Whether all of the content views are hidden or not, but the background translucent view remains. - Useful for obscuring the content while the app is suspended. */ -@property (nonatomic, assign) BOOL contentHidden; - -/** - Create a new instance of this view controller with the preset style and passcode type. - - @param type The type of passcode to enter (6-digit/numeric) - */ -- (instancetype)initPasscodeType:(TOPasscodeType)type allowCancel:(BOOL)cancel; - -/** - Hide everything except the background translucency view. - - @param hidden Whether the content is hidden or not. - @param animated The content will play a crossfade animation. - */ -- (void)setContentHidden:(BOOL)hidden animated:(BOOL)animated; - -@end - -NS_ASSUME_NONNULL_END - -//! Project version number for TOPasscodeViewController. -FOUNDATION_EXPORT double TOPasscodeViewControllerVersionNumber; - -//! Project version string for TOPasscodeViewController. -FOUNDATION_EXPORT const unsigned char TOPasscodeViewControllerVersionString[]; diff --git a/iOSClient/Utility/TOPasscodeViewController/TOPasscodeViewController.m b/iOSClient/Utility/TOPasscodeViewController/TOPasscodeViewController.m deleted file mode 100755 index acbdabfcbe..0000000000 --- a/iOSClient/Utility/TOPasscodeViewController/TOPasscodeViewController.m +++ /dev/null @@ -1,711 +0,0 @@ -// -// TOPasscodeViewController.m -// -// Copyright 2017 Timothy Oliver. All rights reserved. -// -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files (the "Software"), to -// deal in the Software without restriction, including without limitation the -// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or -// sell copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: -// -// The above copyright notice and this permission notice shall be included in -// all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS -// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, -// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR -// IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - -#import "TOPasscodeViewController.h" -#import "TOPasscodeView.h" -#import "TOPasscodeViewControllerAnimatedTransitioning.h" -#import "TOPasscodeKeypadView.h" -#import "TOPasscodeInputField.h" - -@interface TOPasscodeViewController () - -/* State */ -@property (nonatomic, assign, readwrite) TOPasscodeType passcodeType; -@property (nonatomic, assign) CGFloat keyboardHeight; -@property (nonatomic, assign) BOOL passcodeSuccess; -@property (nonatomic, readonly) UIView *leftButton; -@property (nonatomic, readonly) UIView *rightButton; - -/* Views */ -@property (nonatomic, strong, readwrite) UIVisualEffectView *backgroundEffectView; -@property (nonatomic, strong, readwrite) UIView *backgroundView; -@property (nonatomic, strong, readwrite) TOPasscodeView *passcodeView; -@property (nonatomic, strong, readwrite) UIButton *biometricButton; -@property (nonatomic, strong, readwrite) UIButton *cancelButton; - -/* Style */ -@property (nonatomic, assign) TOPasscodeViewStyle style; -@property (nonatomic, assign) BOOL allowCancel; - -@end - -@implementation TOPasscodeViewController - -#pragma mark - Instance Creation - - -- (instancetype)initPasscodeType:(TOPasscodeType)type allowCancel:(BOOL)cancel -{ - if (self = [super initWithNibName:nil bundle:nil]) { - _passcodeType = type; - _allowCancel = cancel; - [self setUp]; - } - - return self; -} - -- (instancetype)initWithNibName:(NSString *)nibNameOrNil bundle:(NSBundle *)nibBundleOrNil -{ - if (self = [super initWithNibName:nibNameOrNil bundle:nibBundleOrNil]) { - [self setUp]; - } - - return self; -} - -- (void)dealloc -{ - [[NSNotificationCenter defaultCenter] removeObserver:self name:UIKeyboardWillChangeFrameNotification object:nil]; -} - -#pragma mark - View Setup - - -- (void)setUp -{ - self.transitioningDelegate = self; - self.automaticallyPromptForBiometricValidation = NO; - self.handleDeletePress = YES; - - if (@available(iOS 13.0, *)) { - if ([self.traitCollection userInterfaceStyle] == UIUserInterfaceStyleDark) { - self.style = TOPasscodeViewStyleTranslucentDark; - } else { - self.style = TOPasscodeViewStyleTranslucentLight; - } - } else { - self.style = TOPasscodeViewStyleTranslucentLight; - } - - if (TOPasscodeViewStyleIsTranslucent(self.style)) { - self.modalPresentationStyle = UIModalPresentationOverFullScreen; - } - else { - self.modalPresentationStyle = UIModalPresentationFullScreen; - } - - [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(keyboardWillChangeFrame:) - name:UIKeyboardWillChangeFrameNotification object:nil]; -} - -- (void)setUpBackgroundEffectViewForStyle:(TOPasscodeViewStyle)style -{ - BOOL translucent = TOPasscodeViewStyleIsTranslucent(style); - - // Return if it already exists when it should - if (translucent && self.backgroundEffectView) { return; } - - // Return if it doesn't exist when it shouldn't - if (!translucent && !self.backgroundEffectView) { return; } - - // Remove it if we're now opaque - if (!translucent) { - [self.backgroundEffectView removeFromSuperview]; - self.backgroundEffectView = nil; - return; - } - - // Create it otherwise - UIBlurEffect *blurEffect = [UIBlurEffect effectWithStyle:[self blurEffectStyleForStyle:style]]; - self.backgroundEffectView = [[UIVisualEffectView alloc] initWithEffect:blurEffect]; - self.backgroundEffectView.frame = self.view.bounds; - self.backgroundEffectView.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight; - [self.view insertSubview:self.backgroundEffectView atIndex:0]; -} - -- (void)setUpBackgroundViewForStyle:(TOPasscodeViewStyle)style -{ - BOOL translucent = TOPasscodeViewStyleIsTranslucent(style); - - if (!translucent && self.backgroundView) { return; } - - if (translucent && !self.backgroundView) { return; } - - if (translucent) { - [self.backgroundView removeFromSuperview]; - self.backgroundView = nil; - return; - } - - self.backgroundView = [[UIView alloc] initWithFrame:self.view.bounds]; - self.backgroundView.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight; - [self.view insertSubview:self.backgroundView atIndex:0]; -} - -- (UIBlurEffectStyle)blurEffectStyleForStyle:(TOPasscodeViewStyle)style -{ - switch (self.style) { - case TOPasscodeViewStyleTranslucentDark: return UIBlurEffectStyleDark; - case TOPasscodeViewStyleTranslucentLight: return UIBlurEffectStyleExtraLight; - default: return 0; - } - - return 0; -} - -- (void)setUpAccessoryButtons -{ - UIFont *buttonFont = [UIFont systemFontOfSize:16.0f]; - BOOL isPad = [UIDevice currentDevice].userInterfaceIdiom == UIUserInterfaceIdiomPad; - - if (!self.leftAccessoryButton && self.allowBiometricValidation && !self.biometricButton) { - self.biometricButton = [UIButton buttonWithType:UIButtonTypeSystem]; - [self.biometricButton setTitle:TOPasscodeBiometryTitleForType(self.biometryType) forState:UIControlStateNormal]; - [self.biometricButton addTarget:self action:@selector(accessoryButtonTapped:) forControlEvents:UIControlEventTouchUpInside]; - - if (isPad) { - self.passcodeView.leftButton = self.biometricButton; - } - else { - [self.view addSubview:self.biometricButton]; - } - } - else { - if (self.leftAccessoryButton) { - [self.biometricButton removeFromSuperview]; - self.biometricButton = nil; - } - } - - if (!self.rightAccessoryButton && !self.cancelButton) { - self.cancelButton = [UIButton buttonWithType:UIButtonTypeSystem]; - [self.cancelButton setTitle:NSLocalizedString(@"Cancel", @"Cancel") forState:UIControlStateNormal]; - self.cancelButton.titleLabel.font = buttonFont; - [self.cancelButton addTarget:self action:@selector(accessoryButtonTapped:) forControlEvents:UIControlEventTouchUpInside]; - // If cancelling is disabled, we hide the cancel button but we still create it, because it can - // transition to backspace after user input. - self.cancelButton.hidden = !self.allowCancel; - if (isPad) { - self.passcodeView.rightButton = self.cancelButton; - } - else { - [self.view addSubview:self.cancelButton]; - } - } - else { - if (self.rightAccessoryButton) { - [self.cancelButton removeFromSuperview]; - self.cancelButton = nil; - } - } - - [self updateAccessoryButtonFontsForSize:self.view.bounds.size]; -} - -#pragma mark - View Management - -- (void)viewDidLoad -{ - [super viewDidLoad]; - self.view.backgroundColor = [UIColor clearColor]; - self.view.layer.allowsGroupOpacity = NO; - [self setUpBackgroundEffectViewForStyle:self.style]; - [self setUpBackgroundViewForStyle:self.style]; - [self setUpAccessoryButtons]; - [self applyThemeForStyle:self.style]; -} - -- (void)viewDidAppear:(BOOL)animated -{ - [super viewDidAppear:animated]; - - // Automatically trigger biometric validation if available - if (self.allowBiometricValidation && self.automaticallyPromptForBiometricValidation) { - [self accessoryButtonTapped:self.biometricButton]; - } -} - -- (void)viewDidLayoutSubviews -{ - CGSize bounds = self.view.bounds.size; - CGSize maxSize = bounds; - if (@available(iOS 11.0, *)) { - UIEdgeInsets safeAreaInsets = self.view.safeAreaInsets; - if (safeAreaInsets.bottom > 0) { - maxSize.height -= safeAreaInsets.bottom; - } - if (safeAreaInsets.left > 0) { - maxSize.width -= safeAreaInsets.left; - } - if (safeAreaInsets.right > 0) { - maxSize.width -= safeAreaInsets.right; - } - } - - // Resize the pin view to scale to the new size - [self.passcodeView sizeToFitSize:maxSize]; - - // Re-center the pin view - CGRect frame = self.passcodeView.frame; - frame.origin.x = (bounds.width - frame.size.width) * 0.5f; - frame.origin.y = ((bounds.height - self.keyboardHeight) - frame.size.height) * 0.5f; - self.passcodeView.frame = CGRectIntegral(frame); - - // -------------------------------------------------- - - // Update the accessory button sizes - [self updateAccessoryButtonFontsForSize:maxSize]; - - // Re-layout the accessory buttons - [self layoutAccessoryButtonsForSize:maxSize]; -} - -- (void)viewWillAppear:(BOOL)animated -{ - [super viewWillAppear:animated]; - [self setNeedsStatusBarAppearanceUpdate]; - - // Force an initial layout if the view hasn't been presented yet - [UIView performWithoutAnimation:^{ - [self.view setNeedsLayout]; - [self.view layoutIfNeeded]; - }]; - - // Show the keyboard if we're entering alphanumeric characters - if (self.passcodeType == TOPasscodeTypeCustomAlphanumeric) { - [self.passcodeView.inputField becomeFirstResponder]; - } -} - -- (void)viewWillDisappear:(BOOL)animated -{ - [super viewWillDisappear:animated]; - - // Dismiss the keyboard if it is visible - if (self.passcodeView.inputField.isFirstResponder) { - [self.passcodeView.inputField resignFirstResponder]; - } -} - -- (UIStatusBarStyle)preferredStatusBarStyle -{ - return TOPasscodeViewStyleIsDark(self.style) ? UIStatusBarStyleLightContent : UIStatusBarStyleDefault; -} - -#pragma mark - View Rotations - -- (void)viewWillTransitionToSize:(CGSize)size withTransitionCoordinator:(id)coordinator -{ - [super viewWillTransitionToSize:size withTransitionCoordinator:coordinator]; - - // We don't need to do anything special on iPad or if we're using character input - if ([UIDevice currentDevice].userInterfaceIdiom == UIUserInterfaceIdiomPad || self.passcodeType == TOPasscodeTypeCustomAlphanumeric) { return; } - - // Work out if we need to transition to horizontal - BOOL horizontalLayout = size.height < size.width; - - // Perform layout animation - [self.passcodeView setHorizontalLayout:horizontalLayout animated:coordinator.animated duration:coordinator.transitionDuration]; -} - -#pragma mark - View Styling - -- (void)applyThemeForStyle:(TOPasscodeViewStyle)style -{ - BOOL isDark = TOPasscodeViewStyleIsDark(style); - - // Apply the tint color to the accessory buttons - UIColor *accessoryTintColor = self.accessoryButtonTintColor; - if (!accessoryTintColor) { - accessoryTintColor = isDark ? [UIColor whiteColor] : nil; - } - - self.biometricButton.tintColor = accessoryTintColor; - self.cancelButton.tintColor = accessoryTintColor; - self.leftAccessoryButton.tintColor = accessoryTintColor; - self.rightAccessoryButton.tintColor = accessoryTintColor; - - self.backgroundView.backgroundColor = isDark ? [UIColor colorWithWhite:0.1f alpha:1.0f] : [UIColor whiteColor]; -} - -- (void)updateAccessoryButtonFontsForSize:(CGSize)size -{ - CGFloat width = size.width; - if ([UIDevice currentDevice].userInterfaceIdiom == UIUserInterfaceIdiomPhone) { - width = MIN(size.width, size.height); - } - - CGFloat pointSize = 17.0f; - if (width < (CGFloat)TOPasscodeViewContentSizeMedium) { - pointSize = 14.0f; - } - else if (width < (CGFloat)TOPasscodeViewContentSizeDefault) { - pointSize = 16.0f; - } - - UIFont *accessoryFont = [UIFont systemFontOfSize:pointSize]; - - self.biometricButton.titleLabel.font = accessoryFont; - self.cancelButton.titleLabel.font = accessoryFont; - self.leftAccessoryButton.titleLabel.font = accessoryFont; - self.rightAccessoryButton.titleLabel.font = accessoryFont; -} - -- (void)verticalLayoutAccessoryButtonsForSize:(CGSize)size -{ - CGFloat width = MIN(size.width, size.height); - - CGFloat verticalInset = 44.0f; - if (width < (CGFloat)TOPasscodeViewContentSizeMedium) { - verticalInset = 20.0f; - } - else if (width < (CGFloat)TOPasscodeViewContentSizeDefault) { - verticalInset = 30.0f; - } - - if (self.accessoryButtonsVerticalInset > 0) { - verticalInset = self.accessoryButtonsVerticalInset; - } - - CGFloat inset = self.passcodeView.keypadButtonInset; - CGPoint point = (CGPoint){0.0f, (self.view.bounds.size.height - self.keyboardHeight) - verticalInset}; - if (@available(iOS 11.0, *)) { - UIEdgeInsets safeAreaInsets = self.view.safeAreaInsets; - if (safeAreaInsets.bottom > 0) { - point.y -= safeAreaInsets.bottom; - } - } - - if (self.leftButton) { - [self.leftButton sizeToFit]; - point.x = self.passcodeView.frame.origin.x + inset; - self.leftButton.center = point; - } - - if (self.rightButton) { - [self.rightButton sizeToFit]; - point.x = CGRectGetMaxX(self.passcodeView.frame) - inset; - self.rightButton.center = point; - } -} - -- (void)horizontalLayoutAccessoryButtonsForSize:(CGSize)size -{ - CGRect passcodeViewFrame = self.passcodeView.frame; - CGFloat buttonInset = self.passcodeView.keypadButtonInset; - CGFloat width = MIN(size.width, size.height); - CGFloat verticalInset = 35.0f; - if (width < (CGFloat)TOPasscodeViewContentSizeMedium) { - verticalInset = 30.0f; - } - else if (width < (CGFloat)TOPasscodeViewContentSizeDefault) { - verticalInset = 35.0f; - } - - if (self.leftButton) { - [self.leftButton sizeToFit]; - CGRect frame = self.leftButton.frame; - frame.origin.y = (self.view.bounds.size.height - verticalInset) - (frame.size.height * 0.5f); - frame.origin.x = (CGRectGetMaxX(passcodeViewFrame) - buttonInset) - (frame.size.width * 0.5f); - self.leftButton.frame = CGRectIntegral(frame); - } - - if (self.rightButton) { - [self.rightButton sizeToFit]; - CGRect frame = self.rightButton.frame; - frame.origin.y = verticalInset - (frame.size.height * 0.5f); - frame.origin.x = (CGRectGetMaxX(passcodeViewFrame) - buttonInset) - (frame.size.width * 0.5f); - self.rightButton.frame = CGRectIntegral(frame); - } - - [self.view bringSubviewToFront:self.rightButton]; - [self.view bringSubviewToFront:self.leftButton]; -} - -- (void)layoutAccessoryButtonsForSize:(CGSize)size -{ - // The buttons are always embedded in the keypad view on iPad - if ([UIDevice currentDevice].userInterfaceIdiom != UIUserInterfaceIdiomPhone) { return; } - - if (self.passcodeView.horizontalLayout && self.passcodeType != TOPasscodeTypeCustomAlphanumeric) { - [self horizontalLayoutAccessoryButtonsForSize:size]; - } - else { - [self verticalLayoutAccessoryButtonsForSize:size]; - } -} - -#pragma mark - Interactions - -- (void)accessoryButtonTapped:(id)sender -{ - if (sender == self.cancelButton) { - // When entering keyboard input, just leave the button as 'cancel' - if (self.handleDeletePress && self.passcodeType != TOPasscodeTypeCustomAlphanumeric && self.passcodeView.passcode.length > 0) { - [self.passcodeView deleteLastPasscodeCharacterAnimated:YES]; - [self keypadButtonTapped]; - return; - } - - if ([self.delegate respondsToSelector:@selector(didTapCancelInPasscodeViewController:)]) { - [self.delegate didTapCancelInPasscodeViewController:self]; - } - } - else if (sender == self.biometricButton) { - if ([self.delegate respondsToSelector:@selector(didPerformBiometricValidationRequestInPasscodeViewController:)]) { - [self.delegate didPerformBiometricValidationRequestInPasscodeViewController:self]; - } - } -} - -- (void)keypadButtonTapped -{ - NSString *title = nil; - if (self.passcodeView.passcode.length > 0) { - title = NSLocalizedString(@"Delete", @"Delete"); - } else if (self.allowCancel) { - title = NSLocalizedString(@"Cancel", @"Cancel"); - } - [UIView performWithoutAnimation:^{ - if (title != nil) { - [self.cancelButton setTitle:title forState:UIControlStateNormal]; - [self.cancelButton layoutIfNeeded]; - } - self.cancelButton.hidden = (title == nil); - }]; -} - -- (void)didCompleteEnteringPasscode:(NSString *)passcode -{ - if (![self.delegate respondsToSelector:@selector(passcodeViewController:isCorrectCode:)]) { - return; - } - - // Validate the code - BOOL isCorrect = [self.delegate passcodeViewController:self isCorrectCode:passcode]; - if (!isCorrect) { - [self.passcodeView resetPasscodeAnimated:YES playImpact:YES]; - return; - } - - // Hang onto the fact the passcode was successful to play a nicer dismissal animation - self.passcodeSuccess = YES; - - // Perform handler if correctly entered - if ([self.delegate respondsToSelector:@selector(didInputCorrectPasscodeInPasscodeViewController:)]) { - [self.delegate didInputCorrectPasscodeInPasscodeViewController:self]; - } - else { - [self.presentingViewController dismissViewControllerAnimated:YES completion:nil]; - } -} - -#pragma mark - Keyboard Handling - -- (void)keyboardWillChangeFrame:(NSNotification *)notification -{ - // Extract the keyboard information we need from the notification - CGRect keyboardFrame = [notification.userInfo[UIKeyboardFrameEndUserInfoKey] CGRectValue]; - CGFloat animationDuration = [notification.userInfo[UIKeyboardAnimationDurationUserInfoKey] floatValue]; - UIViewAnimationOptions animationCurve = [notification.userInfo[UIKeyboardAnimationCurveUserInfoKey] integerValue]; - - // Work out the on-screen height of the keyboard - self.keyboardHeight = self.view.bounds.size.height - keyboardFrame.origin.y; - self.keyboardHeight = MAX(self.keyboardHeight, 0.0f); - - // Set that the view needs to be laid out - [self.view setNeedsLayout]; - - if (animationDuration < FLT_EPSILON) { - return; - } - - // Animate the content sliding up and down with the keyboard - [UIView animateWithDuration:animationDuration - delay:0.0f - options:animationCurve - animations:^{ [self.view layoutIfNeeded]; } - completion:nil]; -} - -#pragma mark - Transitioning Delegate - -- (nullable id )animationControllerForPresentedController:(UIViewController *)presented - presentingController:(UIViewController *)presenting - sourceController:(UIViewController *)source -{ - return [[TOPasscodeViewControllerAnimatedTransitioning alloc] initWithPasscodeViewController:self dismissing:NO success:NO]; -} - -- (nullable id )animationControllerForDismissedController:(UIViewController *)dismissed -{ - return [[TOPasscodeViewControllerAnimatedTransitioning alloc] initWithPasscodeViewController:self dismissing:YES success:self.passcodeSuccess]; -} - -#pragma mark - Convenience Accessors - -- (UIView *)leftButton -{ - return self.leftAccessoryButton ? self.leftAccessoryButton : self.biometricButton; -} - -- (UIView *)rightButton -{ - return self.rightAccessoryButton ? self.rightAccessoryButton : self.cancelButton; -} - -#pragma mark - Public Accessors - -- (TOPasscodeView *)passcodeView -{ - if (_passcodeView) { return _passcodeView; } - - _passcodeView = [[TOPasscodeView alloc] initWithStyle:self.style passcodeType:self.passcodeType]; - _passcodeView.autoresizingMask = UIViewAutoresizingFlexibleTopMargin | UIViewAutoresizingFlexibleBottomMargin | - UIViewAutoresizingFlexibleLeftMargin | UIViewAutoresizingFlexibleRightMargin; - [_passcodeView sizeToFit]; - _passcodeView.center = self.view.center; - [self.view addSubview:_passcodeView]; - - __weak typeof(self) weakSelf = self; - _passcodeView.passcodeCompletedHandler = ^(NSString *passcode) { - [weakSelf didCompleteEnteringPasscode:passcode]; - }; - - _passcodeView.passcodeDigitEnteredHandler = ^{ - [weakSelf keypadButtonTapped]; - }; - - // Set initial layout to horizontal if we're rotated on an iPhone - if (self.passcodeType != TOPasscodeTypeCustomAlphanumeric && [UIDevice currentDevice].userInterfaceIdiom != UIUserInterfaceIdiomPad) { - CGSize boundsSize = self.view.bounds.size; - _passcodeView.horizontalLayout = boundsSize.width > boundsSize.height; - } - - return _passcodeView; -} - -- (void)setStyle:(TOPasscodeViewStyle)style -{ - if (style == _style) { return; } - _style = style; - - self.passcodeView.style = style; - [self setUpBackgroundEffectViewForStyle:style]; -} - -- (void)setAllowBiometricValidation:(BOOL)allowBiometricValidation -{ - if (_allowBiometricValidation == allowBiometricValidation) { - return; - } - - _allowBiometricValidation = allowBiometricValidation; - [self setUpAccessoryButtons]; - [self applyThemeForStyle:self.style]; -} - -- (void)setTitleLabelColor:(UIColor *)titleLabelColor -{ - self.passcodeView.titleLabelColor = titleLabelColor; -} - -- (void)setSubtitleLabelColor:(UIColor *)subtitleLabelColor -{ - self.passcodeView.subtitleLabelColor = subtitleLabelColor; -} - -- (UIColor *)titleLabelColor { return self.passcodeView.titleLabelColor; } - -- (void)setInputProgressViewTintColor:(UIColor *)inputProgressViewTintColor -{ - self.passcodeView.inputProgressViewTintColor = inputProgressViewTintColor; -} - -- (UIColor *)inputProgressViewTintColor { return self.passcodeView.inputProgressViewTintColor; } - -- (void)setKeypadButtonBackgroundTintColor:(UIColor *)keypadButtonBackgroundTintColor -{ - self.passcodeView.keypadButtonBackgroundColor = keypadButtonBackgroundTintColor; -} - -- (void)setKeypadButtonShowLettering:(BOOL)keypadButtonShowLettering -{ - self.passcodeView.keypadView.showLettering = keypadButtonShowLettering; -} - -- (UIColor *)keypadButtonBackgroundTintColor { return self.passcodeView.keypadButtonBackgroundColor; } - -- (void)setKeypadButtonTextColor:(UIColor *)keypadButtonTextColor -{ - self.passcodeView.keypadButtonTextColor = keypadButtonTextColor; -} - -- (UIColor *)keypadButtonTextColor { return self.passcodeView.keypadButtonTextColor; } - -- (void)setKeypadButtonHighlightedTextColor:(UIColor *)keypadButtonHighlightedTextColor -{ - self.passcodeView.keypadButtonHighlightedTextColor = keypadButtonHighlightedTextColor; -} - -- (UIColor *)keypadButtonHighlightedTextColor { return self.passcodeView.keypadButtonHighlightedTextColor; } - -- (void)setAccessoryButtonTintColor:(UIColor *)accessoryButtonTintColor -{ - if (accessoryButtonTintColor == _accessoryButtonTintColor) { return; } - _accessoryButtonTintColor = accessoryButtonTintColor; - [self applyThemeForStyle:self.style]; -} - -- (void)setBiometryType:(TOPasscodeBiometryType)biometryType -{ - if (_biometryType == biometryType) { return; } - - _biometryType = biometryType; - - if (self.biometricButton) { - [self.biometricButton setTitle:TOPasscodeBiometryTitleForType(_biometryType) forState:UIControlStateNormal]; - } -} - -- (void)setContentHidden:(BOOL)contentHidden -{ - [self setContentHidden:contentHidden animated:NO]; -} - -- (void)setContentHidden:(BOOL)hidden animated:(BOOL)animated -{ - if (hidden == _contentHidden) { return; } - _contentHidden = hidden; - - void (^setViewsHiddenBlock)(BOOL) = ^(BOOL hidden) { - self.passcodeView.hidden = hidden; - self.leftButton.hidden = hidden; - self.rightButton.hidden = hidden; - }; - - void (^completionBlock)(BOOL) = ^(BOOL complete) { - setViewsHiddenBlock(hidden); - }; - - if (!animated) { - completionBlock(YES); - return; - } - - // Make sure the views are visible before the animation - setViewsHiddenBlock(NO); - - void (^animationBlock)(void) = ^{ - CGFloat alpha = hidden ? 0.0f : 1.0f; - self.passcodeView.contentAlpha = alpha; - self.leftButton.alpha = alpha; - self.rightButton.alpha = alpha; - }; - - // Animate - [UIView animateWithDuration:0.4f animations:animationBlock completion:completionBlock]; -} - -@end - diff --git a/iOSClient/Utility/TOPasscodeViewController/Views/Main/TOPasscodeCircleButton.h b/iOSClient/Utility/TOPasscodeViewController/Views/Main/TOPasscodeCircleButton.h deleted file mode 100644 index b44bfc7dc1..0000000000 --- a/iOSClient/Utility/TOPasscodeViewController/Views/Main/TOPasscodeCircleButton.h +++ /dev/null @@ -1,81 +0,0 @@ -// -// TOPasscodeCircleButton.h -// -// Copyright 2017 Timothy Oliver. All rights reserved. -// -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files (the "Software"), to -// deal in the Software without restriction, including without limitation the -// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or -// sell copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: -// -// The above copyright notice and this permission notice shall be included in -// all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS -// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, -// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR -// IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - -#import - -@class TOPasscodeCircleView; -@class TOPasscodeButtonLabel; - -NS_ASSUME_NONNULL_BEGIN - -/** - A UI control representing a single PIN code button for the keypad, - including the number, lettering (eg 'ABC'), and circle border. - */ -@interface TOPasscodeCircleButton : UIControl - -// Alpha value that properly controls the necessary subviews -@property (nonatomic, assign) CGFloat contentAlpha; - -// Required to be set before this view can be properly rendered -@property (nonatomic, strong) UIImage *backgroundImage; -@property (nonatomic, strong) UIImage *hightlightedBackgroundImage; -@property (nonatomic, strong) UIVibrancyEffect *vibrancyEffect; - -// Properties with default values -@property (nonatomic, strong) UIColor *textColor; -@property (nonatomic, strong, nullable) UIColor *highlightedTextColor; -@property (nonatomic, strong) UIFont *numberFont; -@property (nonatomic, strong) UIFont *letteringFont; -@property (nonatomic, assign) CGFloat letteringCharacterSpacing; -@property (nonatomic, assign) CGFloat letteringVerticalSpacing; - -@property (nonatomic, readonly) NSString *numberString; -@property (nonatomic, readonly) NSString *letteringString; - -// The internal views -@property (nonatomic, readonly) TOPasscodeButtonLabel *buttonLabel; -@property (nonatomic, readonly) TOPasscodeCircleView *circleView; -@property (nonatomic, readonly) UIVisualEffectView *vibrancyView; - -// Callback handler -@property (nonatomic, copy) void (^buttonTappedHandler)(void); - -/** - Create a new instance of the class with the supplied number and lettering string - - @param numberString The string of the number to display in this button (eg '1'). - @param letteringString The string of the lettering to display underneath. - */ -- (instancetype)initWithNumberString:(NSString *)numberString letteringString:(NSString *)letteringString; - -/** - Set the background of the button to be the filled circle instead of hollow. - - @param highlighted When YES, the circle is full, when NO, it is hollow. - @param animated When animated, the transition is a crossfade. - */ -- (void)setHighlighted:(BOOL)highlighted animated:(BOOL)animated; - -@end - -NS_ASSUME_NONNULL_END diff --git a/iOSClient/Utility/TOPasscodeViewController/Views/Main/TOPasscodeCircleButton.m b/iOSClient/Utility/TOPasscodeViewController/Views/Main/TOPasscodeCircleButton.m deleted file mode 100644 index 790bfe0db3..0000000000 --- a/iOSClient/Utility/TOPasscodeViewController/Views/Main/TOPasscodeCircleButton.m +++ /dev/null @@ -1,236 +0,0 @@ -// -// TOPasscodeCircleButton.m -// -// Copyright 2017 Timothy Oliver. All rights reserved. -// -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files (the "Software"), to -// deal in the Software without restriction, including without limitation the -// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or -// sell copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: -// -// The above copyright notice and this permission notice shall be included in -// all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS -// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, -// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR -// IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - -#import "TOPasscodeCircleButton.h" -#import "TOPasscodeCircleView.h" -#import "TOPasscodeButtonLabel.h" - -@interface TOPasscodeCircleButton () - -@property (nonatomic, strong, readwrite) TOPasscodeButtonLabel *buttonLabel; -@property (nonatomic, strong, readwrite) TOPasscodeCircleView *circleView; -@property (nonatomic, strong, readwrite) UIVisualEffectView *vibrancyView; - -@property (nonatomic, readwrite, copy) NSString *numberString; -@property (nonatomic, readwrite, copy) NSString *letteringString; - -@end - -@implementation TOPasscodeCircleButton - -- (instancetype)initWithNumberString:(NSString *)numberString letteringString:(NSString *)letteringString -{ - if (self = [super init]) { - _numberString = numberString; - _letteringString = letteringString; - _contentAlpha = 1.0f; - [self setUp]; - } - - return self; -} - -- (void)setUp -{ - self.userInteractionEnabled = YES; - - _textColor = [UIColor whiteColor]; - - [self setUpSubviews]; - [self setUpViewInteraction]; -} - -- (void)setUpSubviews -{ - if (!self.circleView) { - self.circleView = [[TOPasscodeCircleView alloc] initWithFrame:self.bounds]; - [self addSubview:self.circleView]; - } - - if (!self.buttonLabel) { - self.buttonLabel = [[TOPasscodeButtonLabel alloc] initWithFrame:self.bounds]; - self.buttonLabel.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight; - self.buttonLabel.userInteractionEnabled = NO; - self.buttonLabel.textColor = self.textColor; - self.buttonLabel.numberString = self.numberString; - self.buttonLabel.letteringString = self.letteringString; - [self addSubview:self.buttonLabel]; - } - - if (!self.vibrancyView) { - self.vibrancyView = [[UIVisualEffectView alloc] initWithEffect:nil]; - self.vibrancyView.userInteractionEnabled = NO; - [self.vibrancyView.contentView addSubview:self.circleView]; - [self addSubview:self.vibrancyView]; - } -} - -- (void)setUpViewInteraction -{ - if (self.allTargets.count) { return; } - - [self addTarget:self action:@selector(buttonDidTouchDown:) forControlEvents:UIControlEventTouchDown]; - [self addTarget:self action:@selector(buttonDidTouchUpInside:) forControlEvents:UIControlEventTouchUpInside]; - [self addTarget:self action:@selector(buttonDidDragInside:) forControlEvents:UIControlEventTouchDragEnter]; - [self addTarget:self action:@selector(buttonDidDragOutside:) forControlEvents:UIControlEventTouchDragExit]; -} - -- (void)layoutSubviews -{ - [super layoutSubviews]; - - self.vibrancyView.frame = self.bounds; - self.circleView.frame = self.vibrancyView ? self.vibrancyView.bounds : self.bounds; - self.buttonLabel.frame = self.bounds; - [self bringSubviewToFront:self.buttonLabel]; -} - -#pragma mark - User Interaction - - -- (void)buttonDidTouchDown:(id)sender -{ - if (self.buttonTappedHandler) { self.buttonTappedHandler(); } - [self setHighlighted:YES animated:NO]; -} - -- (void)buttonDidTouchUpInside:(id)sender { [self setHighlighted:NO animated:YES]; } -- (void)buttonDidDragInside:(id)sender { [self setHighlighted:YES animated:NO]; } -- (void)buttonDidDragOutside:(id)sender { [self setHighlighted:NO animated:YES]; } - -#pragma mark - Animated Accessors - - -- (void)setHighlighted:(BOOL)highlighted animated:(BOOL)animated -{ - [self.circleView setHighlighted:highlighted animated:animated]; - - if (!self.highlightedTextColor) { return; } - - void (^textFadeBlock)(void) = ^{ - self.buttonLabel.textColor = highlighted ? self.highlightedTextColor : self.textColor; - }; - - if (!animated) { - textFadeBlock(); - return; - } - - [UIView transitionWithView:self.buttonLabel - duration:0.6f - options:UIViewAnimationOptionTransitionCrossDissolve - animations:textFadeBlock - completion:nil]; -} - -#pragma mark - Accessors - - -- (void)setBackgroundImage:(UIImage *)backgroundImage -{ - self.circleView.circleImage = backgroundImage; - CGRect frame = self.frame; - frame.size = backgroundImage.size; - self.frame = CGRectIntegral(frame); -} - -- (UIImage *)backgroundImage { return self.circleView.circleImage; } - -/***********************************************************/ - -- (void)setVibrancyEffect:(UIVibrancyEffect *)vibrancyEffect -{ - if (_vibrancyEffect == vibrancyEffect) { return; } - _vibrancyEffect = vibrancyEffect; - self.vibrancyView.effect = _vibrancyEffect; -} - -/***********************************************************/ - -- (void)setHightlightedBackgroundImage:(UIImage *)hightlightedBackgroundImage -{ - self.circleView.highlightedCircleImage = hightlightedBackgroundImage; -} - -- (UIImage *)hightlightedBackgroundImage { return self.circleView.highlightedCircleImage; } - -/***********************************************************/ - -- (void)setNumberFont:(UIFont *)numberFont -{ - self.buttonLabel.numberLabelFont = numberFont; - [self setNeedsLayout]; -} - -- (UIFont *)numberFont { return self.buttonLabel.numberLabelFont; } - -/***********************************************************/ - -- (void)setLetteringFont:(UIFont *)letteringFont -{ - self.buttonLabel.letteringLabelFont = letteringFont; - [self setNeedsLayout]; -} - -- (UIFont *)letteringFont { return self.buttonLabel.letteringLabelFont; } - -/***********************************************************/ - -- (void)setLetteringVerticalSpacing:(CGFloat)letteringVerticalSpacing -{ - self.buttonLabel.letteringVerticalSpacing = letteringVerticalSpacing; - [self.buttonLabel setNeedsLayout]; -} - -- (CGFloat)letteringVerticalSpacing { return self.buttonLabel.letteringVerticalSpacing; } - -/***********************************************************/ - -- (void)setLetteringCharacterSpacing:(CGFloat)letteringCharacterSpacing -{ - self.buttonLabel.letteringCharacterSpacing = letteringCharacterSpacing; -} - -- (CGFloat)letteringCharacterSpacing { return self.buttonLabel.letteringCharacterSpacing; } - -/***********************************************************/ - -- (void)setTextColor:(UIColor *)textColor -{ - if (textColor == _textColor) { return; } - _textColor = textColor; - - self.buttonLabel.textColor = _textColor; -} - -/***********************************************************/ - -- (void)setContentAlpha:(CGFloat)contentAlpha -{ - if (_contentAlpha == contentAlpha) { - return; - } - - _contentAlpha = contentAlpha; - - self.buttonLabel.alpha = contentAlpha; - self.circleView.alpha = contentAlpha; -} - -@end diff --git a/iOSClient/Utility/TOPasscodeViewController/Views/Main/TOPasscodeKeypadView.h b/iOSClient/Utility/TOPasscodeViewController/Views/Main/TOPasscodeKeypadView.h deleted file mode 100644 index 4db6bb9a88..0000000000 --- a/iOSClient/Utility/TOPasscodeViewController/Views/Main/TOPasscodeKeypadView.h +++ /dev/null @@ -1,101 +0,0 @@ -// -// TOPasscodeKeypadView.h -// -// Copyright 2017 Timothy Oliver. All rights reserved. -// -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files (the "Software"), to -// deal in the Software without restriction, including without limitation the -// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or -// sell copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: -// -// The above copyright notice and this permission notice shall be included in -// all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS -// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, -// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR -// IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - -#import - -NS_ASSUME_NONNULL_BEGIN - -@class TOPasscodeCircleButton; - -/** - A view encompassing 9 circle buttons, making up a keypad view for entering PIN numbers. - Can be laid out vertically or horizontally. - */ -@interface TOPasscodeKeypadView : UIView - -/** The type of layout for the buttons (Default is vertical) */ -@property (nonatomic, assign) BOOL horizontalLayout; - -/** The vibrancy effect to be applied to each button background */ -@property (nonatomic, strong, nullable) UIVibrancyEffect *vibrancyEffect; - -/** The size of each input button */ -@property (nonatomic, assign) CGFloat buttonDiameter; - -/** The stroke width of the buttons */ -@property (nonatomic, assign) CGFloat buttonStrokeWidth; - -/** The spacing between the buttons. Default is (CGSize){25,15} */ -@property (nonatomic, assign) CGSize buttonSpacing; - -/** The font of the number in each button */ -@property (nonatomic, strong) UIFont *buttonNumberFont; - -/** The font of the lettering label */ -@property (nonatomic, strong) UIFont *buttonLetteringFont; - -/** The spacing between the lettering and the number label */ -@property (nonatomic, assign) CGFloat buttonLabelSpacing; - -/** The spacing between the letters in the lettering label */ -@property (nonatomic, assign) CGFloat buttonLetteringSpacing; - -/** Show the 'ABC' lettering under the numbers */ -@property (nonatomic, assign) BOOL showLettering; - -/** The spacing in points between the letters */ -@property (nonatomic, assign) CGFloat letteringSpacing; - -/** The tint color of the button backgrounds */ -@property (nonatomic, strong) UIColor *buttonBackgroundColor; - -/** The color of the text elements in each button */ -@property (nonatomic, strong) UIColor *buttonTextColor; - -/** Optionally the color of text when it's tapped. */ -@property (nonatomic, strong, nullable) UIColor *buttonHighlightedTextColor; - -/** The alpha value of all non-translucent views */ -@property (nonatomic, assign) CGFloat contentAlpha; - -/** Accessory views placed on either side of the '0' button */ -@property (nonatomic, strong, nullable) UIView *leftAccessoryView; -@property (nonatomic, strong, nullable) UIView *rightAccessoryView; - -/** The controls making up each of the button views */ -@property (nonatomic, readonly) NSArray *keypadButtons; - -/** The block that is triggered whenever a user taps one of the buttons */ -@property (nonatomic, copy) void (^buttonTappedHandler)(NSInteger buttonNumber); - -/* - Perform an animation to transition to a new layout. - - @param horizontalLayout The content is laid out horizontally. - @param animated Whether the transition is animated - @param duration The animation length of the transition. - */ -- (void)setHorizontalLayout:(BOOL)horizontalLayout animated:(BOOL)animated duration:(CGFloat)duration; - -@end - -NS_ASSUME_NONNULL_END diff --git a/iOSClient/Utility/TOPasscodeViewController/Views/Main/TOPasscodeKeypadView.m b/iOSClient/Utility/TOPasscodeViewController/Views/Main/TOPasscodeKeypadView.m deleted file mode 100644 index a9cf0bb511..0000000000 --- a/iOSClient/Utility/TOPasscodeViewController/Views/Main/TOPasscodeKeypadView.m +++ /dev/null @@ -1,432 +0,0 @@ -// -// TOPasscodeKeypadView.h -// -// Copyright 2017 Timothy Oliver. All rights reserved. -// -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files (the "Software"), to -// deal in the Software without restriction, including without limitation the -// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or -// sell copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: -// -// The above copyright notice and this permission notice shall be included in -// all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS -// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, -// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR -// IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - -#import "TOPasscodeKeypadView.h" -#import "TOPasscodeCircleImage.h" -#import "TOPasscodeCircleButton.h" -#import "TOPasscodeCircleView.h" -#import "TOPasscodeButtonLabel.h" - -@interface TOPasscodeKeypadView() - -/* Passcode buttons */ -@property (nonatomic, strong, readwrite) NSArray *keypadButtons; - -/* The '0' button for the different layouts */ -@property (nonatomic, strong) TOPasscodeCircleButton *verticalZeroButton; -@property (nonatomic, strong) TOPasscodeCircleButton *horizontalZeroButton; - -/* Images */ -@property (nonatomic, strong) UIImage *buttonImage; -@property (nonatomic, strong) UIImage *tappedButtonImage; - -@end - -@implementation TOPasscodeKeypadView - -- (instancetype)initWithFrame:(CGRect)frame -{ - if (self = [super initWithFrame:frame]) { - self.userInteractionEnabled = YES; - _buttonDiameter = 81.0f; - _buttonSpacing = (CGSize){25,15}; - _buttonStrokeWidth = 1.5f; - _showLettering = YES; - _buttonNumberFont = nil; - _buttonLetteringFont = nil; - _buttonLabelSpacing = FLT_MIN; - _buttonLetteringSpacing = FLT_MIN; - [self sizeToFit]; - } - - return self; -} - -- (TOPasscodeCircleButton *)makeCircleButtonWithNumber:(NSInteger)number letteringString:(NSString *)letteringString -{ - NSString *numberString = [NSString stringWithFormat:@"%ld", (long)number]; - - TOPasscodeCircleButton *circleButton = [[TOPasscodeCircleButton alloc] initWithNumberString:numberString letteringString:letteringString]; - circleButton.backgroundImage = self.buttonImage; - circleButton.hightlightedBackgroundImage = self.tappedButtonImage; - circleButton.vibrancyEffect = self.vibrancyEffect; - - // Add handler for when button is tapped - __weak typeof(self) weakSelf = self; - circleButton.buttonTappedHandler = ^{ - if (weakSelf.buttonTappedHandler) { - weakSelf.buttonTappedHandler(number); - } - }; - - return circleButton; -} - -- (void)setUpButtons -{ - NSMutableArray *buttons = [NSMutableArray array]; - - NSInteger numberOfButtons = 11; // 1-9 are normal, 10 is the vertical '0', 11 is the horizontal '0' - NSArray *letteredTitles = @[@"ABC", @"DEF", @"GHI", @"JKL", - @"MNO", @"PQRS", @"TUV", @"WXYZ"]; - - for (NSInteger i = 0; i < numberOfButtons; i++) { - // Work out the button number text - NSInteger buttonNumber = i + 1; - if (buttonNumber == 10 || buttonNumber == 11) { buttonNumber = 0; } - - // Work out the lettering text - NSString *letteringString = nil; - if (self.showLettering && i > 0 && i-1 < letteredTitles.count) { // (Skip 1 and 0) - letteringString = letteredTitles[i-1]; - } - - // Create a new button and add it to this view - TOPasscodeCircleButton *circleButton = [self makeCircleButtonWithNumber:buttonNumber letteringString:letteringString]; - [self addSubview:circleButton]; - [buttons addObject:circleButton]; - - if (!self.showLettering) { - circleButton.buttonLabel.verticallyCenterNumberLabel = YES; // Center the digit in the middle - } - - // Hang onto the 0 button if it's the vertical one - // And center the text - if (i == 9) { - self.verticalZeroButton = circleButton; - - // Hide the button if it's not vertically laid out - if (self.horizontalLayout) { - self.verticalZeroButton.contentAlpha = 0.0f; - self.verticalZeroButton.hidden = YES; - } - } - else if (i == 10) { - self.horizontalZeroButton = circleButton; - - // Hide the button if it's not horizontally laid out - if (!self.horizontalLayout) { - self.horizontalZeroButton.contentAlpha = 0.0f; - self.horizontalZeroButton.hidden = YES; - } - } - } - - _keypadButtons = [NSArray arrayWithArray:buttons]; -} - -- (void)sizeToFit -{ - CGFloat padding = 2.0f; - - CGRect frame = self.frame; - if (self.horizontalLayout) { - frame.size.width = ((self.buttonDiameter + padding) * 4) + (self.buttonSpacing.width * 3); - frame.size.height = ((self.buttonDiameter + padding) * 3) + (self.buttonSpacing.height * 2); - } - else { - frame.size.width = ((self.buttonDiameter + padding) * 3) + (self.buttonSpacing.width * 2); - frame.size.height = ((self.buttonDiameter + padding) * 4) + (self.buttonSpacing.height * 3); - } - self.frame = CGRectIntegral(frame); -} - -- (void)layoutSubviews -{ - [super layoutSubviews]; - - NSInteger i = 0; - CGPoint origin = CGPointZero; - for (TOPasscodeCircleButton *button in self.keypadButtons) { - // Set the button frame - CGRect frame = button.frame; - frame.origin = origin; - button.frame = frame; - - // Work out the next offset - CGFloat horizontalOffset = frame.size.width + self.buttonSpacing.width; - origin.x += horizontalOffset; - - i++; - - // If we're at the end of the row, move to the next one - if (i % 3 == 0) { - origin.x = 0.0f; - origin.y = origin.y + frame.size.height + self.buttonSpacing.height; - } - } - - // Lay out the vertical button - CGRect frame = self.verticalZeroButton.frame; - frame.origin.x += (frame.size.width + self.buttonSpacing.width); - self.verticalZeroButton.frame = frame; - - // Lay out the horizontal button - frame = self.horizontalZeroButton.frame; - frame.origin.x = (frame.size.width + self.buttonSpacing.width) * 3.0f; - frame.origin.y = frame.size.height + self.buttonSpacing.height; - self.horizontalZeroButton.frame = frame; - - // Layout the accessory buttons - CGFloat midPointY = CGRectGetMidY(self.verticalZeroButton.frame); - - if (self.leftAccessoryView) { - CGRect leftButtonFrame = self.keypadButtons.firstObject.frame; - CGFloat midPointX = CGRectGetMidX(leftButtonFrame); - - [self.leftAccessoryView sizeToFit]; - self.leftAccessoryView.center = (CGPoint){midPointX, midPointY}; - } - - if (self.rightAccessoryView) { - CGRect rightButtonFrame = self.keypadButtons[2].frame; - CGFloat midPointX = CGRectGetMidX(rightButtonFrame); - - [self.rightAccessoryView sizeToFit]; - self.rightAccessoryView.center = (CGPoint){midPointX, midPointY}; - } -} - -#pragma mark - Style Accessors - -- (void)setVibrancyEffect:(UIVibrancyEffect *)vibrancyEffect -{ - if (vibrancyEffect == _vibrancyEffect) { return; } - _vibrancyEffect = vibrancyEffect; - - for (TOPasscodeCircleButton *button in self.keypadButtons) { - button.vibrancyEffect = _vibrancyEffect; - } -} - -#pragma mark - Lazy Getters - -- (UIImage *)buttonImage -{ - if (!_buttonImage) { - _buttonImage = [TOPasscodeCircleImage hollowCircleImageOfSize:self.buttonDiameter strokeWidth:self.buttonStrokeWidth padding:1.0f]; - } - - return _buttonImage; -} - -- (UIImage *)tappedButtonImage -{ - if (!_tappedButtonImage) { - _tappedButtonImage = [TOPasscodeCircleImage circleImageOfSize:self.buttonDiameter inset:self.buttonStrokeWidth * 0.5f padding:1.0f antialias:YES]; - } - - return _tappedButtonImage; -} - -- (NSArray *)keypadButtons -{ - if (_keypadButtons) { return _keypadButtons; } - [self setUpButtons]; - return _keypadButtons; -} - -#pragma mark - Audio Delegate Protocol - -- (BOOL)enableInputClicksWhenVisible -{ - return YES; -} - -#pragma mark - Public Layout Setters - - -- (void)setHorizontalLayout:(BOOL)horizontalLayout -{ - [self setHorizontalLayout:horizontalLayout animated:NO duration:0.0f]; -} - -- (void)setHorizontalLayout:(BOOL)horizontalLayout animated:(BOOL)animated duration:(CGFloat)duration -{ - if (horizontalLayout== _horizontalLayout) { - return; - } - - _horizontalLayout = horizontalLayout; - - // Resize itself now so the frame value is up to date externally - [self sizeToFit]; - - // Set initial animation state - self.verticalZeroButton.hidden = NO; - self.horizontalZeroButton.hidden = NO; - - self.verticalZeroButton.contentAlpha = _horizontalLayout ? 1.0f : 0.0f; - self.horizontalZeroButton.contentAlpha = _horizontalLayout ? 0.0f : 1.0f; - - void (^animationBlock)(void) = ^{ - self.verticalZeroButton.contentAlpha = self.horizontalLayout ? 0.0f : 1.0f; - self.horizontalZeroButton.contentAlpha = self.horizontalLayout ? 1.0f : 0.0f; - }; - - void (^completionBlock)(BOOL) = ^(BOOL complete) { - self.verticalZeroButton.hidden = self.horizontalLayout; - self.horizontalZeroButton.hidden = self.horizontalLayout; - }; - - // Don't animate if not needed - if (!animated) { - animationBlock(); - completionBlock(YES); - return; - } - - // Perform animation - [UIView animateWithDuration:duration animations:animationBlock completion:completionBlock]; -} - -- (void)updateButtonsForCurrentState -{ - for (TOPasscodeCircleButton *circleButton in self.keypadButtons) { - circleButton.backgroundImage = self.buttonImage; - circleButton.hightlightedBackgroundImage = self.tappedButtonImage; - circleButton.numberFont = self.buttonNumberFont; - circleButton.letteringFont = self.buttonLetteringFont; - circleButton.letteringVerticalSpacing = self.buttonLabelSpacing; - circleButton.letteringCharacterSpacing = self.buttonLetteringSpacing; - circleButton.tintColor = self.buttonBackgroundColor; - circleButton.textColor = self.buttonTextColor; - circleButton.highlightedTextColor = self.buttonHighlightedTextColor; - if (!_showLettering) { - circleButton.buttonLabel.letteringLabel.text = nil; - circleButton.buttonLabel.verticallyCenterNumberLabel = YES; - } - } - - [self setNeedsLayout]; -} - -- (void)setButtonDiameter:(CGFloat)buttonDiameter -{ - if (_buttonDiameter == buttonDiameter) { return; } - _buttonDiameter = buttonDiameter; - _tappedButtonImage = nil; - _buttonImage = nil; - [self updateButtonsForCurrentState]; -} - -- (void)setButtonSpacing:(CGSize)buttonSpacing -{ - if (CGSizeEqualToSize(_buttonSpacing, buttonSpacing)) { return; } - _buttonSpacing = buttonSpacing; - [self updateButtonsForCurrentState]; -} - -- (void)setButtonStrokeWidth:(CGFloat)buttonStrokeWidth -{ - if (_buttonStrokeWidth== buttonStrokeWidth) { return; } - _buttonStrokeWidth = buttonStrokeWidth; - _tappedButtonImage = nil; - _buttonImage = nil; - [self updateButtonsForCurrentState]; -} - -- (void)setShowLettering:(BOOL)showLettering -{ - if (_showLettering == showLettering) { return; } - _showLettering = showLettering; - [self updateButtonsForCurrentState]; -} - -- (void)setButtonNumberFont:(UIFont *)buttonNumberFont -{ - if (_buttonNumberFont == buttonNumberFont) { return; } - _buttonNumberFont = buttonNumberFont; - [self updateButtonsForCurrentState]; -} - -- (void)setButtonLetteringFont:(UIFont *)buttonLetteringFont -{ - if (buttonLetteringFont == _buttonLetteringFont) { return; } - _buttonLetteringFont = buttonLetteringFont; - [self updateButtonsForCurrentState]; -} - -- (void)setButtonLabelSpacing:(CGFloat)buttonLabelSpacing -{ - if (buttonLabelSpacing == _buttonLabelSpacing) { return; } - _buttonLabelSpacing = buttonLabelSpacing; - [self updateButtonsForCurrentState]; -} - -- (void)setButtonLetteringSpacing:(CGFloat)buttonLetteringSpacing -{ - if (buttonLetteringSpacing == _buttonLetteringSpacing) { return; } - _buttonLetteringSpacing = buttonLetteringSpacing; - [self updateButtonsForCurrentState]; -} - -- (void)setButtonBackgroundColor:(UIColor *)buttonBackgroundColor -{ - if (buttonBackgroundColor == _buttonBackgroundColor) { return; } - _buttonBackgroundColor = buttonBackgroundColor; - [self updateButtonsForCurrentState]; -} - -- (void)setButtonTextColor:(UIColor *)buttonTextColor -{ - if (buttonTextColor == _buttonTextColor) { return; } - _buttonTextColor = buttonTextColor; - [self updateButtonsForCurrentState]; -} - -- (void)setButtonHighlightedTextColor:(UIColor *)buttonHighlightedTextColor -{ - if (buttonHighlightedTextColor == _buttonHighlightedTextColor) { return; } - _buttonHighlightedTextColor = buttonHighlightedTextColor; - [self updateButtonsForCurrentState]; -} - -- (void)setLeftAccessoryView:(UIView *)leftAccessoryView -{ - if (_leftAccessoryView == leftAccessoryView) { return; } - _leftAccessoryView = leftAccessoryView; - [self addSubview:_leftAccessoryView]; - [self setNeedsLayout]; -} - -- (void)setRightAccessoryView:(UIView *)rightAccessoryView -{ - if (_rightAccessoryView == rightAccessoryView) { return; } - _rightAccessoryView = rightAccessoryView; - [self addSubview:_rightAccessoryView]; - [self setNeedsLayout]; -} - -- (void)setContentAlpha:(CGFloat)contentAlpha -{ - _contentAlpha = contentAlpha; - - for (TOPasscodeCircleButton *button in self.keypadButtons) { - // Skip whichever '0' button is not presently being used - if ((self.horizontalLayout && button == self.verticalZeroButton) || - (!self.horizontalLayout && button == self.horizontalZeroButton)) - { - continue; - } - - button.contentAlpha = contentAlpha; - } -} - -@end diff --git a/iOSClient/Utility/TOPasscodeViewController/Views/Main/TOPasscodeView.h b/iOSClient/Utility/TOPasscodeViewController/Views/Main/TOPasscodeView.h deleted file mode 100644 index b027ee853b..0000000000 --- a/iOSClient/Utility/TOPasscodeViewController/Views/Main/TOPasscodeView.h +++ /dev/null @@ -1,136 +0,0 @@ -// -// TOPasscodeView.h -// -// Copyright 2017 Timothy Oliver. All rights reserved. -// -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files (the "Software"), to -// deal in the Software without restriction, including without limitation the -// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or -// sell copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: -// -// The above copyright notice and this permission notice shall be included in -// all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS -// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, -// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR -// IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - -#import -#import "TOPasscodeViewControllerConstants.h" -#import "TOPasscodeKeypadView.h" - -NS_ASSUME_NONNULL_BEGIN - -@class TOPasscodeCircleButton; -@class TOPasscodeInputField; -@class TOPasscodeKeypadView; -@class TOPasscodeViewContentLayout; - -/** - The passcode view is the primary content view for the passcode view controller. - On iPad, every view except the background view is a subview of this view. - On iPhone, the auxiliary buttons ('Touch ID', 'Cancel') are managed by the view controller. - */ -@interface TOPasscodeView : UIView - -/* The visual style of the view */ -@property (nonatomic, assign) TOPasscodeViewStyle style; - -/* The type of passcode being managed by it */ -@property (nonatomic, readonly) TOPasscodeType passcodeType; - -/* Whether the content is laid out vertically or horizontally (iPhone only) */ -@property (nonatomic, assign) BOOL horizontalLayout; - -/* The text in the title view (Default is 'Enter Passcode') */ -@property (nonatomic, copy) NSString *titleText; -@property (nonatomic, copy) NSString *subtitleText; - -/* Customizable Accessory Views */ -@property (nonatomic, strong, nullable) UIView *titleView; -@property (nonatomic, strong, nullable) UIButton *leftButton; -@property (nonatomic, strong, nullable) UIButton *rightButton; - -/* The default views always shown in this view */ -@property (nonatomic, readonly) UILabel *titleLabel; -@property (nonatomic, readonly) UILabel *subtitleLabel; -@property (nonatomic, readonly) TOPasscodeInputField *inputField; -@property (nonatomic, readonly) TOPasscodeKeypadView *keypadView; - -/* Overrides for theming the various elements. */ -@property (nonatomic, strong, nullable) UIColor *titleLabelColor; -@property (nonatomic, strong, nullable) UIColor *subtitleLabelColor; -@property (nonatomic, strong, nullable) UIColor *inputProgressViewTintColor; -@property (nonatomic, strong, nullable) UIColor *keypadButtonBackgroundColor; -@property (nonatomic, strong, nullable) UIColor *keypadButtonTextColor; -@property (nonatomic, strong, nullable) UIColor *keypadButtonHighlightedTextColor; - -/* Horizontal inset from edge of keypad view to button center */ -@property (nonatomic, readonly) CGFloat keypadButtonInset; - -/* An animatable property for animating the non-translucent subviews */ -@property (nonatomic, assign) CGFloat contentAlpha; - -/* The passcode currently entered into this view */ -@property (nonatomic, copy, nullable) NSString *passcode; - -/* The default layout object controlling the - sizing and placement of all this view's child elements. */ -@property (nonatomic, strong, null_resettable) TOPasscodeViewContentLayout *defaultContentLayout; - -/* As needed, additional layout objects that will be checked and used in priority over the default content layout. */ -@property (nonatomic, strong, nullable) NSArray *contentLayouts; - -/* Callback triggered each time the user taps a key */ -@property (nonatomic, copy, nullable) void (^passcodeDigitEnteredHandler)(void); - -/* Callback triggered when the user has finished entering the passcode */ -@property (nonatomic, copy, nullable) void (^passcodeCompletedHandler)(NSString *passcode); - -/* - Create a new instance with one of the style types - - @param style The visual style of the passcode view. - @param type The type of passcode to accept. - */ -- (instancetype)initWithStyle:(TOPasscodeViewStyle)style passcodeType:(TOPasscodeType)type; - -/* - Resize the view and all subviews for the optimum size to fit a super view of the suplied width. - - @param size The size of the view to which this view. - */ -- (void)sizeToFitSize:(CGSize)size; - -/* - Reset the passcode to nil and optionally play animation / vibration to match - - @param animated Play a shaking animation to reset the passcode. - @param impact On supported devices, play a small reset vibration as well. - */ -- (void)resetPasscodeAnimated:(BOOL)animated playImpact:(BOOL)impact; - -/* - Delete the last character from the passcode - - @param animated Whether the delete operation is animated or not. - */ -- (void)deleteLastPasscodeCharacterAnimated:(BOOL)animated; - -/* - Animate the transition between horizontal and vertical layouts - - @param horizontalLayout Whether to lay out the content vertically or horizontally. - @param animated Whether the transition is animated or not. - @param duration The duration of the animation - */ -- (void)setHorizontalLayout:(BOOL)horizontalLayout animated:(BOOL)animated duration:(CGFloat)duration; - -@end - -NS_ASSUME_NONNULL_END diff --git a/iOSClient/Utility/TOPasscodeViewController/Views/Main/TOPasscodeView.m b/iOSClient/Utility/TOPasscodeViewController/Views/Main/TOPasscodeView.m deleted file mode 100644 index 2d1a30a3ee..0000000000 --- a/iOSClient/Utility/TOPasscodeViewController/Views/Main/TOPasscodeView.m +++ /dev/null @@ -1,690 +0,0 @@ -// -// TOPasscodeView.m -// -// Copyright 2017 Timothy Oliver. All rights reserved. -// -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files (the "Software"), to -// deal in the Software without restriction, including without limitation the -// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or -// sell copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: -// -// The above copyright notice and this permission notice shall be included in -// all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS -// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, -// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR -// IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - -#import "TOPasscodeView.h" -#import "TOPasscodeViewContentLayout.h" -#import "TOPasscodeCircleButton.h" -#import "TOPasscodeInputField.h" -#import "TOPasscodeKeypadView.h" - -@interface TOPasscodeView () - -/* The current layout object used to configure this view */ -@property (nonatomic, weak) TOPasscodeViewContentLayout *currentLayout; - -/* The main views */ -@property (nonatomic, strong, readwrite) UILabel *titleLabel; -@property (nonatomic, strong, readwrite) UILabel *subtitleLabel; -@property (nonatomic, strong, readwrite) TOPasscodeInputField *inputField; -@property (nonatomic, strong, readwrite) TOPasscodeKeypadView *keypadView; - -/* The type of passcode we're displaying */ -@property (nonatomic, assign, readwrite) TOPasscodeType passcodeType; - -@end - -@implementation TOPasscodeView - -- (instancetype)initWithFrame:(CGRect)frame -{ - if (self = [super initWithFrame:frame]) { - [self setUp]; - } - - return self; -} - -- (instancetype)initWithStyle:(TOPasscodeViewStyle)style passcodeType:(TOPasscodeType)type -{ - if (self = [super initWithFrame:CGRectMake(0,0,320,393)]) { - _style = style; - _passcodeType = type; - [self setUp]; - } - - return self; -} - -- (instancetype)initWithCoder:(NSCoder *)aDecoder -{ - if (self = [super initWithCoder:aDecoder]) { - [self setUp]; - } - - return self; -} - -- (void)setUp -{ - // Set up default properties - self.userInteractionEnabled = YES; - _defaultContentLayout = [TOPasscodeViewContentLayout defaultScreenContentLayout]; - _currentLayout = _defaultContentLayout; - _contentLayouts = @[[TOPasscodeViewContentLayout mediumScreenContentLayout], - [TOPasscodeViewContentLayout smallScreenContentLayout]]; - _titleText = NSLocalizedString(@"Enter Passcode", @""); - - // Start configuring views - [self setUpViewForType:self.passcodeType]; - - // Set the default layout for the views - [self updateSubviewsForContentLayout:_defaultContentLayout]; - - // Configure the theme of all of the views - [self applyThemeForStyle:_style]; -} - -#pragma mark - View Layout - -- (void)verticallyLayoutSubviews -{ - CGSize viewSize = self.frame.size; - CGSize midViewSize = (CGSize){self.frame.size.width * 0.5f, self.frame.size.height * 0.5f}; - - CGRect frame = CGRectZero; - CGFloat y = 0.0f; - - // Title View - if (self.titleView) { - frame = self.titleView.frame; - frame.origin.y = y; - frame.origin.x = midViewSize.width - (CGRectGetWidth(frame) * 0.5f); - self.titleView.frame = CGRectIntegral(frame); - - y = CGRectGetMaxY(frame) + self.currentLayout.titleViewBottomSpacing; - } - - // Title Label - frame = self.titleLabel.frame; - frame.origin.y = y; - frame.origin.x = midViewSize.width - (CGRectGetWidth(frame) * 0.5f); - self.titleLabel.frame = CGRectIntegral(frame); - - y = CGRectGetMaxY(frame) + self.currentLayout.titleLabelBottomSpacing; - - // Circle Row View - [self.inputField sizeToFit]; - frame = self.inputField.frame; - frame.origin.y = y; - frame.origin.x = midViewSize.width - (CGRectGetWidth(frame) * 0.5f); - self.inputField.frame = CGRectIntegral(frame); - - y = CGRectGetMaxY(frame) + self.currentLayout.circleRowBottomSpacing; - - // Subtitle Label - frame = self.subtitleLabel.frame; - frame.origin.y = y; - frame.origin.x = midViewSize.width - (CGRectGetWidth(frame) * 0.5f); - self.subtitleLabel.frame = CGRectIntegral(frame); - - y = CGRectGetMaxY(frame) + self.currentLayout.subtitleLabelBottomSpacing; - - // PIN Pad View - if (self.keypadView) { - frame = self.keypadView.frame; - frame.origin.y = y; - frame.origin.x = midViewSize.width - (CGRectGetWidth(frame) * 0.5f); - self.keypadView.frame = CGRectIntegral(frame); - } - - // If the keypad view is hidden, lay out the left button manually - if (!self.keypadView && self.leftButton) { - frame = self.leftButton.frame; - frame.origin.x = 0.0f; - frame.origin.y = y; - self.leftButton.frame = frame; - } - - // If the keypad view is hidden, lay out the right button manually - if (!self.keypadView && self.rightButton) { - frame = self.rightButton.frame; - frame.origin.x = viewSize.width - frame.size.width; - frame.origin.y = y; - self.rightButton.frame = frame; - } -} - -- (void)horizontallyLayoutSubviews -{ - CGSize midViewSize = (CGSize){self.frame.size.width * 0.5f, self.frame.size.height * 0.5f}; - CGRect frame = CGRectZero; - - // Work out the y offset, assuming the input field is in the middle - frame.origin.y = midViewSize.height - (self.inputField.frame.size.height * 0.5f); - frame.origin.y -= (self.titleLabel.frame.size.height + self.currentLayout.titleLabelHorizontalBottomSpacing); - - // Include offset for title view if present - if (self.titleView) { - frame.origin.y -= (self.titleView.frame.size.height + self.currentLayout.titleViewHorizontalBottomSpacing); - } - - // Set initial Y offset - frame.origin.y = MAX(frame.origin.y, 0.0f); - - // Set frame of title view - if (self.titleView) { - frame.size = self.titleView.frame.size; - frame.origin.x = (self.currentLayout.titleHorizontalLayoutWidth - frame.size.width) * 0.5f; - self.titleView.frame = CGRectIntegral(frame); - - frame.origin.y += (frame.size.height + self.currentLayout.titleViewHorizontalBottomSpacing); - } - - // Set frame of title label - frame.size = self.titleLabel.frame.size; - frame.origin.x = (self.currentLayout.titleHorizontalLayoutWidth - frame.size.width) * 0.5f; - self.titleLabel.frame = CGRectIntegral(frame); - - frame.origin.y += (frame.size.height + self.currentLayout.subtitleLabelHorizontalBottomSpacing); - - // Set frame of subtitle label - frame.size = self.subtitleLabel.frame.size; - frame.origin.x = (self.currentLayout.titleHorizontalLayoutWidth - frame.size.width) * 0.5f; - self.subtitleLabel.frame = CGRectIntegral(frame); - - frame.origin.y += (frame.size.height + self.currentLayout.titleLabelHorizontalBottomSpacing); - - // Set frame of the input field - frame.size = self.inputField.frame.size; - frame.origin.x = (self.currentLayout.titleHorizontalLayoutWidth - frame.size.width) * 0.5f; - self.inputField.frame = CGRectIntegral(frame); - - // Set the frame of the keypad view - frame.size = self.keypadView.frame.size; - frame.origin.y = 0.0f; - frame.origin.x = self.currentLayout.titleHorizontalLayoutWidth + self.currentLayout.titleHorizontalLayoutSpacing; - self.keypadView.frame = CGRectIntegral(frame); -} - -- (void)layoutSubviews -{ - if (self.horizontalLayout) { - [self horizontallyLayoutSubviews]; - } - else { - [self verticallyLayoutSubviews]; - } -} - -- (void)sizeToFitSize:(CGSize)size -{ - CGFloat width = size.width; - if ([UIDevice currentDevice].userInterfaceIdiom == UIUserInterfaceIdiomPhone) { - width = MIN(size.width, size.height); - } - - NSMutableArray *layouts = [NSMutableArray array]; - [layouts addObject:self.defaultContentLayout]; - [layouts addObjectsFromArray:self.contentLayouts]; - - // Loop through each layout (in ascending order) and pick the best one to fit this view - TOPasscodeViewContentLayout *contentLayout = self.defaultContentLayout; - for (TOPasscodeViewContentLayout *layout in layouts) { - if (width >= layout.viewWidth) { - contentLayout = layout; - break; - } - } - - // Set the new layout - self.currentLayout = contentLayout; - - // Resize the views to fit - [self sizeToFit]; -} - -- (void)verticalSizeToFit -{ - CGRect frame = self.frame; - frame.size.width = 0.0f; - frame.size.height = 0.0f; - - [self.keypadView sizeToFit]; - [self.inputField sizeToFit]; - - if (self.keypadView) { - frame.size.width = self.keypadView.frame.size.width; - } - else { - frame.size.width = self.inputField.frame.size.width; - } - - // Add height for the title view - if (self.titleView) { - frame.size.height += self.titleView.frame.size.height; - frame.size.height += self.currentLayout.titleViewBottomSpacing; - } - - // Add height for the title label - CGRect titleFrame = self.titleLabel.frame; - titleFrame.size = [self.titleLabel sizeThatFits:(CGSize){frame.size.width, CGFLOAT_MAX}]; - self.titleLabel.frame = titleFrame; - - frame.size.height += titleFrame.size.height; - frame.size.height += self.currentLayout.titleLabelBottomSpacing; - - // Add height for the subtitle label - CGRect subtitleFrame = self.subtitleLabel.frame; - subtitleFrame.size = [self.subtitleLabel sizeThatFits:(CGSize){frame.size.width, CGFLOAT_MAX}]; - self.subtitleLabel.frame = subtitleFrame; - - frame.size.height += subtitleFrame.size.height; - frame.size.height += self.currentLayout.subtitleLabelBottomSpacing; - - // Add height for the circle rows - frame.size.height += self.inputField.frame.size.height; - frame.size.height += self.currentLayout.circleRowBottomSpacing; - - // Add height for the keypad - if (self.keypadView) { - frame.size.height += self.keypadView.frame.size.height; - } - else { // If no keypad, just factor in the accessory buttons - [self.leftButton sizeToFit]; - [self.rightButton sizeToFit]; - - CGFloat maxHeight = 0.0f; - maxHeight = MAX(self.leftButton.frame.size.height, 0.0f); - maxHeight = MAX(self.rightButton.frame.size.height, maxHeight); - - frame.size.height += maxHeight; - } - - // Add extra padding at the bottom - frame.size.height += self.currentLayout.bottomPadding; - - // Set the frame back - self.frame = CGRectIntegral(frame); -} - -- (void)horizontalSizeToFit -{ - CGRect frame = self.frame; - - [self.keypadView sizeToFit]; - [self.inputField sizeToFit]; - - frame.size.width = self.currentLayout.titleHorizontalLayoutWidth; - frame.size.width += self.currentLayout.titleHorizontalLayoutSpacing; - frame.size.width += self.keypadView.frame.size.width; - - frame.size.height = self.keypadView.frame.size.height; - - self.frame = CGRectIntegral(frame); -} - -- (void)sizeToFit -{ - if (self.horizontalLayout && self.passcodeType != TOPasscodeTypeCustomAlphanumeric) { - [self horizontalSizeToFit]; - } - else { - [self verticalSizeToFit]; - } -} - -#pragma mark - View Setup - -- (void)setUpViewForType:(TOPasscodeType)type -{ - __weak typeof(self) weakSelf = self; - - self.backgroundColor = [UIColor clearColor]; - - // Set up title label - if (self.titleLabel == nil) { - self.titleLabel = [[UILabel alloc] initWithFrame:CGRectZero]; - } - self.titleLabel.text = self.titleText; - self.titleLabel.textAlignment = NSTextAlignmentCenter; - self.titleLabel.numberOfLines = 0; - [self.titleLabel sizeToFit]; - [self addSubview:self.titleLabel]; - - // Set up the passcode style - TOPasscodeInputFieldStyle style = TOPasscodeInputFieldStyleFixed; - if (type >= TOPasscodeTypeCustomNumeric) { - style = TOPasscodeInputFieldStyleVariable; - } - - // Set up input field - if (self.inputField == nil) { - self.inputField = [[TOPasscodeInputField alloc] initWithStyle:style]; - } - self.inputField.passcodeCompletedHandler = ^(NSString *passcode) { - if (weakSelf.passcodeCompletedHandler) { - weakSelf.passcodeCompletedHandler(passcode); - } - }; - - // Configure the input field based on the exact passcode type - if (style == TOPasscodeInputFieldStyleFixed) { - self.inputField.fixedInputView.length = (self.passcodeType == TOPasscodeTypeSixDigits) ? 6 : 4; - } - else { - self.inputField.showSubmitButton = (self.passcodeType == TOPasscodeTypeCustomNumeric); - self.inputField.enabled = (self.passcodeType == TOPasscodeTypeCustomAlphanumeric); - } - - [self addSubview:self.inputField]; - - // Set up subtitle label - if (self.subtitleLabel == nil) { - self.subtitleLabel = [[UILabel alloc] initWithFrame:CGRectZero]; - } - self.subtitleLabel.text = self.subtitleText; - self.subtitleLabel.textAlignment = NSTextAlignmentCenter; - self.subtitleLabel.numberOfLines = 0; - [self.subtitleLabel sizeToFit]; - [self addSubview:self.subtitleLabel]; - - // Set up pad row - if (type != TOPasscodeTypeCustomAlphanumeric) { - if (self.keypadView == nil) { - self.keypadView = [[TOPasscodeKeypadView alloc] init]; - } - self.keypadView.buttonTappedHandler = ^(NSInteger button) { - NSString *numberString = [NSString stringWithFormat:@"%ld", (long)button]; - [weakSelf.inputField appendPasscodeCharacters:numberString animated:NO]; - - if (weakSelf.passcodeDigitEnteredHandler) { - weakSelf.passcodeDigitEnteredHandler(); - } - }; - [self addSubview:self.keypadView]; - } - else { - [self.keypadView removeFromSuperview]; - self.keypadView = nil; - } -} - -- (void)updateSubviewsForContentLayout:(TOPasscodeViewContentLayout *)contentLayout -{ - // Title View - self.titleLabel.font = contentLayout.titleLabelFont; - - // Subtitle View - self.subtitleLabel.font = contentLayout.subtitleLabelFont; - - // Circle Row View - self.inputField.fixedInputView.circleDiameter = contentLayout.circleRowDiameter; - self.inputField.fixedInputView.circleSpacing = contentLayout.circleRowSpacing; - - // Text Field Input Row - NSInteger maximumInputLength = (self.passcodeType == TOPasscodeTypeCustomAlphanumeric) ? - contentLayout.textFieldAlphanumericCharacterLength : - contentLayout.textFieldNumericCharacterLength; - - self.inputField.variableInputView.outlineThickness = contentLayout.textFieldBorderThickness; - self.inputField.variableInputView.outlineCornerRadius = contentLayout.textFieldBorderRadius; - self.inputField.variableInputView.circleDiameter = contentLayout.textFieldCircleDiameter; - self.inputField.variableInputView.circleSpacing = contentLayout.textFieldCircleSpacing; - self.inputField.variableInputView.outlinePadding = contentLayout.textFieldBorderPadding; - self.inputField.variableInputView.maximumVisibleLength = maximumInputLength; - - // Submit button - self.inputField.submitButtonSpacing = contentLayout.submitButtonSpacing; - self.inputField.submitButtonFontSize = contentLayout.submitButtonFontSize; - - // Keypad - self.keypadView.buttonNumberFont = contentLayout.circleButtonTitleLabelFont; - self.keypadView.buttonLetteringFont = contentLayout.circleButtonLetteringLabelFont; - self.keypadView.buttonLetteringSpacing = contentLayout.circleButtonLetteringSpacing; - self.keypadView.buttonLabelSpacing = contentLayout.circleButtonLabelSpacing; - self.keypadView.buttonSpacing = contentLayout.circleButtonSpacing; - self.keypadView.buttonDiameter = contentLayout.circleButtonDiameter; -} - -- (void)applyThemeForStyle:(TOPasscodeViewStyle)style -{ - BOOL isTranslucent = TOPasscodeViewStyleIsTranslucent(style); - BOOL isDark = TOPasscodeViewStyleIsDark(style); - - // Set title label color - UIColor *titleLabelColor = self.titleLabelColor; - if (titleLabelColor == nil) { - titleLabelColor = isDark ? [UIColor whiteColor] : [UIColor blackColor]; - } - self.titleLabel.textColor = titleLabelColor; - - // Set subtitle label color - UIColor *subtitleLabelColor = self.subtitleLabelColor; - if (subtitleLabelColor == nil) { - subtitleLabelColor = isDark ? [UIColor whiteColor] : [UIColor blackColor]; - } - self.subtitleLabel.textColor = subtitleLabelColor; - - // Add/remove the translucency effect to the buttons - if (isTranslucent) { - UIBlurEffect *blurEffect = [self blurEffectForStyle:style]; - UIVibrancyEffect *vibrancyEffect = [UIVibrancyEffect effectForBlurEffect:blurEffect]; - self.inputField.visualEffectView.effect = vibrancyEffect; - self.keypadView.vibrancyEffect = vibrancyEffect; - } - else { - self.inputField.visualEffectView.effect = nil; - self.keypadView.vibrancyEffect = nil; - } - - // Set keyboard style of the input field - self.inputField.keyboardAppearance = isDark ? UIKeyboardAppearanceDark : UIKeyboardAppearanceDefault; - - UIColor *defaultTintColor = isDark ? [UIColor colorWithWhite:0.85 alpha:1.0f] : [UIColor colorWithWhite:0.3 alpha:1.0f]; - - // Set the tint color of the circle row view - UIColor *circleRowColor = self.inputProgressViewTintColor; - if (circleRowColor == nil) { - circleRowColor = defaultTintColor; - } - self.inputField.tintColor = defaultTintColor; - - // Set the tint color of the keypad buttons - UIColor *keypadButtonBackgroundColor = self.keypadButtonBackgroundColor; - if (keypadButtonBackgroundColor == nil) { - keypadButtonBackgroundColor = defaultTintColor; - } - self.keypadView.buttonBackgroundColor = keypadButtonBackgroundColor; - - // Set the color of the keypad button labels - UIColor *buttonTextColor = self.keypadButtonTextColor; - if (buttonTextColor == nil) { - buttonTextColor = isDark ? [UIColor whiteColor] : [UIColor blackColor]; - } - self.keypadView.buttonTextColor = buttonTextColor; - - // Set the highlight color of the keypad button - UIColor *buttonHighlightedTextColor = self.keypadButtonHighlightedTextColor; - if (buttonHighlightedTextColor == nil) { - if (isTranslucent) { - buttonHighlightedTextColor = isDark ? nil : [UIColor whiteColor]; - } - else { - buttonHighlightedTextColor = isDark ? [UIColor blackColor] : [UIColor whiteColor]; - } - } - self.keypadView.buttonHighlightedTextColor = buttonHighlightedTextColor; -} - -#pragma mark - Passcode Management - -- (void)resetPasscodeAnimated:(BOOL)animated playImpact:(BOOL)impact -{ - [self.inputField resetPasscodeAnimated:animated playImpact:impact]; -} - -- (void)deleteLastPasscodeCharacterAnimated:(BOOL)animated -{ - [self.inputField deletePasscodeCharactersOfCount:1 animated:animated]; -} - -#pragma mark - Internal Style Management - -- (UIBlurEffect *)blurEffectForStyle:(TOPasscodeViewStyle)style -{ - switch (style) { - case TOPasscodeViewStyleTranslucentDark: - return [UIBlurEffect effectWithStyle:UIBlurEffectStyleDark]; - case TOPasscodeViewStyleTranslucentLight: - return [UIBlurEffect effectWithStyle:UIBlurEffectStyleExtraLight]; - default: return nil; - } - - return nil; -} - -#pragma mark - Accessors - -- (void)setHorizontalLayout:(BOOL)horizontalLayout -{ - [self setHorizontalLayout:horizontalLayout animated:NO duration:0.0f]; -} - -- (void)setHorizontalLayout:(BOOL)horizontalLayout animated:(BOOL)animated duration:(CGFloat)duration -{ - if (horizontalLayout == _horizontalLayout) { return; } - _horizontalLayout = horizontalLayout; - [self.keypadView setHorizontalLayout:horizontalLayout animated:animated duration:duration]; - [self.inputField setHorizontalLayout:horizontalLayout animated:animated duration:duration]; -} - -- (void)setDefaultContentLayout:(TOPasscodeViewContentLayout *)defaultContentLayout -{ - if (defaultContentLayout == _defaultContentLayout) { return; } - _defaultContentLayout = defaultContentLayout; - - if (!_defaultContentLayout) { - _defaultContentLayout = [TOPasscodeViewContentLayout defaultScreenContentLayout]; - } -} - -- (void)setCurrentLayout:(TOPasscodeViewContentLayout *)currentLayout -{ - if (_currentLayout == currentLayout) { return; } - _currentLayout = currentLayout; - - // Update the views - [self updateSubviewsForContentLayout:currentLayout]; -} - -- (void)setStyle:(TOPasscodeViewStyle)style -{ - if (style == _style) { return; } - _style = style; - [self applyThemeForStyle:style]; -} - -- (void)setTitleLabelColor:(UIColor *)titleLabelColor -{ - if (titleLabelColor == _titleLabelColor) { return; } - _titleLabelColor = titleLabelColor; - self.titleLabel.textColor = titleLabelColor; -} - -- (void)setSubtitleLabelColor:(UIColor *)subtitleLabelColor -{ - if (subtitleLabelColor == _subtitleLabelColor) { return; } - _subtitleLabelColor = subtitleLabelColor; - self.subtitleLabel.textColor = subtitleLabelColor; -} - -- (void)setInputProgressViewTintColor:(UIColor *)inputProgressViewTintColor -{ - if (inputProgressViewTintColor == _inputProgressViewTintColor) { return; } - _inputProgressViewTintColor = inputProgressViewTintColor; - self.inputField.tintColor = inputProgressViewTintColor; -} - -- (void)setKeypadButtonBackgroundColor:(UIColor *)keypadButtonBackgroundColor -{ - if (keypadButtonBackgroundColor == _keypadButtonBackgroundColor) { return; } - _keypadButtonBackgroundColor = keypadButtonBackgroundColor; - self.keypadView.buttonBackgroundColor = keypadButtonBackgroundColor; -} - -- (void)setKeypadButtonTextColor:(UIColor *)keypadButtonTextColor -{ - if (keypadButtonTextColor == _keypadButtonTextColor) { return; } - _keypadButtonTextColor = keypadButtonTextColor; - self.keypadView.buttonTextColor = keypadButtonTextColor; -} - -- (void)setKeypadButtonHighlightedTextColor:(UIColor *)keypadButtonHighlightedTextColor -{ - if (keypadButtonHighlightedTextColor == _keypadButtonHighlightedTextColor) { return; } - _keypadButtonHighlightedTextColor = keypadButtonHighlightedTextColor; - self.keypadView.buttonHighlightedTextColor = keypadButtonHighlightedTextColor; -} - -- (void)setLeftButton:(UIButton *)leftButton -{ - if (leftButton == _leftButton) { return; } - _leftButton = leftButton; - - if (self.keypadView) { - self.keypadView.leftAccessoryView = leftButton; - } - else { - [self addSubview:_leftButton]; - } -} - -- (void)setRightButton:(UIButton *)rightButton -{ - if (rightButton == _rightButton) { return; } - _rightButton = rightButton; - - if (self.keypadView) { - self.keypadView.rightAccessoryView = rightButton; - } - else { - [self addSubview:_rightButton]; - } -} - -- (CGFloat)keypadButtonInset -{ - UIView *button = self.keypadView.keypadButtons.firstObject; - return CGRectGetMidX(button.frame); -} - -- (void)setContentAlpha:(CGFloat)contentAlpha -{ - _contentAlpha = contentAlpha; - - self.titleView.alpha = contentAlpha; - self.titleLabel.alpha = contentAlpha; - self.subtitleLabel.alpha = contentAlpha; - self.inputField.contentAlpha = contentAlpha; - self.keypadView.contentAlpha = contentAlpha; - self.keypadView.leftAccessoryView.alpha = contentAlpha; - self.keypadView.rightAccessoryView.alpha = contentAlpha; - self.leftButton.alpha = contentAlpha; - self.rightButton.alpha = contentAlpha; -} - -- (void)setPasscode:(NSString *)passcode -{ - [self.inputField setPasscode:passcode]; -} - -- (NSString *)passcode -{ - return self.inputField.passcode; -} - -@end diff --git a/iOSClient/Utility/TOPasscodeViewController/Views/Settings/TOPasscodeSettingsKeypadButton.h b/iOSClient/Utility/TOPasscodeViewController/Views/Settings/TOPasscodeSettingsKeypadButton.h deleted file mode 100644 index 8179622f61..0000000000 --- a/iOSClient/Utility/TOPasscodeViewController/Views/Settings/TOPasscodeSettingsKeypadButton.h +++ /dev/null @@ -1,45 +0,0 @@ -// -// TOPasscodeSettingsKeypadButton.h -// -// Copyright 2017 Timothy Oliver. All rights reserved. -// -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files (the "Software"), to -// deal in the Software without restriction, including without limitation the -// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or -// sell copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: -// -// The above copyright notice and this permission notice shall be included in -// all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS -// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, -// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR -// IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - -#import - -@class TOPasscodeButtonLabel; - -/** - A single button view that is used in a number keypad views styled in - a pseudo-skeuomorphic style. - */ -@interface TOPasscodeSettingsKeypadButton : UIButton - -/** Background Images */ -@property (nonatomic, strong) UIImage *buttonBackgroundImage; -@property (nonatomic, strong) UIImage *buttonTappedBackgroundImage; - -/* Inset of the label view from the bottom to account for the bevel */ -@property (nonatomic, assign) CGFloat bottomInset; - -/* The button label containing the number and lettering */ -@property (nonatomic, readonly) TOPasscodeButtonLabel *buttonLabel; - -+ (TOPasscodeSettingsKeypadButton *)button; - -@end diff --git a/iOSClient/Utility/TOPasscodeViewController/Views/Settings/TOPasscodeSettingsKeypadButton.m b/iOSClient/Utility/TOPasscodeViewController/Views/Settings/TOPasscodeSettingsKeypadButton.m deleted file mode 100644 index fa41bd4587..0000000000 --- a/iOSClient/Utility/TOPasscodeViewController/Views/Settings/TOPasscodeSettingsKeypadButton.m +++ /dev/null @@ -1,103 +0,0 @@ -// -// TOPasscodeSettingsKeypadButton.m -// -// Copyright 2017 Timothy Oliver. All rights reserved. -// -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files (the "Software"), to -// deal in the Software without restriction, including without limitation the -// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or -// sell copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: -// -// The above copyright notice and this permission notice shall be included in -// all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS -// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, -// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR -// IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - -#import "TOPasscodeSettingsKeypadButton.h" -#import "TOPasscodeButtonLabel.h" - -@interface TOPasscodeSettingsKeypadButton () - -@property (nonatomic, strong, readwrite) TOPasscodeButtonLabel *buttonLabel; - -@end - -@implementation TOPasscodeSettingsKeypadButton - -+ (TOPasscodeSettingsKeypadButton *)button -{ - TOPasscodeSettingsKeypadButton *button = [TOPasscodeSettingsKeypadButton buttonWithType:UIButtonTypeCustom]; - button.frame = CGRectMake(0,0,100,60); - return button; -} - -#pragma mark - Lazy Accessor - -- (TOPasscodeButtonLabel *)buttonLabel -{ - if (_buttonLabel) { return _buttonLabel; } - - CGRect frame = self.bounds; - frame.size.height -= self.bottomInset; - - _buttonLabel = [[TOPasscodeButtonLabel alloc] initWithFrame:frame]; - _buttonLabel.userInteractionEnabled = NO; - _buttonLabel.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight; - [self addSubview:_buttonLabel]; - - return _buttonLabel; -} - -#pragma mark - Layout Accessor - -- (void)setBottomInset:(CGFloat)bottomInset -{ - _bottomInset = bottomInset; - - CGRect frame = self.bounds; - frame.size.height -= _bottomInset; - self.buttonLabel.frame = frame; - [self setNeedsLayout]; -} - -#pragma mark - Control Accessor - -- (void)setEnabled:(BOOL)enabled -{ - [super setEnabled:enabled]; - self.buttonLabel.alpha = enabled ? 1.0f : 0.5f; -} - -#pragma mark - Background Image Accessor - - -- (void)setHighlighted:(BOOL)highlighted { - [self.layer removeAllAnimations]; - [UIView transitionWithView:self - duration:0.25 - options:UIViewAnimationOptionTransitionCrossDissolve | - UIViewAnimationOptionAllowAnimatedContent | - UIViewAnimationOptionAllowUserInteraction - animations:^{ - [super setHighlighted:highlighted]; - } completion:nil]; -} - -- (void)setButtonBackgroundImage:(UIImage *)buttonBackgroundImage -{ - [self setBackgroundImage:buttonBackgroundImage forState:UIControlStateNormal]; -} - -- (UIImage *)buttonBackgroundImage { return [self backgroundImageForState:UIControlStateNormal]; } - -- (void)setButtonTappedBackgroundImage:(UIImage *)buttonTappedBackgroundImage -{ - [self setBackgroundImage:buttonTappedBackgroundImage forState:UIControlStateHighlighted]; -} - -- (UIImage *)buttonTappedBackgroundImage { return [self backgroundImageForState:UIControlStateHighlighted]; } - -@end diff --git a/iOSClient/Utility/TOPasscodeViewController/Views/Settings/TOPasscodeSettingsKeypadView.h b/iOSClient/Utility/TOPasscodeViewController/Views/Settings/TOPasscodeSettingsKeypadView.h deleted file mode 100644 index 13234bd07b..0000000000 --- a/iOSClient/Utility/TOPasscodeViewController/Views/Settings/TOPasscodeSettingsKeypadView.h +++ /dev/null @@ -1,74 +0,0 @@ -// -// TOPasscodeSettingsKeypadView.h -// -// Copyright 2017 Timothy Oliver. All rights reserved. -// -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files (the "Software"), to -// deal in the Software without restriction, including without limitation the -// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or -// sell copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: -// -// The above copyright notice and this permission notice shall be included in -// all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS -// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, -// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR -// IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - -#import -#import "TOPasscodeViewControllerConstants.h" - -NS_ASSUME_NONNULL_BEGIN - -/** - A keypad view of 9 buttons that allow numerical input on both iPad and iPhone. - Designed to match the base system, with a pseudo-skeuomorphical styling. - */ -@interface TOPasscodeSettingsKeypadView : UIView - -/* Whether the control is allowing input */ -@property (nonatomic, assign) BOOL enabled; - -/* Whether the view is currently light mode or dark. */ -@property (nonatomic, assign) TOPasscodeSettingsViewStyle style; - -/* The color of the separator line */ -@property (nonatomic, strong) UIColor *separatorLineColor; - -/* Labels in the buttons are laid out horizontally */ -@property (nonatomic, assign) BOOL buttonLabelHorizontalLayout; - -/* If overridden, the foreground color of the buttons */ -@property (nonatomic, assign) CGFloat keypadButtonBorderThickness; - -/* Untapped background images */ -@property (nonatomic, strong, null_resettable) UIColor *keypadButtonForegroundColor; -@property (nonatomic, strong, null_resettable) UIColor *keypadButtonBorderColor; - -/* Tapped background images */ -@property (nonatomic, strong, null_resettable) UIColor *keypadButtonTappedForegroundColor; -@property (nonatomic, strong, nullable) UIColor *keypadButtonTappedBorderColor; - -/* Button label styling */ -@property (nonatomic, strong) UIFont *keypadButtonNumberFont; -@property (nonatomic, strong) UIFont *keypadButtonLetteringFont; -@property (nonatomic, strong) UIColor *keypadButtonLabelTextColor; -@property (nonatomic, assign) CGFloat keypadButtonVerticalSpacing; -@property (nonatomic, assign) CGFloat keypadButtonHorizontalSpacing; -@property (nonatomic, assign) CGFloat keypadButtonLetteringSpacing; - -/* Callback handlers */ -@property (nonatomic, copy) void (^numberButtonTappedHandler)(NSInteger number); -@property (nonatomic, copy) void (^deleteButtonTappedHandler)(void); - -/* In really small sizes, set the keypad labels to horizontal */ -- (void)setButtonLabelHorizontalLayout:(BOOL)horizontal animated:(BOOL)animated; - -@end - -NS_ASSUME_NONNULL_END diff --git a/iOSClient/Utility/TOPasscodeViewController/Views/Settings/TOPasscodeSettingsKeypadView.m b/iOSClient/Utility/TOPasscodeViewController/Views/Settings/TOPasscodeSettingsKeypadView.m deleted file mode 100644 index eda81bb61b..0000000000 --- a/iOSClient/Utility/TOPasscodeViewController/Views/Settings/TOPasscodeSettingsKeypadView.m +++ /dev/null @@ -1,395 +0,0 @@ -// -// TOPasscodeSettingsKeypadView.m -// -// Copyright 2017 Timothy Oliver. All rights reserved. -// -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files (the "Software"), to -// deal in the Software without restriction, including without limitation the -// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or -// sell copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: -// -// The above copyright notice and this permission notice shall be included in -// all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS -// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, -// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR -// IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - -#import "TOPasscodeSettingsKeypadView.h" -#import "TOPasscodeSettingsKeypadButton.h" -#import "TOPasscodeButtonLabel.h" -#import "TOSettingsKeypadImage.h" - -const CGFloat kTOPasscodeSettingsKeypadButtonInnerSpacing = 7.0f; -const CGFloat kTOPasscodeSettingsKeypadButtonOuterSpacing = 7.0f; -const CGFloat kTOPasscodeSettingsKeypadCornderRadius = 12.0f; - -@interface TOPasscodeSettingsKeypadView () - -@property (nonatomic, strong) UIView *separatorView; -@property (nonatomic, strong) NSArray *keypadButtons; -@property (nonatomic, strong) UIButton *deleteButton; - -@property (nonatomic, strong) UIImage *buttonBackgroundImage; -@property (nonatomic, strong) UIImage *buttonTappedBackgroundImage; - -@end - -@implementation TOPasscodeSettingsKeypadView - -- (instancetype)initWithFrame:(CGRect)frame -{ - if (self = [super initWithFrame:frame]) { - [self setUp]; - } - - return self; -} - -- (void)setUp -{ - /* Button label styling */ - _keypadButtonNumberFont = [UIFont systemFontOfSize:32.0f weight:UIFontWeightRegular]; - _keypadButtonLetteringFont = [UIFont systemFontOfSize:11.0f weight:UIFontWeightRegular]; - _keypadButtonVerticalSpacing = 2.0f; - _keypadButtonHorizontalSpacing = 3.0f; - _keypadButtonLetteringSpacing = 2.0f; - - CGSize viewSize = self.frame.size; - CGFloat height = 1.0f / [[UIScreen mainScreen] scale]; - self.separatorView = [[UIView alloc] initWithFrame:(CGRect){CGPointZero,{viewSize.width, height}}]; - self.separatorView.autoresizingMask = UIViewAutoresizingFlexibleWidth; - [self addSubview:self.separatorView]; - - [self setUpKeypadButtons]; - [self setUpDeleteButton]; - - [self setUpDefaultValuesForStye:_style]; - [self applyTheme]; -} - -- (void)setUpKeypadButtons -{ - NSInteger numberOfButtons = 10; - NSArray *letteredTitles = @[@"ABC", @"DEF", @"GHI", @"JKL", - @"MNO", @"PQRS", @"TUV", @"WXYZ"]; - - NSMutableArray *buttons = [NSMutableArray arrayWithCapacity:10]; - for (NSInteger i = 0; i < numberOfButtons; i++) { - NSInteger number = (i+1) % 10; // Wrap around 0 at the end - TOPasscodeSettingsKeypadButton *button = [TOPasscodeSettingsKeypadButton button]; - button.buttonLabel.numberString = [NSString stringWithFormat:@"%ld", (long)number]; - button.bottomInset = 2.0f; - button.tag = number; - - if (i > 0) { - NSInteger j = i - 1; - if (j < letteredTitles.count) { - button.buttonLabel.letteringString = letteredTitles[j]; - } - } - - [button addTarget:self action:@selector(buttonTapped:) forControlEvents:UIControlEventTouchDown]; - - [self addSubview:button]; - [buttons addObject:button]; - } - - self.keypadButtons = [NSArray arrayWithArray:buttons]; -} - -- (void)setUpDeleteButton -{ - UIImage *deleteIcon = [TOSettingsKeypadImage deleteIcon]; - self.deleteButton = [UIButton buttonWithType:UIButtonTypeSystem]; - [self.deleteButton setImage:deleteIcon forState:UIControlStateNormal]; - self.deleteButton.contentMode = UIViewContentModeCenter; - self.deleteButton.frame = (CGRect){CGPointZero, deleteIcon.size}; - self.deleteButton.tintColor = [UIColor blackColor]; - [self.deleteButton addTarget:self action:@selector(buttonTapped:) forControlEvents:UIControlEventTouchUpInside]; - [self addSubview:self.deleteButton]; -} - -- (void)setUpDefaultValuesForStye:(TOPasscodeSettingsViewStyle)style -{ - BOOL isDark = style == TOPasscodeSettingsViewStyleDark; - - // Keypad label - self.keypadButtonLabelTextColor = isDark ? [UIColor whiteColor] : [UIColor blackColor]; - - self.keypadButtonForegroundColor = isDark ? [UIColor colorWithWhite:0.35f alpha:1.0f] : [UIColor whiteColor]; - self.keypadButtonTappedForegroundColor = isDark ? [UIColor colorWithWhite:0.45f alpha:1.0f] : [UIColor colorWithWhite:0.85f alpha:1.0f]; - - // Button border color - UIColor *borderColor = nil; - if (isDark) { - borderColor = [UIColor colorWithWhite:0.15f alpha:1.0f]; - } - else { - borderColor = [UIColor colorWithRed:166.0f/255.0f green:174.0f/255.0f blue:186.0f/255.0f alpha:1.0f]; - } - self.keypadButtonBorderColor = borderColor; - - // Background Color - UIColor *backgroundColor = nil; - if (isDark) { - backgroundColor = [UIColor colorWithWhite:0.18f alpha:1.0f]; - } - else { - backgroundColor = [UIColor colorWithRed:220.0f/255.0f green:225.0f/255.0f blue:232.0f/255.0f alpha:1.0f]; - } - self.backgroundColor = backgroundColor; - - // Separator lines - UIColor *separatorColor = nil; - if (isDark) { - separatorColor = [UIColor colorWithWhite:0.25f alpha:1.0f]; - } - else { - separatorColor = [UIColor colorWithWhite:0.7f alpha:1.0f]; - } - self.separatorView.backgroundColor = separatorColor; - - self.deleteButton.tintColor = isDark ? [UIColor whiteColor] : [UIColor blackColor]; -} - -- (void)setUpImagesIfNeeded -{ - if (self.buttonBackgroundImage && self.buttonTappedBackgroundImage) { - return; - } - - if (self.buttonBackgroundImage == nil) { - self.buttonBackgroundImage = [TOSettingsKeypadImage buttonImageWithCornerRadius:kTOPasscodeSettingsKeypadCornderRadius - foregroundColor:self.keypadButtonForegroundColor - edgeColor:self.keypadButtonBorderColor - edgeThickness:2.0f]; - } - - if (self.buttonTappedBackgroundImage == nil) { - self.buttonTappedBackgroundImage = [TOSettingsKeypadImage buttonImageWithCornerRadius:kTOPasscodeSettingsKeypadCornderRadius - foregroundColor:self.keypadButtonTappedForegroundColor - edgeColor:self.keypadButtonBorderColor - edgeThickness:2.0f]; - } - - for (TOPasscodeSettingsKeypadButton *button in self.keypadButtons) { - button.buttonBackgroundImage = self.buttonBackgroundImage; - button.buttonTappedBackgroundImage = self.buttonTappedBackgroundImage; - } -} - -- (void)applyTheme -{ - for (TOPasscodeSettingsKeypadButton *button in self.keypadButtons) { - button.buttonLabel.textColor = self.keypadButtonLabelTextColor; - button.buttonLabel.letteringCharacterSpacing = self.keypadButtonLetteringSpacing; - button.buttonLabel.letteringVerticalSpacing = self.keypadButtonVerticalSpacing; - button.buttonLabel.letteringHorizontalSpacing = self.keypadButtonHorizontalSpacing; - button.buttonLabel.numberLabelFont = self.keypadButtonNumberFont; - button.buttonLabel.letteringLabelFont = self.keypadButtonLetteringFont; - } -} - -- (void)layoutSubviews -{ - [super layoutSubviews]; - [self setUpImagesIfNeeded]; - - CGFloat outerSpacing = kTOPasscodeSettingsKeypadButtonOuterSpacing; - CGFloat innerSpacing = kTOPasscodeSettingsKeypadButtonInnerSpacing; - - CGSize viewSize = self.bounds.size; - CGSize buttonSize = CGSizeZero; - - viewSize.width -= (outerSpacing * 2.0f); - viewSize.height -= (outerSpacing * 2.0f); - - // Pull the buttons up to avoid overlapping the home indicator on iPhone X - if (@available(iOS 11.0, *)) { - viewSize.height -= self.safeAreaInsets.bottom; - } - - // Four rows of three buttons - buttonSize.width = floorf((viewSize.width - (innerSpacing * 2.0f)) / 3.0f); - buttonSize.height = floorf((viewSize.height - (innerSpacing * 3.0f)) / 4.0f); - - CGPoint point = CGPointMake(outerSpacing, outerSpacing); - CGRect buttonFrame = (CGRect){point, buttonSize}; - - NSInteger i = 0; - for (TOPasscodeSettingsKeypadButton *button in self.keypadButtons) { - button.frame = buttonFrame; - buttonFrame.origin.x += buttonFrame.size.width + innerSpacing; - - if (++i % 3 == 0) { - buttonFrame.origin.x = outerSpacing; - buttonFrame.origin.y += buttonFrame.size.height + innerSpacing; - } - - if (button == self.keypadButtons.lastObject) { - button.frame = buttonFrame; - } - } - - //Layout delete button - CGSize boundsSize = self.bounds.size; - - // Adjust for home indicator on iPhone X - if (@available(iOS 11.0, *)) { - boundsSize.height -= self.safeAreaInsets.bottom; - } - - CGRect frame = self.deleteButton.frame; - frame.size = buttonSize; - frame.origin.x = boundsSize.width - (outerSpacing + buttonSize.width * 0.5f); - frame.origin.x -= (CGRectGetWidth(frame) * 0.5f); - frame.origin.y = boundsSize.height - (outerSpacing + buttonSize.height * 0.5f); - frame.origin.y -= (CGRectGetHeight(frame) * 0.5f); - self.deleteButton.frame = frame; -} - -#pragma mark - Interaction - -- (void)buttonTapped:(id)sender -{ - // Handler for the delete button - if (sender == self.deleteButton) { - if (self.deleteButtonTappedHandler) { - self.deleteButtonTappedHandler(); - } - return; - } - - // Handler for the keypad buttons - UIButton *button = (UIButton *)sender; - NSInteger number = button.tag; - - [[UIDevice currentDevice] playInputClick]; - - if (self.numberButtonTappedHandler) { - self.numberButtonTappedHandler(number); - } -} - -#pragma mark - Accessors - - -- (void)setStyle:(TOPasscodeSettingsViewStyle)style -{ - if (style == _style) { - return; - } - - _style = style; - [self setUpDefaultValuesForStye:_style]; - [self applyTheme]; -} - -#pragma mark - Label Layout - -- (void)setButtonLabelHorizontalLayout:(BOOL)buttonLabelHorizontalLayout -{ - [self setButtonLabelHorizontalLayout:buttonLabelHorizontalLayout animated:NO]; -} - -- (void)setButtonLabelHorizontalLayout:(BOOL)horizontal animated:(BOOL)animated -{ - if (horizontal == _buttonLabelHorizontalLayout) { return; } - - _buttonLabelHorizontalLayout = horizontal; - - for (TOPasscodeSettingsKeypadButton *button in self.keypadButtons) { - if (!animated) { - button.buttonLabel.horizontalLayout = horizontal; - continue; - } - - UIView *snapshotView = [button.buttonLabel snapshotViewAfterScreenUpdates:NO]; - snapshotView.autoresizingMask = UIViewAutoresizingFlexibleLeftMargin | UIViewAutoresizingFlexibleRightMargin; - [button addSubview:snapshotView]; - - button.buttonLabel.horizontalLayout = horizontal; - [button.buttonLabel setNeedsLayout]; - [button.buttonLabel layoutIfNeeded]; - - [button.buttonLabel.layer removeAllAnimations]; - for (CALayer *sublayer in button.buttonLabel.layer.sublayers) { - [sublayer removeAllAnimations]; - } - - button.buttonLabel.alpha = 0.0f; - [UIView animateWithDuration:0.4f animations:^{ - button.buttonLabel.alpha = 1.0f; - snapshotView.alpha = 0.0f; - snapshotView.center = button.buttonLabel.center; - } completion:^(BOOL complete) { - [snapshotView removeFromSuperview]; - }]; - } -} - -#pragma mark - Null Resettable Accessors - -- (void)setKeypadButtonForegroundColor:(nullable UIColor *)keypadButtonForegroundColor -{ - if (keypadButtonForegroundColor == _keypadButtonForegroundColor) { return; } - _keypadButtonForegroundColor = keypadButtonForegroundColor; - - if (_keypadButtonForegroundColor == nil) { - BOOL isDark = self.style == TOPasscodeSettingsViewStyleDark; - _keypadButtonForegroundColor = isDark ? [UIColor colorWithWhite:0.3f alpha:1.0f] : [UIColor whiteColor]; - } - - self.buttonBackgroundImage = nil; - [self setNeedsLayout]; -} - -- (void)setKeypadButtonBorderColor:(nullable UIColor *)keypadButtonBorderColor -{ - if (keypadButtonBorderColor == _keypadButtonBorderColor) { return; } - _keypadButtonBorderColor = keypadButtonBorderColor; - - if (_keypadButtonBorderColor == nil) { - BOOL isDark = self.style == TOPasscodeSettingsViewStyleDark; - UIColor *borderColor = nil; - if (isDark) { - borderColor = [UIColor colorWithWhite:0.2 alpha:1.0f]; - } - else { - borderColor = [UIColor colorWithRed:166.0f/255.0f green:174.0f/255.0f blue:186.0f/255.0f alpha:1.0f]; - } - _keypadButtonBorderColor = borderColor; - } - - self.buttonBackgroundImage = nil; - [self setNeedsLayout]; -} - -- (void)setKeypadButtonTappedForegroundColor:(nullable UIColor *)keypadButtonTappedForegroundColor -{ - if (keypadButtonTappedForegroundColor == _keypadButtonTappedForegroundColor) { return; } - _keypadButtonTappedForegroundColor = keypadButtonTappedForegroundColor; - - if (_keypadButtonTappedForegroundColor == nil) { - BOOL isDark = self.style == TOPasscodeSettingsViewStyleDark; - _keypadButtonTappedForegroundColor = isDark ? [UIColor colorWithWhite:0.4f alpha:1.0f] : [UIColor colorWithWhite:0.85f alpha:1.0f]; - } - - self.buttonTappedBackgroundImage = nil; - [self setNeedsLayout]; -} - -- (void)setEnabled:(BOOL)enabled -{ - _enabled = enabled; - - for (TOPasscodeSettingsKeypadButton *button in self.keypadButtons) { - button.enabled = enabled; - } - - self.deleteButton.enabled = enabled; -} - -@end diff --git a/iOSClient/Utility/TOPasscodeViewController/Views/Settings/TOPasscodeSettingsWarningLabel.h b/iOSClient/Utility/TOPasscodeViewController/Views/Settings/TOPasscodeSettingsWarningLabel.h deleted file mode 100644 index 5455a70586..0000000000 --- a/iOSClient/Utility/TOPasscodeViewController/Views/Settings/TOPasscodeSettingsWarningLabel.h +++ /dev/null @@ -1,43 +0,0 @@ -// -// TOPasscodeSettingsWarningLabel.h -// -// Copyright 2017 Timothy Oliver. All rights reserved. -// -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files (the "Software"), to -// deal in the Software without restriction, including without limitation the -// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or -// sell copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: -// -// The above copyright notice and this permission notice shall be included in -// all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS -// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, -// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR -// IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - -#import - -/** - When a user enters an incorrect passcode in the settings interface, - this view is displayed to show the number of failed attempts. - */ -@interface TOPasscodeSettingsWarningLabel : UIImageView - -/** The number of incorrect passcode attempts to display */ -@property (nonatomic, assign) NSInteger numberOfWarnings; - -/** The font of the text */ -@property (nonatomic, strong) UIFont *textFont; - -/** The background color of the view */ -@property (nonatomic, strong) UIColor *backgroundColor; - -/** Set the padding around the label */ -@property (nonatomic, assign) CGSize textPadding; - -@end diff --git a/iOSClient/Utility/TOPasscodeViewController/Views/Settings/TOPasscodeSettingsWarningLabel.m b/iOSClient/Utility/TOPasscodeViewController/Views/Settings/TOPasscodeSettingsWarningLabel.m deleted file mode 100644 index 27aa470507..0000000000 --- a/iOSClient/Utility/TOPasscodeViewController/Views/Settings/TOPasscodeSettingsWarningLabel.m +++ /dev/null @@ -1,154 +0,0 @@ -// -// TOPasscodeSettingsWarningLabel.m -// -// Copyright 2017 Timothy Oliver. All rights reserved. -// -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files (the "Software"), to -// deal in the Software without restriction, including without limitation the -// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or -// sell copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: -// -// The above copyright notice and this permission notice shall be included in -// all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS -// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, -// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR -// IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - -#import "TOPasscodeSettingsWarningLabel.h" - -@interface TOPasscodeSettingsWarningLabel () -@property (nonatomic, strong) UILabel *label; -@end - -@implementation TOPasscodeSettingsWarningLabel - -@synthesize backgroundColor = __backgroundColor; - -#pragma mark - View Setup - - -- (instancetype)initWithFrame:(CGRect)frame -{ - if (self = [super initWithFrame:frame]) { - [self setUp]; - } - - return self; -} - -- (void)setUp -{ - _numberOfWarnings = 0; - _textPadding = CGSizeMake(14.0f, 6.0f); - - self.tintColor = [UIColor colorWithRed:214.0f/255.0f green:63.0f/255.0f blue:63.0f/255.0f alpha:1.0f]; - - self.label = [[UILabel alloc] initWithFrame:CGRectZero]; - self.label.backgroundColor = [UIColor clearColor]; - self.label.textAlignment = NSTextAlignmentCenter; - self.label.textColor = [UIColor whiteColor]; - self.label.font = [UIFont systemFontOfSize:15.0f]; - [self setTextForCount:0]; - [self.label sizeToFit]; - [self addSubview:self.label]; -} - -- (void)didMoveToSuperview -{ - [super didMoveToSuperview]; - [self setBackgroundImageIfNeeded]; -} - -#pragma mark - View Layout - - -- (void)sizeToFit -{ - [super sizeToFit]; - [self.label sizeToFit]; - - CGRect labelFrame = self.label.frame; - CGRect frame = self.frame; - - labelFrame = CGRectInset(labelFrame, -self.textPadding.width, -self.textPadding.height); - frame.size = labelFrame.size; - self.frame = CGRectIntegral(frame); -} - -- (void)layoutSubviews -{ - CGRect frame = self.frame; - CGRect labelFrame = self.label.frame; - - labelFrame.origin.x = (CGRectGetWidth(frame) - CGRectGetWidth(labelFrame)) * 0.5f; - labelFrame.origin.y = (CGRectGetHeight(frame) - CGRectGetHeight(labelFrame)) * 0.5f; - self.label.frame = labelFrame; -} - -#pragma mark - View State Handling - - -- (void)setTextForCount:(NSInteger)count -{ - NSString *text = nil; - if (count == 1) { - text = NSLocalizedString(@"1 Failed Passcode Attempt", @""); - } - else { - text = [NSString stringWithFormat:NSLocalizedString(@"%ld Failed Passcode Attempts", @""), count]; - } - self.label.text = text; - - [self sizeToFit]; -} - -#pragma mark - Background Image Managements - - -- (void)setBackgroundImageIfNeeded -{ - // Don't bother if we're not in a view - if (self.superview == nil) { return; } - - // Compare the view height and don't proceed if - if (lround(self.image.size.height) == lround(self.frame.size.height)) { return; } - - // Create the image - self.image = [[self class] roundedBackgroundImageWithHeight:self.frame.size.height]; -} - -+ (UIImage *)roundedBackgroundImageWithHeight:(CGFloat)height -{ - UIImage *image = nil; - CGRect frame = CGRectZero; - frame.size.width = height + 1.0; - frame.size.height = height; - - UIGraphicsBeginImageContextWithOptions(frame.size, NO, 0.0f); - { - UIBezierPath* path = [UIBezierPath bezierPathWithRoundedRect:frame cornerRadius:height * 0.5f]; - [[UIColor blackColor] setFill]; - [path fill]; - - image = UIGraphicsGetImageFromCurrentImageContext(); - } - UIGraphicsEndImageContext(); - - CGFloat halfHeight = height * 0.5f; - UIEdgeInsets insets = UIEdgeInsetsMake(halfHeight, halfHeight, halfHeight, halfHeight); - image = [image imageWithRenderingMode:UIImageRenderingModeAlwaysTemplate]; - image = [image resizableImageWithCapInsets:insets]; - return image; -} - -#pragma mark - Accessors - - -- (void)setNumberOfWarnings:(NSInteger)numberOfWarnings -{ - _numberOfWarnings = numberOfWarnings; - [self setTextForCount:_numberOfWarnings]; -} - -@end diff --git a/iOSClient/Utility/TOPasscodeViewController/Views/Shared/TOPasscodeButtonLabel.h b/iOSClient/Utility/TOPasscodeViewController/Views/Shared/TOPasscodeButtonLabel.h deleted file mode 100644 index 12b4d00fef..0000000000 --- a/iOSClient/Utility/TOPasscodeViewController/Views/Shared/TOPasscodeButtonLabel.h +++ /dev/null @@ -1,61 +0,0 @@ -// -// TOPasscodeButtonLabel.h -// -// Copyright 2017 Timothy Oliver. All rights reserved. -// -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files (the "Software"), to -// deal in the Software without restriction, including without limitation the -// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or -// sell copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: -// -// The above copyright notice and this permission notice shall be included in -// all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS -// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, -// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR -// IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - -#import - -NS_ASSUME_NONNULL_BEGIN - -/** - A view that manages two label subviews: a larger label showing a single number - and a smaller label showing lettering as well. - */ -@interface TOPasscodeButtonLabel : UIView - -// Draws the lettering label to the side -@property (nonatomic, assign) BOOL horizontalLayout; - -// The strings of both labels -@property (nonatomic, copy) NSString *numberString; -@property (nonatomic, copy, nullable) NSString *letteringString; - -// The color of both labels -@property (nonatomic, strong) UIColor *textColor; - -// The label views -@property (nonatomic, readonly) UILabel *numberLabel; -@property (nonatomic, readonly) UILabel *letteringLabel; - -// The fonts for each label (In case they are nil) -@property (nonatomic, strong) UIFont *numberLabelFont; -@property (nonatomic, strong) UIFont *letteringLabelFont; - -// Has initial default values -@property (nonatomic, assign) CGFloat letteringCharacterSpacing; -@property (nonatomic, assign) CGFloat letteringVerticalSpacing; -@property (nonatomic, assign) CGFloat letteringHorizontalSpacing; - -// Whether the number label is centered vertically or not (NO by default) -@property (nonatomic, assign) BOOL verticallyCenterNumberLabel; - -@end - -NS_ASSUME_NONNULL_END diff --git a/iOSClient/Utility/TOPasscodeViewController/Views/Shared/TOPasscodeButtonLabel.m b/iOSClient/Utility/TOPasscodeViewController/Views/Shared/TOPasscodeButtonLabel.m deleted file mode 100644 index d3df12ab45..0000000000 --- a/iOSClient/Utility/TOPasscodeViewController/Views/Shared/TOPasscodeButtonLabel.m +++ /dev/null @@ -1,186 +0,0 @@ -// -// TOPasscodeButtonLabel.m -// -// Copyright 2017 Timothy Oliver. All rights reserved. -// -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files (the "Software"), to -// deal in the Software without restriction, including without limitation the -// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or -// sell copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: -// -// The above copyright notice and this permission notice shall be included in -// all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS -// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, -// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR -// IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - -#import "TOPasscodeButtonLabel.h" - -@interface TOPasscodeButtonLabel () - -@property (nonatomic, strong, readwrite) UILabel *numberLabel; -@property (nonatomic, strong, readwrite) UILabel *letteringLabel; - -@end - -@implementation TOPasscodeButtonLabel - -#pragma mark - View Setup - - -- (instancetype)initWithFrame:(CGRect)frame -{ - if (self = [super initWithFrame:frame]) { - _letteringVerticalSpacing = 6.0f; - _letteringCharacterSpacing = 3.0f; - _letteringHorizontalSpacing = 5.0f; - _numberLabelFont = [UIFont systemFontOfSize:37.5f weight:UIFontWeightThin]; - _letteringLabelFont = [UIFont systemFontOfSize:9.0f weight:UIFontWeightThin]; - [self setUpViews]; - } - - return self; -} - -- (void)setUpViews -{ - if (!self.numberLabel) { - self.numberLabel = [[UILabel alloc] initWithFrame:CGRectZero]; - self.numberLabel.text = self.numberString; - self.numberLabel.textColor = self.textColor; - self.numberLabel.font = self.numberLabelFont; - [self.numberLabel sizeToFit]; - [self addSubview:self.numberLabel]; - } - - // Create the lettering string only if we have a lettering value for it - if (!self.letteringLabel && self.letteringString.length > 0) { - self.letteringLabel = [[UILabel alloc] initWithFrame:CGRectZero]; - self.letteringLabel.textColor = self.textColor; - self.letteringLabel.font = self.letteringLabelFont; - [self.letteringLabel sizeToFit]; - [self addSubview:self.letteringLabel]; - [self updateLetteringLabelText]; - } -} - -#pragma mark - View Layout - - -- (void)updateLetteringLabelText -{ - if (self.letteringString.length == 0) { - return; - } - - NSMutableAttributedString* attrStr = [[NSMutableAttributedString alloc] initWithString:self.letteringString]; - [attrStr addAttribute:NSKernAttributeName value:@(_letteringCharacterSpacing) range:NSMakeRange(0, attrStr.length-1)]; - self.letteringLabel.attributedText = attrStr; -} - -- (void)layoutSubviews -{ - [super layoutSubviews]; - - CGSize viewSize = self.bounds.size; - - [self.numberLabel sizeToFit]; - [self.letteringLabel sizeToFit]; - - CGFloat numberVerticalHeight = self.numberLabelFont.capHeight; - CGFloat letteringVerticalHeight = self.letteringLabelFont.capHeight; - CGFloat textTotalHeight = (numberVerticalHeight+2.0f) + self.letteringVerticalSpacing + (letteringVerticalHeight+2.0f); - - CGRect frame = self.numberLabel.frame; - frame.size.height = ceil(numberVerticalHeight) + 2.0f; - frame.origin.x = ceilf((viewSize.width - frame.size.width) * 0.5f); - - if (!self.horizontalLayout && !self.verticallyCenterNumberLabel) { - frame.origin.y = floorf((viewSize.height - textTotalHeight) * 0.5f); - } - else { - frame.origin.y = floorf((viewSize.height - frame.size.height) * 0.5f); - } - self.numberLabel.frame = CGRectIntegral(frame); - - if (self.letteringLabel) { - CGFloat y = CGRectGetMaxY(frame); - y += self.letteringVerticalSpacing; - - frame = self.letteringLabel.frame; - frame.size.height = ceil(letteringVerticalHeight) + 2.0f; - - if (!self.horizontalLayout) { - frame.origin.y = floorf(y); - frame.origin.x = (viewSize.width - frame.size.width) * 0.5f; - } - else { - frame.origin.y = floorf((viewSize.height - frame.size.height) * 0.5f); - frame.origin.x = CGRectGetMaxX(self.numberLabel.frame) + self.letteringHorizontalSpacing; - } - - self.letteringLabel.frame = CGRectIntegral(frame); - } -} - -#pragma mark - Accessors - - -- (void)setTextColor:(UIColor *)textColor -{ - if (textColor == _textColor) { return; } - _textColor = textColor; - - self.numberLabel.textColor = _textColor; - self.letteringLabel.textColor = _textColor; -} -/***********************************************************/ - -- (void)setNumberString:(NSString *)numberString -{ - self.numberLabel.text = numberString; - [self setNeedsLayout]; -} - -- (NSString *)numberString { return self.numberLabel.text; } - -/***********************************************************/ - -- (void)setLetteringString:(NSString *)letteringString -{ - _letteringString = [letteringString copy]; - [self setUpViews]; - [self updateLetteringLabelText]; - [self setNeedsLayout]; -} - -/***********************************************************/ - -- (void)setLetteringCharacterSpacing:(CGFloat)letteringCharacterSpacing -{ - _letteringCharacterSpacing = letteringCharacterSpacing; - [self updateLetteringLabelText]; -} - -/***********************************************************/ - -- (void)setNumberLabelFont:(UIFont *)numberLabelFont -{ - if (_numberLabelFont == numberLabelFont) { return; } - _numberLabelFont = numberLabelFont; - self.numberLabel.font = _numberLabelFont; -} - -/***********************************************************/ - -- (void)setLetteringLabelFont:(UIFont *)letteringLabelFont -{ - if (_letteringLabelFont == letteringLabelFont) { return; } - _letteringLabelFont = letteringLabelFont; - self.letteringLabel.font = letteringLabelFont; -} - -@end diff --git a/iOSClient/Utility/TOPasscodeViewController/Views/Shared/TOPasscodeCircleView.h b/iOSClient/Utility/TOPasscodeViewController/Views/Shared/TOPasscodeCircleView.h deleted file mode 100644 index 5dc9ca2251..0000000000 --- a/iOSClient/Utility/TOPasscodeViewController/Views/Shared/TOPasscodeCircleView.h +++ /dev/null @@ -1,46 +0,0 @@ -// -// TOPasscodeCircleView.h -// -// Copyright 2017 Timothy Oliver. All rights reserved. -// -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files (the "Software"), to -// deal in the Software without restriction, including without limitation the -// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or -// sell copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: -// -// The above copyright notice and this permission notice shall be included in -// all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS -// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, -// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR -// IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - -#import - -NS_ASSUME_NONNULL_BEGIN - -/** - A view containing two circle image views that can animate - between filled and hollow, whilst maintaining compatibility - with translucency views. - */ -@interface TOPasscodeCircleView : UIView - -/* The circle patterns used for neutral and highlighted states. */ -@property (nonatomic, strong) UIImage *circleImage; -@property (nonatomic, strong) UIImage *highlightedCircleImage; - -/* Whether the highlighted view is visible. */ -@property (nonatomic, assign) BOOL isHighlighted; - -/* Animate the circle to be highlighted */ -- (void)setHighlighted:(BOOL)highlighted animated:(BOOL)animated; - -@end - -NS_ASSUME_NONNULL_END diff --git a/iOSClient/Utility/TOPasscodeViewController/Views/Shared/TOPasscodeCircleView.m b/iOSClient/Utility/TOPasscodeViewController/Views/Shared/TOPasscodeCircleView.m deleted file mode 100644 index 3bb5de974a..0000000000 --- a/iOSClient/Utility/TOPasscodeViewController/Views/Shared/TOPasscodeCircleView.m +++ /dev/null @@ -1,89 +0,0 @@ -// -// TOPasscodeCircleView.m -// -// Copyright 2017 Timothy Oliver. All rights reserved. -// -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files (the "Software"), to -// deal in the Software without restriction, including without limitation the -// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or -// sell copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: -// -// The above copyright notice and this permission notice shall be included in -// all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS -// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, -// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR -// IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - -#import "TOPasscodeCircleView.h" - -@interface TOPasscodeCircleView () -@property (nonatomic, strong) UIImageView *bottomView; -@property (nonatomic, strong) UIImageView *topView; -@end - -@implementation TOPasscodeCircleView - -- (instancetype)initWithFrame:(CGRect)frame -{ - if (self = [super initWithFrame:frame]) { - self.userInteractionEnabled = NO; - - self.bottomView = [[UIImageView alloc] initWithFrame:self.bounds]; - self.bottomView.userInteractionEnabled = NO; - self.bottomView.contentMode = UIViewContentModeCenter; - self.bottomView.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight; - [self addSubview:self.bottomView]; - - self.topView = [[UIImageView alloc] initWithFrame:self.bounds]; - self.topView.userInteractionEnabled = NO; - self.topView.contentMode = UIViewContentModeCenter; - self.topView.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight; - self.topView.alpha = 0.0f; - [self addSubview:self.topView]; - } - - return self; -} - -- (void)setIsHighlighted:(BOOL)isHighlighted -{ - [self setHighlighted:isHighlighted animated:NO]; -} - -- (void)setHighlighted:(BOOL)highlighted animated:(BOOL)animated -{ - if (highlighted == self.isHighlighted) { return; } - - _isHighlighted = highlighted; - - void (^animationBlock)(void) = ^{ - self.topView.alpha = highlighted ? 1.0f : 0.0f; - }; - - if (!animated) { - animationBlock(); - return; - } - - [UIView animateWithDuration:0.45f animations:animationBlock]; -} - -- (void)setCircleImage:(UIImage *)circleImage -{ - _circleImage = circleImage; - self.bottomView.image = circleImage; -} - -- (void)setHighlightedCircleImage:(UIImage *)highlightedCircleImage -{ - _highlightedCircleImage = highlightedCircleImage; - self.topView.image = highlightedCircleImage; -} - -@end diff --git a/iOSClient/Utility/TOPasscodeViewController/Views/Shared/TOPasscodeFixedInputView.h b/iOSClient/Utility/TOPasscodeViewController/Views/Shared/TOPasscodeFixedInputView.h deleted file mode 100644 index a448d8ca0b..0000000000 --- a/iOSClient/Utility/TOPasscodeViewController/Views/Shared/TOPasscodeFixedInputView.h +++ /dev/null @@ -1,54 +0,0 @@ -// -// TOPasscodeFixedInputView.h -// -// Copyright 2017 Timothy Oliver. All rights reserved. -// -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files (the "Software"), to -// deal in the Software without restriction, including without limitation the -// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or -// sell copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: -// -// The above copyright notice and this permission notice shall be included in -// all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS -// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, -// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR -// IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - -#import - -@class TOPasscodeCircleView; - -/** - A basic content view showing a row of circles that can be used to represent - a fixed size passcode. - */ -@interface TOPasscodeFixedInputView : UIView - -/* The size of each circle in this view (Default is 16) */ -@property (nonatomic, assign) CGFloat circleDiameter; - -/* The spacing between each circle (Default is 25.0f) */ -@property (nonatomic, assign) CGFloat circleSpacing; - -/* The number of circles in this view (Default is 4) */ -@property (nonatomic, assign) NSInteger length; - -/* The number of highlighted circles */ -@property (nonatomic, assign) NSInteger highlightedLength; - -/* The circle views managed by this view */ -@property (nonatomic, strong, readonly) NSArray *circleViews; - -/* Init with a set number of circles */ -- (instancetype)initWithLength:(NSInteger)length; - -/* Set the number of highlighted circles */ -- (void)setHighlightedLength:(NSInteger)highlightedLength animated:(BOOL)animated; - -@end diff --git a/iOSClient/Utility/TOPasscodeViewController/Views/Shared/TOPasscodeFixedInputView.m b/iOSClient/Utility/TOPasscodeViewController/Views/Shared/TOPasscodeFixedInputView.m deleted file mode 100644 index e4dac42805..0000000000 --- a/iOSClient/Utility/TOPasscodeViewController/Views/Shared/TOPasscodeFixedInputView.m +++ /dev/null @@ -1,171 +0,0 @@ -// -// TOPasscodeFixedInputView.h -// -// Copyright 2017 Timothy Oliver. All rights reserved. -// -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files (the "Software"), to -// deal in the Software without restriction, including without limitation the -// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or -// sell copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: -// -// The above copyright notice and this permission notice shall be included in -// all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS -// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, -// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR -// IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - -#import "TOPasscodeFixedInputView.h" -#import "TOPasscodeCircleView.h" -#import "TOPasscodeCircleImage.h" - -@interface TOPasscodeFixedInputView () - -@property (nonatomic, strong, readwrite) NSArray *circleViews; -@property (nonatomic, strong) UIImage *circleImage; -@property (nonatomic, strong) UIImage *highlightedCircleImage; - -@end - -@implementation TOPasscodeFixedInputView - -#pragma mark - Object Creation - - -- (instancetype)initWithLength:(NSInteger)length -{ - if (self = [self initWithFrame:CGRectZero]) { - _length = length; - } - - return self; -} - -- (instancetype)initWithFrame:(CGRect)frame -{ - if (self = [super initWithFrame:frame]) { - _circleSpacing = 25.0f; - _circleDiameter = 16.0f; - _length = 4; - } - - return self; -} - -#pragma mark - View Configuration - - -- (void)sizeToFit -{ - // Resize the view to encompass the circles - CGRect frame = self.frame; - frame.size.width = (_circleDiameter * _length) + (_circleSpacing * (_length - 1)) + 2.0f; - frame.size.height = _circleDiameter + 2.0f; - self.frame = CGRectIntegral(frame); -} - -- (void)layoutSubviews -{ - CGRect frame = CGRectZero; - frame.size = (CGSize){self.circleDiameter + 2.0f, self.circleDiameter + 2.0f}; - - for (TOPasscodeCircleView *circleView in self.circleViews) { - circleView.frame = frame; - frame.origin.x += self.circleDiameter + self.circleSpacing; - } -} - -#pragma mark - State Configuration - - -- (void)setHighlightedLength:(NSInteger)highlightedLength animated:(BOOL)animated -{ - NSInteger i = 0; - for (TOPasscodeCircleView *circleView in self.circleViews) { - [circleView setHighlighted:(i < highlightedLength) animated:animated]; - i++; - } -} - -#pragma mark - Circle View Configuration - - -- (void)setCircleViewsForLength:(NSInteger)length -{ - NSMutableArray *circleViews = [NSMutableArray array]; - if (self.circleViews) { - [circleViews addObjectsFromArray:self.circleViews]; - } - - [UIView performWithoutAnimation:^{ - while (circleViews.count != length) { - // Remove any extra circle views - if (circleViews.count > length) { - TOPasscodeCircleView *lastCircle = circleViews.lastObject; - [lastCircle removeFromSuperview]; - [circleViews removeLastObject]; - continue; - } - - // Add any new circle views - TOPasscodeCircleView *newCircleView = [[TOPasscodeCircleView alloc] init]; - [self setImagesOfCircleView:newCircleView]; - [self addSubview:newCircleView]; - [circleViews addObject:newCircleView]; - } - - self.circleViews = [NSArray arrayWithArray:circleViews]; - [self setNeedsLayout]; - [self layoutIfNeeded]; - }]; -} - -- (void)setCircleImagesForDiameter:(CGFloat)diameter -{ - self.circleImage = [TOPasscodeCircleImage hollowCircleImageOfSize:diameter strokeWidth:1.2f padding:1.0f]; - self.highlightedCircleImage = [TOPasscodeCircleImage circleImageOfSize:diameter inset:0.5f padding:1.0f antialias:YES]; - - for (TOPasscodeCircleView *circleView in self.circleViews) { - [self setImagesOfCircleView:circleView]; - } -} - -- (void)setImagesOfCircleView:(TOPasscodeCircleView *)circleView -{ - circleView.circleImage = self.circleImage; - circleView.highlightedCircleImage = self.highlightedCircleImage; -} - -#pragma mark - Accessors - - -- (NSArray *)circleViews -{ - if (_circleViews) { return _circleViews; } - _circleViews = [NSArray array]; - [self setCircleViewsForLength:self.length]; - [self setCircleImagesForDiameter:self.circleDiameter]; - return _circleViews; -} - -- (void)setCircleDiameter:(CGFloat)circleDiameter -{ - if (circleDiameter == _circleDiameter) { return; } - _circleDiameter = circleDiameter; - [self setCircleImagesForDiameter:_circleDiameter]; - [self sizeToFit]; -} - -- (void)setLength:(NSInteger)length -{ - if (_length == length) { return; } - _length = length; - [self setCircleViewsForLength:length]; -} - -- (void)setHighlightedLength:(NSInteger)highlightedLength -{ - [self setHighlightedLength:highlightedLength animated:NO]; -} - -@end diff --git a/iOSClient/Utility/TOPasscodeViewController/Views/Shared/TOPasscodeInputField.h b/iOSClient/Utility/TOPasscodeViewController/Views/Shared/TOPasscodeInputField.h deleted file mode 100644 index 05103b27d9..0000000000 --- a/iOSClient/Utility/TOPasscodeViewController/Views/Shared/TOPasscodeInputField.h +++ /dev/null @@ -1,111 +0,0 @@ -// -// TOPasscodeInputField.h -// -// Copyright 2017 Timothy Oliver. All rights reserved. -// -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files (the "Software"), to -// deal in the Software without restriction, including without limitation the -// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or -// sell copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: -// -// The above copyright notice and this permission notice shall be included in -// all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS -// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, -// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR -// IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - -#import - -#import "TOPasscodeFixedInputView.h" -#import "TOPasscodeVariableInputView.h" - - -NS_ASSUME_NONNULL_BEGIN - -typedef NS_ENUM(NSInteger, TOPasscodeInputFieldStyle) { - TOPasscodeInputFieldStyleFixed, // The passcode explicitly requires a specific number of characters (Shows hollow circles) - TOPasscodeInputFieldStyleVariable // The passcode can be any arbitrary number of characters (Shows an empty rectangle) -}; - -/** - An abstract input view capable of receiving different types of passcodes. - When a fixed character passcode is specified, the view shows a row of circles. - When a variable passcode is specified, a rounded rectangle is shown. - */ -@interface TOPasscodeInputField : UIView - -/* The visual effects view used to control the vibrancy of the input field */ -@property (nonatomic, strong, readonly) UIVisualEffectView *visualEffectView; - -/* The input style of this control */ -@property (nonatomic, assign) TOPasscodeInputFieldStyle style; - -/* A row of hollow circles at a preset length. Valid only when `style` is set to `fixed` */ -@property (nonatomic, readonly, nullable) TOPasscodeFixedInputView *fixedInputView; - -/* A rounded rectangle representing a passcode of arbitrary length. Valid only when `style` is set to `variable`. */ -@property (nonatomic, readonly, nullable) TOPasscodeVariableInputView *variableInputView; - -/* The 'submit' button shown when `showSubmitButton` is true. */ -@property (nonatomic, readonly, nullable) UIButton *submitButton; - -/* Shows an 'OK' button next to the view when characters have been added. */ -@property (nonatomic, assign) BOOL showSubmitButton; - -/* The amount of spacing between the 'OK' button and the passcode field */ -@property (nonatomic, assign) CGFloat submitButtonSpacing; - -/* The amount of spacing between the 'OK' button and the passcode field */ -@property (nonatomic, assign) CGFloat submitButtonVerticalSpacing; - -/* The font size of the submit button */ -@property (nonatomic, assign) CGFloat submitButtonFontSize; - -/* The current passcode entered into this view */ -@property (nonatomic, copy, nullable) NSString *passcode; - -/* If this view is directly receiving input, this can change the `UIKeyboard` appearance. */ -@property (nonatomic, assign) UIKeyboardAppearance keyboardAppearance; - -/* The type of button used for the 'Done' button in the keyboard */ -@property(nonatomic, assign) UIReturnKeyType returnKeyType; - -/* The alpha value of the views in this view (For tranclucent styling) */ -@property (nonatomic, assign) CGFloat contentAlpha; - -/* Whether the view may be tapped to enable character input (Default is NO) */ -@property (nonatomic, assign) BOOL enabled; - -/** Called when the number of digits has been entered, or the user tapped 'Done' on the keyboard */ -@property (nonatomic, copy) void (^passcodeCompletedHandler)(NSString *code); - -/** Horizontal layout. The 'OK' button will be placed under the text field */ -@property (nonatomic, assign) BOOL horizontalLayout; - -/* Init with the target length needed for this passcode */ -- (instancetype)initWithStyle:(TOPasscodeInputFieldStyle)style; - -/* Replace the passcode with this one, and animate the transition. */ -- (void)setPasscode:(nullable NSString *)passcode animated:(BOOL)animated; - -/* Add additional characters to the end of the passcode, and animate if desired. */ -- (void)appendPasscodeCharacters:(NSString *)characters animated:(BOOL)animated; - -/* Delete a number of characters from the end, animated if desired. */ -- (void)deletePasscodeCharactersOfCount:(NSInteger)deleteCount animated:(BOOL)animated; - -/* Plays a shaking animation and resets the passcode back to empty */ -- (void)resetPasscodeAnimated:(BOOL)animated playImpact:(BOOL)impact; - -/* Animates the OK button changing location. */ -- (void)setHorizontalLayout:(BOOL)horizontalLayout animated:(BOOL)animated duration:(CGFloat)duration; - -@end - -NS_ASSUME_NONNULL_END diff --git a/iOSClient/Utility/TOPasscodeViewController/Views/Shared/TOPasscodeInputField.m b/iOSClient/Utility/TOPasscodeViewController/Views/Shared/TOPasscodeInputField.m deleted file mode 100644 index 0b3227c0ea..0000000000 --- a/iOSClient/Utility/TOPasscodeViewController/Views/Shared/TOPasscodeInputField.m +++ /dev/null @@ -1,423 +0,0 @@ -// -// TOPasscodeInputField.m -// -// Copyright 2017 Timothy Oliver. All rights reserved. -// -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files (the "Software"), to -// deal in the Software without restriction, including without limitation the -// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or -// sell copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: -// -// The above copyright notice and this permission notice shall be included in -// all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS -// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, -// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR -// IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - -#import "TOPasscodeInputField.h" - -#import "TOPasscodeVariableInputView.h" -#import "TOPasscodeFixedInputView.h" - -#import - -@interface TOPasscodeInputField () - -// Convenience getters -@property (nonatomic, readonly) UIView *inputField; // Returns whichever input field is currently visible -@property (nonatomic, readonly) NSInteger maximumPasscodeLength; // The mamximum number of characters allowed (0 if uncapped) - -@property (nonatomic, strong, readwrite) TOPasscodeFixedInputView *fixedInputView; -@property (nonatomic, strong, readwrite) TOPasscodeVariableInputView *variableInputView; -@property (nonatomic, strong, readwrite) UIButton *submitButton; -@property (nonatomic, strong, readwrite) UIVisualEffectView *visualEffectView; - -@end - -@implementation TOPasscodeInputField - -#pragma mark - View Set-up - - -- (instancetype)initWithFrame:(CGRect)frame -{ - if (self = [super initWithFrame:frame]) { - [self setUp]; - [self setUpForStyle:TOPasscodeInputFieldStyleFixed]; - } - - return self; -} - -- (instancetype)initWithStyle:(TOPasscodeInputFieldStyle)style -{ - if (self = [self initWithFrame:CGRectZero]) { - _style = style; - [self setUp]; - [self setUpForStyle:style]; - } - - return self; -} - -- (void)setUp -{ - self.backgroundColor = [UIColor clearColor]; - _submitButtonSpacing = 4.0f; - _submitButtonVerticalSpacing = 5.0f; - - _visualEffectView = [[UIVisualEffectView alloc] initWithEffect:nil]; - [self addSubview:_visualEffectView]; -} - -- (void)setUpForStyle:(TOPasscodeInputFieldStyle)style -{ - if (self.inputField) { - [self.inputField removeFromSuperview]; - self.variableInputView = nil; - self.fixedInputView = nil; - } - - if (style == TOPasscodeInputFieldStyleVariable) { - self.variableInputView = [[TOPasscodeVariableInputView alloc] init]; - [self.visualEffectView.contentView addSubview:self.variableInputView]; - } - else { - self.fixedInputView = [[TOPasscodeFixedInputView alloc] init]; - [self.visualEffectView.contentView addSubview:self.fixedInputView]; - } - - // Set the frame for the currently visible input view - [self.inputField sizeToFit]; - - // Size this view to match - [self sizeToFit]; -} - -#pragma mark - View Layout - -- (void)sizeToFit -{ - // Resize the view to encompass the current input view - CGRect frame = self.frame; - [self.inputField sizeToFit]; - frame.size = self.inputField.frame.size; - if (self.horizontalLayout) { - frame.size.height += self.submitButtonVerticalSpacing + CGRectGetHeight(self.submitButton.frame); - } - self.frame = CGRectIntegral(frame); -} - -- (void)layoutSubviews -{ - [super layoutSubviews]; - - self.visualEffectView.frame = self.inputField.bounds; - - if (!self.submitButton) { return; } - - [self.submitButton sizeToFit]; - [self bringSubviewToFront:self.submitButton]; - - CGRect frame = self.submitButton.frame; - if (!self.horizontalLayout) { - frame.origin.x = CGRectGetMaxX(self.bounds) + self.submitButtonSpacing; - frame.origin.y = (CGRectGetHeight(self.bounds) - CGRectGetHeight(frame)) * 0.5f; - } - else { - frame.origin.x = (CGRectGetWidth(self.frame) - frame.size.width) * 0.5f; - frame.origin.y = CGRectGetMaxY(self.inputField.frame) + self.submitButtonVerticalSpacing; - } - self.submitButton.frame = CGRectIntegral(frame); -} - -#pragma mark - Interaction - -- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event -{ - [super touchesBegan:touches withEvent:event]; - if (!self.enabled) { return; } - self.contentAlpha = 0.5f; -} - -- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event -{ - [super touchesCancelled:touches withEvent:event]; - if (!self.enabled) { return; } - [UIView animateWithDuration:0.3f animations:^{ - self.contentAlpha = 1.0f; - }]; - [self becomeFirstResponder]; -} - -- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event -{ - CGRect frame = self.bounds; - frame.size.width += self.submitButton.frame.size.width + (self.submitButtonSpacing * 2.0f); - frame.size.height += self.submitButtonVerticalSpacing; - - if (CGRectContainsPoint(frame, point)) { - return YES; - } - return NO; -} - -- (id)hitTest:(CGPoint)point withEvent:(UIEvent *)event { - - if ([[super hitTest:point withEvent:event] isEqual:self.submitButton]) { - if (CGRectContainsPoint(self.submitButton.frame, point)) { - return self.submitButton; - } else { - return self; - } - } - - return [super hitTest:point withEvent:event]; -} - -#pragma mark - Text Input Protocol - -- (BOOL)canBecomeFirstResponder { return self.enabled; } - -- (BOOL)hasText { return self.passcode.length > 0; } - -- (void)insertText:(NSString *)text -{ - if ([text isEqualToString:@"\n"]) { - if (self.passcodeCompletedHandler) { self.passcodeCompletedHandler(self.passcode); } - return; - } - - [self appendPasscodeCharacters:text animated:NO]; -} -- (void)deleteBackward -{ - [self deletePasscodeCharactersOfCount:1 animated:YES]; -} - -- (UIKeyboardType)keyboardType { return UIKeyboardTypeASCIICapable; } - -- (UITextAutocorrectionType)autocorrectionType { return UITextAutocorrectionTypeNo; } - -- (UIReturnKeyType)returnKeyType { return UIReturnKeyGo; } - -- (BOOL)enablesReturnKeyAutomatically { return YES; } - -#pragma mark - Text Input - -- (void)setPasscode:(NSString *)passcode animated:(BOOL)animated -{ - if (passcode == self.passcode) { return; } - _passcode = passcode; - - BOOL passcodeIsComplete = NO; - if (self.fixedInputView) { - [self.fixedInputView setHighlightedLength:_passcode.length animated:animated]; - passcodeIsComplete = _passcode.length >= self.maximumPasscodeLength; - } - else { - [self.variableInputView setLength:_passcode.length animated:animated]; - } - - if (self.submitButton) { - self.submitButton.hidden = (_passcode.length == 0); - [self bringSubviewToFront:self.submitButton]; - } - - if (passcodeIsComplete && self.passcodeCompletedHandler) { - self.passcodeCompletedHandler(_passcode); - } - - [self reloadInputViews]; -} - -- (void)appendPasscodeCharacters:(NSString *)characters animated:(BOOL)animated -{ - if (characters == nil) { return; } - if (self.maximumPasscodeLength > 0 && self.passcode.length >= self.maximumPasscodeLength) { return; } - - if (_passcode == nil) { _passcode = @""; } - [self setPasscode:[_passcode stringByAppendingString:characters] animated:animated]; -} - -- (void)deletePasscodeCharactersOfCount:(NSInteger)deleteCount animated:(BOOL)animated -{ - if (deleteCount <= 0 || self.passcode.length <= 0) { return; } - [self setPasscode:[self.passcode substringToIndex:(self.passcode.length - 1)] animated:animated]; -} - -- (void)resetPasscodeAnimated:(BOOL)animated playImpact:(BOOL)impact -{ - [self setPasscode:nil animated:animated]; - - // Play a negative impact effect - if (@available(iOS 9.0, *)) { - // https://stackoverflow.com/questions/41444274/how-to-check-if-haptic-engine-uifeedbackgenerator-is-supported - if (impact) { AudioServicesPlaySystemSoundWithCompletion(1521, nil); } - } - - if (!animated) { return; } - - CGPoint center = self.center; - CGPoint offset = center; - offset.x -= self.frame.size.width * 0.3f; - - // Play the view sliding out and then springing back in - id completionBlock = ^(BOOL finished) { - [UIView animateWithDuration:1.0f - delay:0.0f - usingSpringWithDamping:0.15f - initialSpringVelocity:10.0f - options:0 animations:^{ - self.center = center; - }completion:nil]; - }; - - [UIView animateWithDuration:0.05f animations:^{ - self.center = offset; - }completion:completionBlock]; - - if (!self.submitButton) { return; } - - [UIView animateWithDuration:0.7f animations:^{ - self.submitButton.alpha = 0.0f; - } completion:^(BOOL complete) { - self.submitButton.alpha = 1.0f; - self.submitButton.hidden = YES; - }]; -} - -#pragma mark - Button Callbacks - -- (void)submitButtonTapped:(id)sender -{ - if (self.passcodeCompletedHandler) { - self.passcodeCompletedHandler(self.passcode); - } -} - -#pragma mark - Private Accessors - -- (UIView *)inputField -{ - if (self.fixedInputView) { - return (UIView *)self.fixedInputView; - } - - return (UIView *)self.variableInputView; -} - -- (NSInteger)maximumPasscodeLength -{ - if (self.style == TOPasscodeInputFieldStyleFixed) { - return self.fixedInputView.length; - } - - return 0; -} - -#pragma mark - Public Accessors - - -- (void)setShowSubmitButton:(BOOL)showSubmitButton -{ - if (_showSubmitButton == showSubmitButton) { - return; - } - - _showSubmitButton = showSubmitButton; - - if (!_showSubmitButton) { - [self.submitButton removeFromSuperview]; - self.submitButton = nil; - return; - } - - self.submitButton = [UIButton buttonWithType:UIButtonTypeSystem]; - [self.submitButton setTitle:@"OK" forState:UIControlStateNormal]; - [self.submitButton addTarget:self action:@selector(submitButtonTapped:) forControlEvents:UIControlEventTouchUpInside]; - [self.submitButton.titleLabel setFont:[UIFont systemFontOfSize:18.0f]]; - self.submitButton.hidden = YES; - [self addSubview:self.submitButton]; - - [self setNeedsLayout]; -} - -- (void)setSubmitButtonSpacing:(CGFloat)submitButtonSpacing -{ - if (submitButtonSpacing == _submitButtonSpacing) { return; } - _submitButtonSpacing = submitButtonSpacing; - [self setNeedsLayout]; -} - -- (void)setSubmitButtonFontSize:(CGFloat)submitButtonFontSize -{ - if (submitButtonFontSize == _submitButtonFontSize) { return; } - _submitButtonFontSize = submitButtonFontSize; - self.submitButton.titleLabel.font = [UIFont systemFontOfSize:_submitButtonFontSize]; - [self.submitButton sizeToFit]; - [self setNeedsLayout]; -} - -- (void)setStyle:(TOPasscodeInputFieldStyle)style -{ - if (style == _style) { return; } - _style = style; - [self setUpForStyle:_style]; -} - -- (void)setPasscode:(NSString *)passcode -{ - [self setPasscode:passcode animated:NO]; -} - -- (void)setContentAlpha:(CGFloat)contentAlpha -{ - _contentAlpha = contentAlpha; - self.inputField.alpha = contentAlpha; - self.submitButton.alpha = contentAlpha; -} - -- (void)setHorizontalLayout:(BOOL)horizontalLayout -{ - [self setHorizontalLayout:horizontalLayout animated:NO duration:0.0f]; -} - -- (void)setHorizontalLayout:(BOOL)horizontalLayout animated:(BOOL)animated duration:(CGFloat)duration -{ - if (_horizontalLayout == horizontalLayout) { - return; - } - - UIView *snapshotView = nil; - - if (self.submitButton && self.submitButton.hidden == NO && animated) { - snapshotView = [self.submitButton snapshotViewAfterScreenUpdates:NO]; - snapshotView.frame = self.submitButton.frame; - [self addSubview:snapshotView]; - } - - _horizontalLayout = horizontalLayout; - - if (!animated || !self.submitButton) { - [self sizeToFit]; - [self setNeedsLayout]; - return; - } - - self.submitButton.alpha = 0.0f; - [self setNeedsLayout]; - [self layoutIfNeeded]; - - id animationBlock = ^{ - self.submitButton.alpha = 1.0f; - snapshotView.alpha = 0.0f; - }; - - id completionBlock = ^(BOOL complete) { - [snapshotView removeFromSuperview]; - [self bringSubviewToFront:self.submitButton]; - }; - - [UIView animateWithDuration:duration animations:animationBlock completion:completionBlock]; -} - -@end diff --git a/iOSClient/Utility/TOPasscodeViewController/Views/Shared/TOPasscodeVariableInputView.h b/iOSClient/Utility/TOPasscodeViewController/Views/Shared/TOPasscodeVariableInputView.h deleted file mode 100644 index 737f76761f..0000000000 --- a/iOSClient/Utility/TOPasscodeViewController/Views/Shared/TOPasscodeVariableInputView.h +++ /dev/null @@ -1,55 +0,0 @@ -// -// TOPasscodeVariableInputView.h -// -// Copyright 2017 Timothy Oliver. All rights reserved. -// -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files (the "Software"), to -// deal in the Software without restriction, including without limitation the -// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or -// sell copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: -// -// The above copyright notice and this permission notice shall be included in -// all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS -// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, -// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR -// IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - -#import - -/** - A basic content view showing a rounded rectangle containing circles - that can be used to represent a variable size passcode. - */ -@interface TOPasscodeVariableInputView : UIImageView - -/* The thickness of the stroke around the view (Default is 1.5) */ -@property (nonatomic, assign) CGFloat outlineThickness; - -/* The corner radius of the stroke (Default is 5) */ -@property (nonatomic, assign) CGFloat outlineCornerRadius; - -/* The size of each circle bullet point representing a passcoded character (Default is 10) */ -@property (nonatomic, assign) CGFloat circleDiameter; - -/* The spacing between each circle (Default is 15) */ -@property (nonatomic, assign) CGFloat circleSpacing; - -/* The padding between the circles and the outer outline (Default is {10,10}) */ -@property (nonatomic, assign) CGSize outlinePadding; - -/* The maximum number of circles to show (This will indicate the view's width) (Default is 12) */ -@property (nonatomic, assign) NSInteger maximumVisibleLength; - -/* Set the number of characters entered into this view (May be larger than `maximumVisibleLength`) */ -@property (nonatomic, assign) NSInteger length; - -/* Set the number of characters represented by this field, animated if desired */ -- (void)setLength:(NSInteger)length animated:(BOOL)animated; - -@end diff --git a/iOSClient/Utility/TOPasscodeViewController/Views/Shared/TOPasscodeVariableInputView.m b/iOSClient/Utility/TOPasscodeViewController/Views/Shared/TOPasscodeVariableInputView.m deleted file mode 100644 index 1dc0783b91..0000000000 --- a/iOSClient/Utility/TOPasscodeViewController/Views/Shared/TOPasscodeVariableInputView.m +++ /dev/null @@ -1,254 +0,0 @@ -// -// TOPasscodeVariableInputView.m -// -// Copyright 2017 Timothy Oliver. All rights reserved. -// -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files (the "Software"), to -// deal in the Software without restriction, including without limitation the -// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or -// sell copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: -// -// The above copyright notice and this permission notice shall be included in -// all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS -// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, -// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR -// IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - -#import "TOPasscodeVariableInputView.h" -#import "TOPasscodeCircleImage.h" - -@interface TOPasscodeVariableInputView () - -@property (nonatomic, strong) UIImage *backgroundImage; // The outline image for this view -@property (nonatomic, strong) UIImage *circleImage; // The circle image representing a single character - -@property (nonatomic, strong) NSMutableArray *circleViews; - -@end - -@implementation TOPasscodeVariableInputView - -#pragma mark - Class Creation - - -- (instancetype)initWithFrame:(CGRect)frame -{ - if (self = [super initWithFrame:frame]) { - _outlineThickness = 1.0f; - _outlineCornerRadius = 5.0f; - _circleDiameter = 11.0f; - _circleSpacing = 7.0f; - _outlinePadding = (CGSize){10,10}; - _maximumVisibleLength = 12; - } - - return self; -} - -#pragma mark - View Setup - -- (void)setUpImageForCircleViews -{ - if (self.circleImage != nil) { return; } - - self.circleImage = [TOPasscodeCircleImage circleImageOfSize:_circleDiameter inset:0.0f padding:1.0f antialias:YES]; - for (UIImageView *circleView in self.circleViews) { - circleView.image = self.circleImage; - [circleView sizeToFit]; - } -} - -- (void)setUpCircleViewsForLength:(NSInteger)length -{ - // Set up the number of circle views if needed - if (self.circleViews.count == length) { return; } - - if (self.circleViews == nil) { - self.circleViews = [NSMutableArray arrayWithCapacity:_maximumVisibleLength]; - } - - // Reduce the number of views - while (self.circleViews.count > length) { - UIImageView *circleView = self.circleViews.lastObject; - [circleView removeFromSuperview]; - [self.circleViews removeLastObject]; - } - - // Increase the number of views - [UIView performWithoutAnimation:^{ - while (self.circleViews.count < length) { - UIImageView *circleView = [[UIImageView alloc] initWithImage:self.circleImage]; - circleView.alpha = 0.0f; - [self addSubview:circleView]; - [self.circleViews addObject:circleView]; - } - }]; -} - -- (void)setUpBackgroundImage -{ - if (self.backgroundImage != nil) { return; } - - self.backgroundImage = [[self class] backgroundImageWithThickness:_outlineThickness cornerRadius:_outlineCornerRadius]; - self.image = self.backgroundImage; -} - -#pragma mark - View Layout - - -- (void)sizeToFit -{ - CGRect frame = self.frame; - - // Calculate the width - frame.size.width = self.outlineThickness * 2.0f; - frame.size.width += (self.outlinePadding.width * 2.0f); - frame.size.width += (self.maximumVisibleLength * (self.circleDiameter+2.0f)); // +2 for padding - frame.size.width += ((self.maximumVisibleLength - 1) * self.circleSpacing); - - // Height - frame.size.height = self.outlineThickness * 2.0f; - frame.size.height += self.outlinePadding.height * 2.0f; - frame.size.height += self.circleDiameter; - - self.frame = CGRectIntegral(frame); -} - -- (void)layoutSubviews -{ - [super layoutSubviews]; - - // Genearate the background image if we don't have one yet - [self setUpBackgroundImage]; - - // Set up the circle view image - [self setUpImageForCircleViews]; - - // Set up the circle views - [self setUpCircleViewsForLength:self.maximumVisibleLength]; - - // Layout the circle views for the current length - CGRect frame = CGRectZero; - frame.size = self.circleImage.size; - frame.origin.y = CGRectGetMidY(self.bounds) - (frame.size.height * 0.5f); - frame.origin.x = self.outlinePadding.width + self.outlineThickness; - - for (UIImageView *circleView in self.circleViews) { - circleView.frame = frame; - frame.origin.x += frame.size.width + self.circleSpacing; - } -} - -#pragma mark - Accessors - - -- (void)setOutlineThickness:(CGFloat)outlineThickness -{ - if (_outlineThickness == outlineThickness) { return; } - _outlineThickness = outlineThickness; - self.backgroundImage = nil; - [self setNeedsLayout]; -} - -- (void)setOutlineCornerRadius:(CGFloat)outlineCornerRadius -{ - if (_outlineCornerRadius == outlineCornerRadius) { return; } - _outlineCornerRadius = outlineCornerRadius; - self.backgroundImage = nil; - [self setNeedsLayout]; -} - -- (void)setCircleDiameter:(CGFloat)circleDiameter -{ - if (_circleDiameter == circleDiameter) { return; } - _circleDiameter = circleDiameter; - self.circleImage = nil; - [self setUpImageForCircleViews]; -} - -- (void)setCircleSpacing:(CGFloat)circleSpacing -{ - if (_circleSpacing == circleSpacing) { return; } - _circleSpacing = circleSpacing; - [self sizeToFit]; - [self setNeedsLayout]; -} - -- (void)setOutlinePadding:(CGSize)outlinePadding -{ - if (CGSizeEqualToSize(outlinePadding, _outlinePadding)) { return; } - _outlinePadding = outlinePadding; - [self sizeToFit]; - [self setNeedsLayout]; -} - -- (void)setMaximumVisibleLength:(NSInteger)maximumVisibleLength -{ - if (_maximumVisibleLength == maximumVisibleLength) { return; } - _maximumVisibleLength = maximumVisibleLength; - [self setUpCircleViewsForLength:maximumVisibleLength]; - [self sizeToFit]; - [self setNeedsLayout]; -} - -- (void)setLength:(NSInteger)length -{ - [self setLength:length animated:NO]; -} - -- (void)setLength:(NSInteger)length animated:(BOOL)animated -{ - if (length == _length) { return; } - - _length = length; - - void (^animationBlock)(void) = ^{ - NSInteger i = 0; - for (UIImageView *circleView in self.circleViews) { - circleView.alpha = i < length ? 1.0f : 0.0f; - i++; - } - }; - - if (!animated) { - animationBlock(); - return; - } - - [UIView animateWithDuration:0.4f animations:animationBlock]; -} - -#pragma mark - Image Creation - - -+ (UIImage *)backgroundImageWithThickness:(CGFloat)thickness cornerRadius:(CGFloat)radius -{ - CGFloat inset = thickness / 2.0f; - CGFloat dimension = (radius * 2.0f) + 2.0f; - - CGRect frame = CGRectZero; - frame.origin = CGPointMake(inset, inset); - frame.size = CGSizeMake(dimension, dimension); - - CGSize canvasSize = frame.size; - canvasSize.width += thickness; - canvasSize.height += thickness; - - UIGraphicsBeginImageContextWithOptions(canvasSize, NO, 0.0f); - { - UIBezierPath *path = [UIBezierPath bezierPathWithRoundedRect:frame cornerRadius:radius]; - path.lineWidth = thickness; - [[UIColor blackColor] setStroke]; - [path stroke]; - } - - UIImage *image = UIGraphicsGetImageFromCurrentImageContext(); - UIGraphicsEndImageContext(); - - UIEdgeInsets insets = UIEdgeInsetsMake(radius+1, radius+1, radius+1, radius+1); - image = [image resizableImageWithCapInsets:insets]; - return [image imageWithRenderingMode:UIImageRenderingModeAlwaysTemplate]; -} - -@end From 41feee0632105c28112a3da03724dc3f38a0a7ca Mon Sep 17 00:00:00 2001 From: Marino Faggiana Date: Fri, 23 Oct 2026 10:55:58 +0200 Subject: [PATCH 06/14] Acknowledgements update Signed-off-by: Marino Faggiana --- iOSClient/Settings/Acknowledgements.rtf | 455 ++++++++++++++++++------ 1 file changed, 344 insertions(+), 111 deletions(-) diff --git a/iOSClient/Settings/Acknowledgements.rtf b/iOSClient/Settings/Acknowledgements.rtf index 4a7b3713d8..9ca49c01cb 100644 --- a/iOSClient/Settings/Acknowledgements.rtf +++ b/iOSClient/Settings/Acknowledgements.rtf @@ -1,238 +1,471 @@ -{\rtf1\ansi\ansicpg1252\cocoartf2868 -\cocoatextscaling0\cocoaplatform0{\fonttbl\f0\fswiss\fcharset0 Helvetica;\f1\fswiss\fcharset0 Helvetica-Bold;} -{\colortbl;\red255\green255\blue255;} -{\*\expandedcolortbl;;} -\paperw12240\paperh15840\margl1440\margr1440\vieww25280\viewh28800\viewkind0 -\pard\tx566\tx1133\tx1700\tx2267\tx2834\tx3401\tx3968\tx4535\tx5102\tx5669\tx6236\tx6803\pardirnatural\partightenfactor0 - -\f0\fs24 \cf0 \ -This software contains additional third party software. \ -All the third party software included or linked is redistributed under the terms and conditions of their original licenses. \ +{\rtf1\ansi\ansicpg1252\uc1 +{\fonttbl\f0\fswiss Helvetica;\f1\fswiss Helvetica-Bold;} +\paperw12240\paperh15840\margl1440\margr1440 +\pard\f0\fs24 +This software contains additional third-party software, including direct and transitive Swift Package dependencies.\ +Each project is distributed under its original license. Links below identify the license files for the resolved package revisions.\ __________________________________\ \ -\f1\b Nextcloud Kit\ +\f1\b Abseil\f0\b0\ +\ +https://github.com/google/abseil-cpp-binary\ +\ +Apache License 2.0\ +\ +License: https://github.com/google/abseil-cpp-binary/blob/26509120768c4add61cc1a63059f15f4fb49edb4/LICENSE\ +__________________________________\ +\ -\f0\b0 \ -https://github.com/nextcloud/NextcloudKit\ +\f1\b Alamofire\f0\b0\ +\ +https://github.com/Alamofire/Alamofire\ \ -GPLv3 Licensed \ +MIT License\ +\ +Copyright (c) 2014-2022 Alamofire Software Foundation (http://alamofire.org/)\ \ -Copyright (c) Marino Faggiana\ +License: https://github.com/Alamofire/Alamofire/blob/bda9ed57d72988a3a2ada33d824583541f86eac6/LICENSE\ +__________________________________\ \ -\f1\b Alamofire - -\f0\b0 MIT License (MIT) -\f1\b \ -SwiftyXMLParser - -\f0\b0 MIT License (MIT) -\f1\b \ -SwiftyJSON - -\f0\b0 MIT License (MIT) \ +\f1\b App Check\f0\b0\ +\ +https://github.com/google/app-check\ +\ +Apache License 2.0\ +\ +License: https://github.com/google/app-check/blob/c7eb17e5545f36e43dc50e70da09c2e999714e16/LICENSE\ __________________________________\ \ -\f1\b NYMnemonic - BIP 39 style mnemonic codes\ +\f1\b CodeScanner\f0\b0\ +\ +https://github.com/twostraws/CodeScanner\ +\ +MIT License\ +\ +Copyright (c) 2021 Paul Hudson\ +\ +License: https://github.com/twostraws/CodeScanner/blob/5e886430238944c7200fc9e10dbf2d9550dba865/LICENSE\ +__________________________________\ +\ -\f0\b0 __________________________________\ +\f1\b DropDown\f0\b0\ +\ +https://github.com/AssistoLab/DropDown\ +\ +MIT License\ +\ +Copyright (c) 2014 kevin-hirsch\ +\ +License: https://github.com/AssistoLab/DropDown/blob/2ab6f6ce19f0117d1a76ea043ef8f57722c65d16/LICENSE\ +__________________________________\ \ -\pard\pardeftab720\partightenfactor0 -\f1\b \cf0 LucidBanner\ +\f1\b EasyTipView\f0\b0\ +\ +https://github.com/marinofaggiana/EasyTipView\ \ -h -\f0\b0 ttps://github.com/marinofaggiana/LucidBanner -\f1\b \ +MIT License\ +\ +Copyright (c) 2015 Teodor Patra\u351?\ +\ +License: https://github.com/marinofaggiana/EasyTipView/blob/bb25a13c8fa5d65144c8e5b5d31237b80a001b07/LICENSE\ +__________________________________\ \ -\f0\b0 MIT License\ +\f1\b Firebase (Crashlytics and Database)\f0\b0\ +\ +https://github.com/firebase/firebase-ios-sdk\ +\ +Apache License 2.0\ +\ +License: https://github.com/firebase/firebase-ios-sdk/blob/1c35b9453868cea1ecdf9f112b5b23b8b6343854/LICENSE\ +__________________________________\ \ -\pard\tx566\tx1133\tx1700\tx2267\tx2834\tx3401\tx3968\tx4535\tx5102\tx5669\tx6236\tx6803\pardirnatural\partightenfactor0 -\cf0 Copyright (c) Marino Faggiana -\f1\b \ -\pard\tx566\tx1133\tx1700\tx2267\tx2834\tx3401\tx3968\tx4535\tx5102\tx5669\tx6236\tx6803\pardirnatural\partightenfactor0 -\f0\b0 \cf0 __________________________________\ +\f1\b Google Ads On Device Conversion\f0\b0\ +\ +https://github.com/googleads/google-ads-on-device-conversion-ios-sdk\ +\ +Apache License 2.0\ +\ +License: https://github.com/googleads/google-ads-on-device-conversion-ios-sdk/blob/0208e2681ec81f8a9c81084696f60fcce26cb7ee/LICENSE\ +__________________________________\ \ -\pard\pardeftab720\partightenfactor0 -\f1\b \cf0 NextcloudKit\ +\f1\b GoogleAppMeasurement\f0\b0\ +\ +https://github.com/google/GoogleAppMeasurement\ \ -h -\f0\b0 ttps://github.com/nextcloud/NextcloudKit -\f1\b \ +Apache License 2.0\ +\ +License: https://github.com/google/GoogleAppMeasurement/blob/1a3546fb9189292e9d64db9e72add7a286c0f85a/LICENSE\ +__________________________________\ \ -\f0\b0 GPLv3 License -\f1\b \ -\pard\tx566\tx1133\tx1700\tx2267\tx2834\tx3401\tx3968\tx4535\tx5102\tx5669\tx6236\tx6803\pardeftab720\pardirnatural\partightenfactor0 +\f1\b GoogleDataTransport\f0\b0\ +\ +https://github.com/google/GoogleDataTransport\ +\ +Apache License 2.0\ +\ +License: https://github.com/google/GoogleDataTransport/blob/ba3358d3c3dbae8ef230b58a46b97ad65e84e974/LICENSE\ +__________________________________\ +\ -\f0\b0 \cf0 __________________________________\ -\pard\tx566\tx1133\tx1700\tx2267\tx2834\tx3401\tx3968\tx4535\tx5102\tx5669\tx6236\tx6803\pardirnatural\partightenfactor0 +\f1\b GoogleUtilities\f0\b0\ +\ +https://github.com/google/GoogleUtilities\ +\ +Apache License 2.0\ +\ +License: https://github.com/google/GoogleUtilities/blob/cc31f5ed464b111f877ce8554cbef4455d410d91/LICENSE\ +__________________________________\ +\ -\f1\b \cf0 \ -OpenSSL\ +\f1\b GRDB.swift\f0\b0\ +\ +https://github.com/groue/GRDB.swift\ +\ +MIT License\ +\ +Copyright (C) 2015-2025 Gwendal Rou\u233?\ +\ +License: https://github.com/groue/GRDB.swift/blob/b83108d10f42680d78f23fe4d4d80fc88dab3212/LICENSE\ +__________________________________\ \ -\f0\b0 https://www.openssl.org/source/license.html\ +\f1\b gRPC\f0\b0\ +\ +https://github.com/google/grpc-binary\ +\ +Apache License 2.0\ +\ +License: https://github.com/google/grpc-binary/blob/9d5e9f05670c9eab8d8c22731ddd5d4d9cf867a8/LICENSE\ +__________________________________\ +\ -\f1\b \ +\f1\b GTM Session Fetcher\f0\b0\ +\ +https://github.com/google/gtm-session-fetcher\ +\ +Apache License 2.0\ +\ +License: https://github.com/google/gtm-session-fetcher/blob/724a52eea6329b7e12d3ad8300d76ca9f3895fcc/LICENSE\ +__________________________________\ +\ -\f0\b0 Copyright (C) 1995-1998 Eric Young (eay@cryptsoft.com)\ +\f1\b GzipSwift\f0\b0\ +\ +https://github.com/1024jp/GzipSwift\ +\ +MIT License\ +\ +\u169? 2014-2026 1024jp\ +\ +License: https://github.com/1024jp/GzipSwift/blob/41d37421fe2cbbfae4a5593dc29d12999fb73cd7/LICENSE\ __________________________________\ \ -\f1\b Realm -\f0\b0 \ +\f1\b Interop for Google SDKs\f0\b0\ +\ +https://github.com/google/interop-ios-for-google-sdks\ \ Apache License 2.0\ \ -Copyright (c) 2014-2017 https://realm.io\ +License: https://github.com/google/interop-ios-for-google-sdks/blob/040d087ac2267d2ddd4cca36c757d1c6a05fdbfe/LICENSE\ __________________________________\ \ -\f1\b KeychainAccess -\f0\b0 \ +\f1\b KeychainAccess\f0\b0\ +\ +https://github.com/kishikawakatsumi/KeychainAccess\ \ MIT License\ \ -Copyright (c) kishikawa katsumi\ +Copyright (c) 2014 kishikawa katsumi\ +\ +License: https://github.com/kishikawakatsumi/KeychainAccess/blob/84e546727d66f1adc5439debad16270d0fdd04e7/LICENSE\ __________________________________\ \ -\f1\b VLCKit -\f0\b0 \ +\f1\b LevelDB\f0\b0\ \ -GNU Lesser General Public License 2.1\ +https://github.com/firebase/leveldb\ \ -Copyright (c) VideoLAN\ +BSD 3-Clause License\ +\ +Copyright (c) 2011 The LevelDB Authors. All rights reserved.\ +\ +License: https://github.com/firebase/leveldb/blob/a0bc79961d7be727d258d33d5a6b2f1023270ba1/LICENSE\ __________________________________\ \ -\f1\b SwiftRichString -\f0\b0 \ +\f1\b LucidBanner\f0\b0\ +\ +https://github.com/marinofaggiana/LucidBanner\ \ MIT License\ \ -Copyright (c) Daniele Margutti\ +Copyright (c) 2025 Marino Faggiana\ +\ +License: https://github.com/marinofaggiana/LucidBanner/blob/408af8b2ff2451f055ff17042d8b9fb5c822d5fd/LICENSE\ __________________________________\ \ -\f1\b QRCodeReader -\f0\b0 \ +\f1\b Mantis\f0\b0\ +\ +https://github.com/marinofaggiana/Mantis\ \ MIT License\ \ -Copyright (c) Yannick Loriot\ +Copyright (c) 2018 Yingtao Guo\ +\ +License: https://github.com/marinofaggiana/Mantis/blob/d145ce2403e0d94d4926eb8510bc4966fbf1bfca/LICENSE\ __________________________________\ \ -\f1\b Parchment -\f0\b0 \ +\f1\b MarkdownKit\f0\b0\ +\ +https://github.com/bmoliveira/MarkdownKit\ \ MIT License\ \ -Copyright (c) Martin Rechsteiner\ +Copyright (c) 2016 Ivan Bruel\ +\ +License: https://github.com/bmoliveira/MarkdownKit/blob/39ea7a3fc7e65bcafe55152592b834859be574a5/LICENSE\ __________________________________\ \ -\f1\b DropDown -\f0\b0 \ +\f1\b nanopb\f0\b0\ \ -MIT License\ +https://github.com/firebase/nanopb\ +\ +zlib License\ +\ +Copyright (c) 2011 Petteri Aimonen \ \ -Copyright (c) Kevin Hirsch\ +License: https://github.com/firebase/nanopb/blob/3851d94a41890dea16dc3db34caf60e585cb4163/LICENSE.txt\ __________________________________\ \ -\f1\b TLPhotoPicker -\f0\b0 \ +\f1\b NextcloudKit\f0\b0\ \ -MIT License\ +https://github.com/nextcloud/NextcloudKit\ +\ +GNU General Public License v3.0 or later, with Apple App Store exception (COPYING.iOS)\ \ -Copyright (c) wade.hawk, junhyi.park@gmail.com\ +Copyright Nextcloud GmbH, Marino Faggiana and Nextcloud contributors\ +\ +License: https://github.com/nextcloud/NextcloudKit/blob/705e69cf0526cac0bdeaf21699c41c895cae42a0/LICENSE.txt\ __________________________________\ \ -\f1\b MarkdownKit -\f0\b0 \ +\f1\b NYMnemonic - BIP 39 style mnemonic codes\f0\b0\ \ MIT License\ +\ +Copyright (c) 2014 Nybex, Inc. (https://nybex.com)\ __________________________________\ \ -\f1\b Firebase Crashlytics -\f0\b0 \ +\f1\b OpenSSL\f0\b0\ +\ +https://github.com/krzyzanowskim/OpenSSL\ \ Apache License 2.0\ \ -Copyright (c) Google Inc.\ +License: https://github.com/krzyzanowskim/OpenSSL/blob/0a6149bff4fb00921ce0ec1811f726b166638095/LICENSE.txt\ __________________________________\ \ -\f1\b Queuer -\f0\b0 \ +\f1\b PopupView\f0\b0\ +\ +https://github.com/exyte/PopupView\ \ MIT License\ \ -Copyright (c) Fabrizio Brancati\ +Copyright (c) 2019 exyte \ +\ +License: https://github.com/exyte/PopupView/blob/99695337ce9e965b70e4aa14b49b1a9920017333/LICENSE\ +__________________________________\ +\ + +\f1\b Promises\f0\b0\ +\ +https://github.com/google/promises\ +\ +Apache License 2.0\ +\ +License: https://github.com/google/promises/blob/f4a19a3c313dc2616c70bb49d29a799fb16be837/LICENSE\ __________________________________\ \ -\f1\b EasyTipView -\f0\b0 \ +\f1\b QRCodeReader.swift\f0\b0\ +\ +https://github.com/yannickl/QRCodeReader.swift\ \ MIT License\ \ -Copyright (c) Teodor Patras\ +Copyright (c) 2014-present Yannick Loriot\ +\ +License: https://github.com/yannickl/QRCodeReader.swift/blob/5020b5a47199d8ba80c83a4b4fafd70e9dc9dc7f/LICENSE\ __________________________________\ \ -\f1\b Swifter -\f0\b0 \ +\f1\b Realm Core\f0\b0\ +\ +https://github.com/realm/realm-core\ \ -BSD 3\ +Apache License 2.0\ \ -Copyright (c) Damian Kolakowski\ +License: https://github.com/realm/realm-core/blob/b4192c46305570577c4d08df790fddec5ab3aa04/LICENSE\ __________________________________\ \ -\f1\b Mantis -\f0\b0 \ +\f1\b Realm Swift\f0\b0\ \ -MIT License\ +https://github.com/realm/realm-swift\ +\ +Apache License 2.0\ +\ +License: https://github.com/realm/realm-swift/blob/a40ced79265b644a896e393b0f3a37e7916e7de3/LICENSE\ +__________________________________\ +\ + +\f1\b Swift Atomics\f0\b0\ +\ +https://github.com/apple/swift-atomics\ +\ +Apache License 2.0 with Swift Runtime Library Exception\ +\ +License: https://github.com/apple/swift-atomics/blob/0442cb5a3f98ab802acb777929fdb446bda11a34/LICENSE.txt\ +__________________________________\ +\ + +\f1\b Swift Collections\f0\b0\ +\ +https://github.com/apple/swift-collections\ \ -Copyright (c) Yingtao Guo\ +Apache License 2.0 with Swift Runtime Library Exception\ +\ +License: https://github.com/apple/swift-collections/blob/35a1edcb8ebd2bfe251d587f0b69e62af28f508c/LICENSE.txt\ +__________________________________\ +\ + +\f1\b Swift System\f0\b0\ +\ +https://github.com/apple/swift-system\ +\ +Apache License 2.0 with Swift Runtime Library Exception\ +\ +License: https://github.com/apple/swift-system/blob/869129b7bf4ecc57b97d0193ad29690ca2134750/LICENSE.txt\ __________________________________\ \ -\f1\b TagListView -\f0\b0 \ +\f1\b Swifter\f0\b0\ +\ +https://github.com/httpswift/swifter\ +\ +BSD 3-Clause License\ +\ +Copyright (c) 2014, Damian Ko\u322?akowski\ +\ +License: https://github.com/httpswift/swifter/blob/1e4f51c92d7ca486242d8bf0722b99de2c3531aa/LICENSE\ +__________________________________\ +\ + +\f1\b SwiftNIO\f0\b0\ +\ +https://github.com/apple/swift-nio\ +\ +Apache License 2.0\ +\ +License: https://github.com/apple/swift-nio/blob/6db9d9abd92ad7399b8a564cafbb87d3507245fa/LICENSE.txt\ +__________________________________\ +\ + +\f1\b SwiftNIO SSL\f0\b0\ +\ +https://github.com/apple/swift-nio-ssl\ +\ +Apache License 2.0\ +\ +License: https://github.com/apple/swift-nio-ssl/blob/03827c1a9fdb2b6b00a4e93ede8861520263af8c/LICENSE.txt\ +__________________________________\ +\ + +\f1\b SwiftRichString\f0\b0\ +\ +https://github.com/malcommac/SwiftRichString\ \ MIT License\ \ -Copyright (c) LIU Dongyuan\ +Copyright (c) 2018 Daniele Margutti\ +\ +License: https://github.com/malcommac/SwiftRichString/blob/9bf4b5af6bb4386865636fc504d6c588c2b65040/LICENSE\ __________________________________\ \ -\pard\pardeftab720\partightenfactor0 -\f1\b \cf0 GzipSwift\ +\f1\b SwiftyJSON\f0\b0\ +\ +https://github.com/SwiftyJSON/SwiftyJSON\ +\ +MIT License\ +\ +Copyright (c) 2017 Ruoyu Fu\ +\ +License: https://github.com/SwiftyJSON/SwiftyJSON/blob/af76cf3ef710b6ca5f8c05f3a31307d44a3c5828/LICENSE\ +__________________________________\ +\ -\f0\b0 \ +\f1\b SwiftyXMLParser\f0\b0\ +\ +https://github.com/yahoojapan/SwiftyXMLParser\ +\ MIT License\ \ -Copyright (c) 1024jp -\f1\b \ -\pard\tx566\tx1133\tx1700\tx2267\tx2834\tx3401\tx3968\tx4535\tx5102\tx5669\tx6236\tx6803\pardirnatural\partightenfactor0 +Copyright (c) 2016 Yahoo Japan Corporation\ +\ +License: https://github.com/yahoojapan/SwiftyXMLParser/blob/d7a1d23f04c86c1cd2e8f19247dd15d74e0ea8be/LICENSE\ +__________________________________\ +\ -\f0\b0 \cf0 __________________________________\ +\f1\b TagListView\f0\b0\ +\ +https://github.com/ElaWorkshop/TagListView\ +\ +MIT License\ +\ +Copyright (c) 2015 LIU Dongyuan\ +\ +License: https://github.com/ElaWorkshop/TagListView/blob/b30b61826736f4d9ec04aef5df891f3914423f16/LICENSE\ +__________________________________\ \ -\f1\b LRUCache -\f0\b0 \ +\f1\b TLPhotoPicker\f0\b0\ +\ +https://github.com/tilltue/TLPhotoPicker\ \ MIT License\ \ -Copyright (c) Nick Lockwood\ +Copyright (c) 2017 wade.hawk \ +\ +License: https://github.com/tilltue/TLPhotoPicker/blob/91d7bca65721928af30c6a2bb1a1f517f7862964/LICENSE\ __________________________________\ -} \ No newline at end of file +\ + +\f1\b VLCKit (MobileVLCKit)\f0\b0\ +\ +https://code.videolan.org/videolan/VLCKit\ +\ +GNU Lesser General Public License 2.1\ +\ +Copyright VideoLAN\ +\ +License: https://code.videolan.org/videolan/VLCKit/-/blob/master/COPYING\ +__________________________________\ +\ +} From 9429331a14cf5f0ca87e28f4994e9f1de2173db0 Mon Sep 17 00:00:00 2001 From: Marino Faggiana Date: Fri, 23 Oct 2026 11:01:39 +0200 Subject: [PATCH 07/14] fix Signed-off-by: Marino Faggiana --- Nextcloud.xcodeproj/project.pbxproj | 53 +++++++++++++------ .../xcshareddata/swiftpm/Package.resolved | 11 +++- 2 files changed, 47 insertions(+), 17 deletions(-) diff --git a/Nextcloud.xcodeproj/project.pbxproj b/Nextcloud.xcodeproj/project.pbxproj index 760299cde2..6e16370d47 100644 --- a/Nextcloud.xcodeproj/project.pbxproj +++ b/Nextcloud.xcodeproj/project.pbxproj @@ -14,6 +14,10 @@ 2C33C48623E2C475005F963B /* Notification Service Extension.appex in Embed Foundation Extensions */ = {isa = PBXBuildFile; fileRef = 2C33C47F23E2C475005F963B /* Notification Service Extension.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; 370D26AF248A3D7A00121797 /* NCCellMain.swift in Sources */ = {isa = PBXBuildFile; fileRef = 370D26AE248A3D7A00121797 /* NCCellMain.swift */; }; 63563FE53573412998852DD3 /* NCAlbumsPersistenceTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 439616AAA50F4360A761E5EF /* NCAlbumsPersistenceTests.swift */; }; + 7670DABED44E4D56AD99E6DC /* NCPasscodeView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F902B24E44354B6D9F329F12 /* NCPasscodeView.swift */; }; + 87768C6AB8E040219DF85898 /* NCPasscodeModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 131F8E2F43AD414E9DB53019 /* NCPasscodeModel.swift */; }; + 9310075861174A62B1C9FED6 /* NCPasscodeView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F902B24E44354B6D9F329F12 /* NCPasscodeView.swift */; }; + 9F0E3B428AB44A6480190C2D /* NCPasscodeModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 131F8E2F43AD414E9DB53019 /* NCPasscodeModel.swift */; }; A12720261001000000000001 /* NCBackgroundUploadExtensionManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = F793E7C230417C7100F011FE /* NCBackgroundUploadExtensionManager.swift */; }; A1E2EE010000000000000002 /* NCEndToEndKeySet.swift in Sources */ = {isa = PBXBuildFile; fileRef = A1E2EE010000000000000001 /* NCEndToEndKeySet.swift */; }; A1E2EE010000000000000003 /* NCEndToEndKeySet.swift in Sources */ = {isa = PBXBuildFile; fileRef = A1E2EE010000000000000001 /* NCEndToEndKeySet.swift */; }; @@ -118,6 +122,7 @@ C0DECA022F65000100C0D001 /* NCCameraRollTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0DECA012F65000100C0D001 /* NCCameraRollTests.swift */; }; C0DEDA7E30C1000100A00002 /* NCAutoUploadDateTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0DEDA7E30C1000100A00001 /* NCAutoUploadDateTests.swift */; }; CB3666201AF7550816B5CD6A /* NCContextMenuComment.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8932E90EC4278026D86CCCC9 /* NCContextMenuComment.swift */; }; + D56DFFE5D5014F66A7C9FED0 /* PasscodeUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = BC6C501EF1104A6A8569D9C1 /* PasscodeUITests.swift */; }; D5B6AA7827200C7200D49C24 /* NCActivityTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5B6AA7727200C7200D49C24 /* NCActivityTableViewCell.swift */; }; F0A1B2C430B5000100D4E5F6 /* NCMediaViewerFloatingTitleViewTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F0A1B2C330B5000100D4E5F6 /* NCMediaViewerFloatingTitleViewTests.swift */; }; F0A1B2C630B6000100D4E5F6 /* NCImageZoomViewTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F0A1B2C530B6000100D4E5F6 /* NCImageZoomViewTests.swift */; }; @@ -978,6 +983,8 @@ F7D496FC2EBFA541004F9823 /* NCRecommendationsCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = F75D90202D2BE26C003E740B /* NCRecommendationsCell.swift */; }; F7D496FD2EBFA6D9004F9823 /* String+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = F7A0D1342591FBC5008F8A13 /* String+Extension.swift */; }; F7D497082EBFAFD6004F9823 /* NCImageCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = F76B649B2ADFFAED00014640 /* NCImageCache.swift */; }; + F7D4BF012CA1831900A5E746 /* NCCollectionViewCommonPinchGesture.swift in Sources */ = {isa = PBXBuildFile; fileRef = F7D4BF002CA1831600A5E746 /* NCCollectionViewCommonPinchGesture.swift */; }; + F7D4BF542CA2ED9D00A5E746 /* VLCKitSPM in Frameworks */ = {isa = PBXBuildFile; productRef = F7D4BF532CA2ED9D00A5E746 /* VLCKitSPM */; }; F7D56B1A2972405500FA46C4 /* Mantis in Frameworks */ = {isa = PBXBuildFile; productRef = F7D56B192972405500FA46C4 /* Mantis */; }; F7D60CAF2C941ACB008FBFDD /* NCMediaPinchGesture.swift in Sources */ = {isa = PBXBuildFile; fileRef = F7D60CAE2C941ACB008FBFDD /* NCMediaPinchGesture.swift */; }; F7D61E932EBF1366007F865B /* UIColor+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = F70CEF5523E9C7E50007035B /* UIColor+Extension.swift */; }; @@ -1075,11 +1082,6 @@ F7FF2CB12842159500EBB7A1 /* NCSectionHeader.xib in Resources */ = {isa = PBXBuildFile; fileRef = F7FF2CB02842159500EBB7A1 /* NCSectionHeader.xib */; }; F7FFFCA02FB300440015441E /* NCAssistantSharedTextStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = F7FFFC9D2FB300440015441E /* NCAssistantSharedTextStore.swift */; }; F7FFFCA22FB300600015441E /* NCAssistantSharedTextStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = F7FFFC9D2FB300440015441E /* NCAssistantSharedTextStore.swift */; }; - 9F0E3B428AB44A6480190C2D /* NCPasscodeModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 131F8E2F43AD414E9DB53019 /* NCPasscodeModel.swift */; }; - 87768C6AB8E040219DF85898 /* NCPasscodeModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 131F8E2F43AD414E9DB53019 /* NCPasscodeModel.swift */; }; - 7670DABED44E4D56AD99E6DC /* NCPasscodeView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F902B24E44354B6D9F329F12 /* NCPasscodeView.swift */; }; - 9310075861174A62B1C9FED6 /* NCPasscodeView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F902B24E44354B6D9F329F12 /* NCPasscodeView.swift */; }; - D56DFFE5D5014F66A7C9FED0 /* PasscodeUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = BC6C501EF1104A6A8569D9C1 /* PasscodeUITests.swift */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -1297,6 +1299,7 @@ /* End PBXCopyFilesBuildPhase section */ /* Begin PBXFileReference section */ + 131F8E2F43AD414E9DB53019 /* NCPasscodeModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NCPasscodeModel.swift; sourceTree = ""; }; 2C33C47F23E2C475005F963B /* Notification Service Extension.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = "Notification Service Extension.appex"; sourceTree = BUILT_PRODUCTS_DIR; }; 2C33C48123E2C475005F963B /* NotificationService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationService.swift; sourceTree = ""; }; 2C33C48A23E2CC26005F963B /* Notification_Service_Extension-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Notification_Service_Extension-Bridging-Header.h"; sourceTree = ""; }; @@ -1440,6 +1443,7 @@ AFCE353827E5DE0400FEA6C2 /* Shareable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Shareable.swift; sourceTree = ""; }; B601AA002F10000100C0D3E5 /* NCLogFilesView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NCLogFilesView.swift; sourceTree = ""; }; BB7697C94BA14450A0867940 /* NCContextMenuProfile.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NCContextMenuProfile.swift; sourceTree = ""; }; + BC6C501EF1104A6A8569D9C1 /* PasscodeUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PasscodeUITests.swift; sourceTree = ""; }; C0046CDA2A17B98400D87C9D /* NextcloudUITests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = NextcloudUITests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; C04E2F202A17BB4D001BAD85 /* NextcloudIntegrationTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = NextcloudIntegrationTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; C0DEA070306D000100000004 /* NCLocalDatabase+AutoUpload.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NCLocalDatabase+AutoUpload.swift"; sourceTree = ""; }; @@ -2038,6 +2042,7 @@ F7CF067A2E0FF38F0063AD04 /* NCAppStateManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NCAppStateManager.swift; sourceTree = ""; }; F7CF06822E11273F0063AD04 /* NCManageDatabase+CreateMetadata.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NCManageDatabase+CreateMetadata.swift"; sourceTree = ""; }; F7D1611F23CF19E30039EBBF /* NCViewerRichWorkspace.storyboard */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; path = NCViewerRichWorkspace.storyboard; sourceTree = ""; }; + F7D4BF002CA1831600A5E746 /* NCCollectionViewCommonPinchGesture.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NCCollectionViewCommonPinchGesture.swift; sourceTree = ""; }; F7D532461F5D4123006568B1 /* is */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = is; path = is.lproj/Localizable.strings; sourceTree = ""; }; F7D5324D1F5D4137006568B1 /* nl */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = nl; path = nl.lproj/Localizable.strings; sourceTree = ""; }; F7D532541F5D4155006568B1 /* sk-SK */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "sk-SK"; path = "sk-SK.lproj/Localizable.strings"; sourceTree = ""; }; @@ -2089,9 +2094,7 @@ F7FDFF592E437E55000D7688 /* NCAccount.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NCAccount.swift; sourceTree = ""; }; F7FF2CB02842159500EBB7A1 /* NCSectionHeader.xib */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.xib; path = NCSectionHeader.xib; sourceTree = ""; }; F7FFFC9D2FB300440015441E /* NCAssistantSharedTextStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NCAssistantSharedTextStore.swift; sourceTree = ""; }; - 131F8E2F43AD414E9DB53019 /* NCPasscodeModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NCPasscodeModel.swift; sourceTree = ""; }; F902B24E44354B6D9F329F12 /* NCPasscodeView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NCPasscodeView.swift; sourceTree = ""; }; - BC6C501EF1104A6A8569D9C1 /* PasscodeUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PasscodeUITests.swift; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFileSystemSynchronizedRootGroup section */ @@ -2202,6 +2205,7 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( + F7D4BF542CA2ED9D00A5E746 /* VLCKitSPM in Frameworks */, F7D56B1A2972405500FA46C4 /* Mantis in Frameworks */, F7ED547C25EEA65400956C55 /* QRCodeReader in Frameworks */, F788ECC7263AAAFA00ADC67F /* MarkdownKit in Frameworks */, @@ -2369,6 +2373,15 @@ path = Advanced; sourceTree = ""; }; + B6DE60601BB3410D92EB99EC /* Passcode */ = { + isa = PBXGroup; + children = ( + 131F8E2F43AD414E9DB53019 /* NCPasscodeModel.swift */, + F902B24E44354B6D9F329F12 /* NCPasscodeView.swift */, + ); + path = Passcode; + sourceTree = ""; + }; C0046CDB2A17B98400D87C9D /* NextcloudUITests */ = { isa = PBXGroup; children = ( @@ -3067,6 +3080,7 @@ F7CCAB502ECF315F00F8E68B /* NCCollectionViewCommon+SyncMetadata.swift */, F721C5092FB6F9AA00207DA9 /* NCCollectionViewCommon+TransitionSourceBlink.swift */, F76995F32F9A4AC000291FA7 /* NCCollectionViewCommon+UIEditMenuInteractionDelegate.swift */, + F7D4BF002CA1831600A5E746 /* NCCollectionViewCommonPinchGesture.swift */, F38F71242B6BBDC300473CDC /* NCCollectionViewCommonSelectTabBar.swift */, F7C1EEA425053A9C00866ACC /* NCCollectionViewDataSource.swift */, F78ACD50219046AC0088454D /* Section Header Footer */, @@ -3883,15 +3897,6 @@ path = Account; sourceTree = ""; }; - B6DE60601BB3410D92EB99EC /* Passcode */ = { - isa = PBXGroup; - children = ( - 131F8E2F43AD414E9DB53019 /* NCPasscodeModel.swift */, - F902B24E44354B6D9F329F12 /* NCPasscodeView.swift */, - ); - path = Passcode; - sourceTree = ""; - }; /* End PBXGroup section */ /* Begin PBXNativeTarget section */ @@ -4145,6 +4150,7 @@ F7160A812BE933390034DCB3 /* RealmSwift */, F33EE6E02BF4BDA500CA1A51 /* NIOSSL */, F33EE6EF2BF4C0FF00CA1A51 /* NIO */, + F7D4BF532CA2ED9D00A5E746 /* VLCKitSPM */, F3374AEF2D78B01B002A38F9 /* BitCollections */, F3374AF12D78B01B002A38F9 /* Collections */, F3374AF32D78B01B002A38F9 /* DequeModule */, @@ -4375,6 +4381,7 @@ F3A0479C2BD268B500658E7B /* XCRemoteSwiftPackageReference "PopupView" */, F33EE6DF2BF4BDA500CA1A51 /* XCRemoteSwiftPackageReference "swift-nio-ssl" */, F33EE6EE2BF4C0FF00CA1A51 /* XCRemoteSwiftPackageReference "swift-nio" */, + F7D4BF4E2CA2ECCB00A5E746 /* XCRemoteSwiftPackageReference "vlckit-spm" */, F3374AEE2D78B01B002A38F9 /* XCRemoteSwiftPackageReference "swift-collections" */, F70557B52ED44E2700135623 /* XCRemoteSwiftPackageReference "LucidBanner" */, F76BED72306CE862002E1442 /* XCRemoteSwiftPackageReference "GRDB" */, @@ -5239,6 +5246,7 @@ F71F6D072B6A6A5E00F1EB15 /* ThreadSafeArray.swift in Sources */, F761856C29E98543006EB3B0 /* NCIntroCollectionViewCell.swift in Sources */, F75DD765290ABB25002EB562 /* Intent.intentdefinition in Sources */, + F7D4BF012CA1831900A5E746 /* NCCollectionViewCommonPinchGesture.swift in Sources */, F74B6D952A7E239A00F03C5F /* NCManageDatabase+Chunk.swift in Sources */, F702F2F725EE5CED008F8E80 /* NCLogin.swift in Sources */, F75D90212D2BE26F003E740B /* NCRecommendationsCell.swift in Sources */, @@ -7111,6 +7119,14 @@ minimumVersion = 1.0.0; }; }; + F7D4BF4E2CA2ECCB00A5E746 /* XCRemoteSwiftPackageReference "vlckit-spm" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/tylerjonesio/vlckit-spm"; + requirement = { + kind = upToNextMajorVersion; + minimumVersion = 3.5.1; + }; + }; F7D56B182972405400FA46C4 /* XCRemoteSwiftPackageReference "Mantis" */ = { isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/marinofaggiana/Mantis"; @@ -7513,6 +7529,11 @@ package = F783034028B511D200B84583 /* XCRemoteSwiftPackageReference "NextcloudKit" */; productName = NextcloudKit; }; + F7D4BF532CA2ED9D00A5E746 /* VLCKitSPM */ = { + isa = XCSwiftPackageProductDependency; + package = F7D4BF4E2CA2ECCB00A5E746 /* XCRemoteSwiftPackageReference "vlckit-spm" */; + productName = VLCKitSPM; + }; F7D56B192972405500FA46C4 /* Mantis */ = { isa = XCSwiftPackageProductDependency; package = F7D56B182972405400FA46C4 /* XCRemoteSwiftPackageReference "Mantis" */; diff --git a/Nextcloud.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/Nextcloud.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index 7b5e49729a..87db5d5f5e 100644 --- a/Nextcloud.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/Nextcloud.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -1,5 +1,5 @@ { - "originHash" : "e8140d7bd4476541d44ce4bf03cb73841973e05cd3c93172bf90ff7d869ccd81", + "originHash" : "7880536aedfe5f5b326df402ac666a6126ac1b325106ab3eb7b6c78a6ef725ff", "pins" : [ { "identity" : "abseil-cpp-binary", @@ -360,6 +360,15 @@ "revision" : "91d7bca65721928af30c6a2bb1a1f517f7862964", "version" : "2.1.21" } + }, + { + "identity" : "vlckit-spm", + "kind" : "remoteSourceControl", + "location" : "https://github.com/tylerjonesio/vlckit-spm", + "state" : { + "revision" : "e932bbd488872fdb74f6654d28c2f291eae03daf", + "version" : "3.6.0" + } } ], "version" : 3 From 6f23c17358818b3f00833c2dbcfc8ebc22ab77ee Mon Sep 17 00:00:00 2001 From: Marino Faggiana Date: Fri, 23 Oct 2026 11:22:52 +0200 Subject: [PATCH 08/14] fix: preserve passcode reset threshold across lockouts Clear only the retry counter and lockout deadline when the delay expires. Add UI coverage verifying app reset after ten failures across lockouts. Signed-off-by: Marino Faggiana --- Tests/NextcloudUITests/PasscodeUITests.swift | 34 +++++++++++++++++++ .../Settings/Settings/NCSettingsView.swift | 1 + .../Utility/Passcode/NCPasscodeModel.swift | 4 ++- 3 files changed, 38 insertions(+), 1 deletion(-) diff --git a/Tests/NextcloudUITests/PasscodeUITests.swift b/Tests/NextcloudUITests/PasscodeUITests.swift index c480630482..ec6e3cf3a3 100644 --- a/Tests/NextcloudUITests/PasscodeUITests.swift +++ b/Tests/NextcloudUITests/PasscodeUITests.swift @@ -166,6 +166,40 @@ final class PasscodeUITests: BaseUIXCTestCase { XCTAssertTrue(lockSetting.waitForExistence(timeout: 5), "Unlocking must preserve the Settings screen.") } + /// Waiting out each three-attempt lockout must not reset the ten-attempt app-wipe counter. + func testAppResetAfterTenFailuresAcrossLockouts() { + createPasscode() + let resetSetting = app.switches["settings.passcode.reset"] + for _ in 0..<3 where !resetSetting.isHittable { + app.swipeUp() + } + XCTAssertTrue(resetSetting.waitForExistence(timeout: 5)) + XCTAssertEqual(resetSetting.value as? String, "0") + resetSetting.tap() + XCTAssertEqual(resetSetting.value as? String, "1") + + backgroundAndActivate() + assertTitle("Enter Passcode") + for attempt in 1...10 { + enter(wrongCode) + if attempt.isMultiple(of: 3) { + assertLockedOut() + XCTAssertTrue(lockout.waitForNonExistence(timeout: 65)) + XCTAssertTrue(app.buttons["passcode.digit.1"].isEnabled) + } else if attempt < 10 { + XCTAssertTrue(title.exists, "The app must remain locked until the reset threshold.") + } + } + + // The real reset clears local data and exits. Relaunch without the test-setup reset. + XCTAssertTrue(app.wait(for: .notRunning, timeout: 10)) + currentCode = nil + app.launchArguments = languageArguments + app.launch() + XCTAssertTrue(app.buttons["login"].waitForExistence(timeout: TestConstants.controlExistenceTimeout)) + XCTAssertFalse(title.exists, "The app reset must remove the stored passcode.") + } + private func createPasscode() { lockSetting.tap() assertTitle("Enter a new passcode") diff --git a/iOSClient/Settings/Settings/NCSettingsView.swift b/iOSClient/Settings/Settings/NCSettingsView.swift index fabc2ae45c..9aa1bbc292 100644 --- a/iOSClient/Settings/Settings/NCSettingsView.swift +++ b/iOSClient/Settings/Settings/NCSettingsView.swift @@ -97,6 +97,7 @@ struct NCSettingsView: View { // Reset app wrong attempts Toggle(NSLocalizedString("_reset_wrong_passcode_option_", comment: ""), isOn: $model.resetWrongAttempts) + .accessibilityIdentifier("settings.passcode.reset") .font(.body) .onChange(of: model.resetWrongAttempts) { model.updateResetWrongAttemptsSetting() diff --git a/iOSClient/Utility/Passcode/NCPasscodeModel.swift b/iOSClient/Utility/Passcode/NCPasscodeModel.swift index 9a7febc268..0aaae1392c 100644 --- a/iOSClient/Utility/Passcode/NCPasscodeModel.swift +++ b/iOSClient/Utility/Passcode/NCPasscodeModel.swift @@ -91,7 +91,9 @@ final class NCPasscodeModel: ObservableObject { guard !isPreview else { return } guard result == nil else { return } if let deadline = preferences.passcodeLockoutEnd, deadline <= Date() { - preferences.clearPasscodeFailures() + // Expiring the retry delay must preserve the total used for the app-reset threshold. + preferences.passcodeCounterFail = 0 + preferences.passcodeLockoutEnd = nil } if Self.isPasscodeReset { finish(.reset) From c6c833ed72ff338aee7f71e62cedbcacbc2e16b1 Mon Sep 17 00:00:00 2001 From: Marino Faggiana Date: Sat, 24 Oct 2026 10:15:01 +0200 Subject: [PATCH 09/14] fix: keep app passcode lock above sheets and alerts Present the mandatory lock in a dedicated window per scene and preserve underlying UI after unlocking. Add UI regression coverage, improve authentication test handling, and enforce CI test timeouts with retained diagnostics. Signed-off-by: Marino Faggiana --- .github/workflows/xcode.yml | 9 +- AGENTS.md | 2 +- Nextcloud.xcodeproj/project.pbxproj | 4 + Tests/NextcloudUITests/BaseUIXCTestCase.swift | 143 +++++++++--------- Tests/NextcloudUITests/PasscodeUITests.swift | 56 +++++++ iOSClient/SceneDelegate.swift | 112 +++++++++----- .../Helpers/NCAcknowledgementsView.swift | 1 + .../Utility/Passcode/NCPasscodeView.swift | 10 +- .../Utility/Passcode/NCPasscodeWindow.swift | 53 +++++++ 9 files changed, 276 insertions(+), 114 deletions(-) create mode 100644 iOSClient/Utility/Passcode/NCPasscodeWindow.swift diff --git a/.github/workflows/xcode.yml b/.github/workflows/xcode.yml index e20b49248f..1adf3ef472 100644 --- a/.github/workflows/xcode.yml +++ b/.github/workflows/xcode.yml @@ -62,6 +62,7 @@ jobs: test: name: Run Tests runs-on: xcode-27 + timeout-minutes: 30 needs: [build] if: github.event.pull_request.draft == false @@ -116,7 +117,7 @@ jobs: name: Nextcloud for iOS - name: Assert Nextcloud Server Status - run: curl -s --retry 5 --retry-delay 60 --retry-all-errors http://localhost:8080/status.php || true + run: curl --fail --show-error --silent --max-time 5 --retry 5 --retry-delay 2 --retry-max-time 30 --retry-all-errors http://localhost:8080/status.php - name: Boot iOS Simulator run: | @@ -133,6 +134,7 @@ jobs: fi - name: Run Xcode Test + timeout-minutes: 25 run: | set -o pipefail && \ xcodebuild test-without-building \ @@ -141,11 +143,14 @@ jobs: -derivedDataPath "DerivedData" \ -test-iterations 3 \ -retry-tests-on-failure \ + -test-timeouts-enabled YES \ + -default-test-execution-time-allowance 360 \ + -maximum-test-execution-time-allowance 360 \ -resultBundlePath "TestResult.xcresult" - name: Upload Xcode Test Results uses: actions/upload-artifact@v7 - if: ${{ !cancelled() }} + if: ${{ always() }} with: name: TestResult.xcresult path: "TestResult.xcresult" diff --git a/AGENTS.md b/AGENTS.md index a5e778ff25..18c62c6bca 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -26,7 +26,7 @@ Other platforms like Android are irrelevant for this project. |-----------------|-----------------------------------------------------|----------------------| | `.github` | GitHub CI workflows (build & test, SwiftLint, extension-target builds) and issue templates. | Keep all three checks green: `xcode.yml` (builds the `Nextcloud` scheme and runs its tests against a Nextcloud server provisioned in CI), `lint.yml` (SwiftLint), `additional-targets.yml` (build-checks the extension schemes). CI skips draft PRs. | | `iOSClient` | Main iOS client source code. | — | -| `iOSClient/Utility/Passcode` | Shared SwiftUI passcode UI and authentication model used by the main app and Share Extension. | Preserve existing credentials, lockout rules and app/extension reset behavior. | +| `iOSClient/Utility/Passcode` | Shared SwiftUI passcode UI and authentication model for the app and Share Extension; the mandatory app lock uses a dedicated window per scene. | Preserve existing credentials, lockout rules and app/extension reset behavior. | | `iOSClient/Supporting Files` | Translation files synced with Transifex (see `iOSClient/.tx/config`). | Only add new strings to `en.lproj` (`Localizable.strings`, plurals in `Localizable.stringsdict`); never edit the other `*.lproj` dirs — Transifex overwrites them. | | `Brand` | Branding layer: `NCBrand.swift` (app constants, colors), `Database.swift` (Realm database name and schema version), Info.plists and entitlements for all targets, launch screen and intro screens. | Bump `databaseSchemaVersion` in `Brand/Database.swift` when changing Realm models. | | `File Provider Extension` | Contains business logic for the iOS File Provider extension. | — | diff --git a/Nextcloud.xcodeproj/project.pbxproj b/Nextcloud.xcodeproj/project.pbxproj index 6e16370d47..56e9f8beb5 100644 --- a/Nextcloud.xcodeproj/project.pbxproj +++ b/Nextcloud.xcodeproj/project.pbxproj @@ -7,6 +7,7 @@ objects = { /* Begin PBXBuildFile section */ + A0C0DE0230E0000100C0D001 /* NCPasscodeWindow.swift in Sources */ = {isa = PBXBuildFile; fileRef = A0C0DE0130E0000100C0D001 /* NCPasscodeWindow.swift */; }; 22BA14432CF44BDBA2E6C291 /* NCDocumentEditorSupportTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B74B947E07444CC9CFE0A6C /* NCDocumentEditorSupportTests.swift */; }; 2C1D5D7623E2DE3300334ABB /* NCManageDatabase.swift in Sources */ = {isa = PBXBuildFile; fileRef = F7BAADB51ED5A87C00B7EAD4 /* NCManageDatabase.swift */; }; 2C1D5D7923E2DE9100334ABB /* NCBrand.swift in Sources */ = {isa = PBXBuildFile; fileRef = F76B3CCD1EAE01BD00921AC9 /* NCBrand.swift */; }; @@ -1299,6 +1300,7 @@ /* End PBXCopyFilesBuildPhase section */ /* Begin PBXFileReference section */ + A0C0DE0130E0000100C0D001 /* NCPasscodeWindow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NCPasscodeWindow.swift; sourceTree = ""; }; 131F8E2F43AD414E9DB53019 /* NCPasscodeModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NCPasscodeModel.swift; sourceTree = ""; }; 2C33C47F23E2C475005F963B /* Notification Service Extension.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = "Notification Service Extension.appex"; sourceTree = BUILT_PRODUCTS_DIR; }; 2C33C48123E2C475005F963B /* NotificationService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationService.swift; sourceTree = ""; }; @@ -2377,6 +2379,7 @@ isa = PBXGroup; children = ( 131F8E2F43AD414E9DB53019 /* NCPasscodeModel.swift */, + A0C0DE0130E0000100C0D001 /* NCPasscodeWindow.swift */, F902B24E44354B6D9F329F12 /* NCPasscodeView.swift */, ); path = Passcode; @@ -5393,6 +5396,7 @@ F7BCE48E301091EA00876F4D /* NCMediaDatePickerViewController.swift in Sources */, 9F0E3B428AB44A6480190C2D /* NCPasscodeModel.swift in Sources */, 7670DABED44E4D56AD99E6DC /* NCPasscodeView.swift in Sources */, + A0C0DE0230E0000100C0D001 /* NCPasscodeWindow.swift in Sources */, F77C97392953131000FDDD09 /* NCCameraRoll.swift in Sources */, F7CADEFD2EA159210057849E /* NCMetadataUploadTranfersSuccess.swift in Sources */, F343A4B32A1E01FF00DDA874 /* PHAsset+Extension.swift in Sources */, diff --git a/Tests/NextcloudUITests/BaseUIXCTestCase.swift b/Tests/NextcloudUITests/BaseUIXCTestCase.swift index 5dd1a21de8..1643fbc750 100644 --- a/Tests/NextcloudUITests/BaseUIXCTestCase.swift +++ b/Tests/NextcloudUITests/BaseUIXCTestCase.swift @@ -1,5 +1,6 @@ // SPDX-FileCopyrightText: Nextcloud GmbH // SPDX-FileCopyrightText: 2025 Milen Pivchev +// SPDX-FileCopyrightText: 2026 Marino Faggiana // SPDX-License-Identifier: GPL-3.0-or-later import Foundation @@ -58,92 +59,92 @@ class BaseUIXCTestCase: XCTestCase { try await Task.sleep(for: .seconds(TestConstants.controlExistenceTimeout)) } - /// - /// Automation of the sign-in, if required. - /// - /// + /// Sign in through the app's authentication flow, supporting system authentication and the embedded fallback. func logIn() async throws { - guard app.buttons["login"].exists else { + if app.buttons["openSidebar"].exists || app.buttons["accountSwitcher"].exists { return } - - app.buttons["login"].tap() - - let serverAddressTextField = app.textFields["serverAddress"].firstMatch - guard serverAddressTextField.await() else { return } - - try await aSmallMoment() - - serverAddressTextField.tap() - serverAddressTextField.typeText(TestConstants.server) - + let login = app.buttons["login"] + guard login.await() else { throw UITestError.waitForExistence(login) } + login.tap() + + let serverAddress = app.textFields["serverAddress"].firstMatch + guard serverAddress.await() else { throw UITestError.waitForExistence(serverAddress) } + serverAddress.tap() + serverAddress.typeText(TestConstants.server) app.buttons["submitServerAddress"].tap() - try await aSmallMoment() - - let webView = app.webViews.firstMatch - - guard webView.await() else { - throw UITestError.waitForExistence(webView) + let webView = try await authenticationWebView() + let username = webView.textFields.firstMatch + let loginButton = webView.buttons["Log in"].firstMatch + if !username.exists, loginButton.await(timeout: 5) { + loginButton.tap() } - -// try await aSmallMoment() - - let loginButton = webView.buttons["Log in"] - -// try await aSmallMoment() - - if loginButton.await() { + if username.await() { + username.tap() + username.typeText(TestConstants.username) + let password = webView.secureTextFields.firstMatch + guard password.await() else { throw UITestError.waitForExistence(password) } + password.tap() + password.typeText(TestConstants.password) + guard loginButton.await() else { throw UITestError.waitForExistence(loginButton) } loginButton.tap() } -// try await aSmallMoment() - - let usernameTextField = webView.textFields.firstMatch - - if usernameTextField.await() { - - try await aSmallMoment() - - guard usernameTextField.await() else { return } - usernameTextField.tap() - - try await aSmallMoment() - - usernameTextField.typeText(TestConstants.username) - - try await aSmallMoment() - - let passwordSecureTextField = webView.secureTextFields.firstMatch - - try await aSmallMoment() - - passwordSecureTextField.tap() - - - try await aSmallMoment() - - passwordSecureTextField.typeText(TestConstants.password) - - try await aSmallMoment() - - webView.buttons.firstMatch.tap() + let grant = webView.buttons["Grant access"] + guard grant.await(timeout: 30) else { + attachAuthenticationDiagnostics() + throw UITestError.waitForExistence(grant) } + grant.tap() - try await aSmallMoment() - - let grantButton = webView.buttons["Grant access"] - - guard grantButton.await() else { - throw UITestError.waitForExistence(grantButton) + let sidebar = app.buttons["openSidebar"].firstMatch + guard sidebar.await(timeout: 30) || app.buttons["accountSwitcher"].exists else { + attachAuthenticationDiagnostics() + throw UITestError.waitForExistence(sidebar) } + } - grantButton.tap() - grantButton.awaitInexistence() + /// ASWebAuthenticationSession may expose its UI in SafariViewService rather than in the app hierarchy. + private func authenticationWebView() async throws -> XCUIElement { + let browsers = [app!, XCUIApplication(bundleIdentifier: "com.apple.SafariViewService")] + let promptHosts = browsers + [XCUIApplication(bundleIdentifier: "com.apple.springboard")] + let deadline = Date().addingTimeInterval(30) + repeat { + for host in promptHosts { + for label in ["Continue", "Allow"] { + let button = host.alerts.buttons[label].firstMatch + if button.exists && button.isHittable { + button.tap() + } + } + } + for browser in browsers { + let webView = browser.webViews.firstMatch + if webView.exists { + return webView + } + } + try await Task.sleep(for: .milliseconds(250)) + } while Date() < deadline - app.buttons["accountSwitcher"].await() + attachAuthenticationDiagnostics() + throw UITestError.waitForExistence(app.webViews.firstMatch) + } - try await aSmallMoment() + /// Keep login diagnostics in the xcresult instead of losing the context on a CI failure. + private func attachAuthenticationDiagnostics() { + let screenshot = XCTAttachment(screenshot: XCUIScreen.main.screenshot()) + screenshot.name = "Authentication screen" + screenshot.lifetime = .keepAlways + add(screenshot) + for host in [app!, XCUIApplication(bundleIdentifier: "com.apple.SafariViewService"), XCUIApplication(bundleIdentifier: "com.apple.springboard")] + where host.state == .runningForeground { + let hierarchy = XCTAttachment(string: host.debugDescription) + hierarchy.name = "Authentication UI hierarchy" + hierarchy.lifetime = .keepAlways + add(hierarchy) + } } /// diff --git a/Tests/NextcloudUITests/PasscodeUITests.swift b/Tests/NextcloudUITests/PasscodeUITests.swift index ec6e3cf3a3..fe9b655b06 100644 --- a/Tests/NextcloudUITests/PasscodeUITests.swift +++ b/Tests/NextcloudUITests/PasscodeUITests.swift @@ -15,6 +15,7 @@ final class PasscodeUITests: BaseUIXCTestCase { private var title: XCUIElement { app.staticTexts["passcode.title"] } private var closeButton: XCUIElement { app.buttons["passcode.close"] } + private var lockWindow: XCUIElement { app.windows["passcode.window"] } private var lockout: XCUIElement { app.staticTexts["passcode.lockout.remaining"] } private var lockSetting: XCUIElement { app.buttons["settings.passcode.toggle"] } private var changeSetting: XCUIElement { app.buttons["settings.passcode.change"] } @@ -166,6 +167,50 @@ final class PasscodeUITests: BaseUIXCTestCase { XCTAssertTrue(lockSetting.waitForExistence(timeout: 5), "Unlocking must preserve the Settings screen.") } + /// An informational sheet stays in the app window while the mandatory lock covers it. + func testAppLockAppearsAbovePresentedSheet() { + createPasscode() + let acknowledgements = app.buttons["Acknowledgements"] + scrollTo(acknowledgements) + acknowledgements.tap() + let close = app.buttons["settings.acknowledgements.close"] + XCTAssertTrue(close.waitForExistence(timeout: 5)) + + backgroundAndActivate() + XCTAssertTrue(lockWindow.waitForExistence(timeout: 5)) + assertTitle("Enter Passcode") + XCTAssertFalse(close.isHittable) + enter(originalCode) + XCTAssertTrue(lockWindow.waitForNonExistence(timeout: 5)) + XCTAssertTrue(close.isHittable, "Unlocking must preserve the sheet underneath.") + close.tap() + scrollTo(lockSetting, towardsTop: true) + } + + /// An existing confirmation alert cannot intercept or prevent the app-lock window. + func testAppLockAppearsAbovePresentedAlert() { + createPasscode() + let advanced = app.buttons["Advanced"] + scrollTo(advanced) + advanced.tap() + let clearCache = app.buttons["Clear cache"] + scrollTo(clearCache) + clearCache.tap() + let cancel = app.alerts.firstMatch.buttons["Cancel"] + XCTAssertTrue(cancel.waitForExistence(timeout: 5)) + + backgroundAndActivate() + XCTAssertTrue(lockWindow.waitForExistence(timeout: 5)) + assertTitle("Enter Passcode") + XCTAssertFalse(cancel.isHittable) + enter(originalCode) + XCTAssertTrue(lockWindow.waitForNonExistence(timeout: 5)) + XCTAssertTrue(cancel.isHittable, "Unlocking must preserve the confirmation alert underneath.") + cancel.tap() + app.navigationBars["Advanced"].buttons["Settings"].tap() + scrollTo(lockSetting, towardsTop: true) + } + /// Waiting out each three-attempt lockout must not reset the ten-attempt app-wipe counter. func testAppResetAfterTenFailuresAcrossLockouts() { createPasscode() @@ -221,6 +266,17 @@ final class PasscodeUITests: BaseUIXCTestCase { XCTAssertTrue(lockSetting.waitForExistence(timeout: 5)) } + private func scrollTo(_ element: XCUIElement, towardsTop: Bool = false) { + for _ in 0..<6 where !element.isHittable { + if towardsTop { + app.swipeDown() + } else { + app.swipeUp() + } + } + XCTAssertTrue(element.isHittable) + } + private func enter(_ code: String) { for digit in code { let button = app.buttons["passcode.digit.\(digit)"] diff --git a/iOSClient/SceneDelegate.swift b/iOSClient/SceneDelegate.swift index c3baba7c83..c1ef0c01b5 100644 --- a/iOSClient/SceneDelegate.swift +++ b/iOSClient/SceneDelegate.swift @@ -16,6 +16,7 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate { private let appDelegate = UIApplication.shared.delegate as? AppDelegate private var privacyProtectionWindow: UIWindow? + private var passcodeWindow: NCPasscodeWindow? private let global = NCGlobal.shared private let alreadyMigratedMultiDomains = UserDefaults.standard.bool(forKey: NCGlobal.shared.udMigrationMultiDomains) @@ -112,6 +113,8 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate { // NO account found, start with the Login // NCPreferences().removeAll() + passcodeWindow?.hide() + passcodeWindow = nil if let bundleID = Bundle.main.bundleIdentifier { UserDefaults.standard.removePersistentDomain(forName: bundleID) @@ -184,6 +187,8 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate { // window?.rootViewController = controller window?.makeKeyAndVisible() + // Also protect interfaces created after an asynchronous maintenance or migration. + requestPasscodeIfNeeded(controller: controller) // if activateSceneForAccount { self.activateSceneForAccount(scene, account: activeTblAccount.account, controller: controller) @@ -199,6 +204,9 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate { func sceneDidDisconnect(_ scene: UIScene) { guard let windowScene = scene as? UIWindowScene else { return } + passcodeWindow?.hide() + passcodeWindow = nil + hidePrivacyProtectionWindow() LucidBannerRegistry.shared.remove(for: windowScene) lucidBanner = nil @@ -208,15 +216,11 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate { func sceneWillEnterForeground(_ scene: UIScene) { hidePrivacyProtectionWindow() - if let rootHostingController = scene.rootHostingController() { - if rootHostingController.anyRootView is Maintenance { - return - } + guard let controller = window?.rootViewController as? NCMainTabBarController else { + return } - let session = SceneManager.shared.getSession(scene: scene) - let controller = SceneManager.shared.getController(scene: scene) - activateSceneForAccount(scene, account: session.account, controller: controller) + activateSceneForAccount(scene, account: controller.account, controller: controller) } func sceneDidBecomeActive(_ scene: UIScene) { @@ -255,16 +259,14 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate { guard let tblAccount = NCManageDatabase.shared.getTableAccount(predicate: NSPredicate(format: "account == %@", session.account)) else { return } + if NCPreferences().presentPasscode { + showPrivacyProtectionWindow() + } bgID = app.beginBackgroundTask(withName: "FlushBeforeSuspend") { app.endBackgroundTask(bgID); bgID = .invalid } Task { - Task { @MainActor in - if NCPreferences().presentPasscode { - showPrivacyProtectionWindow() - } - } defer { app.endBackgroundTask(bgID); bgID = .invalid } @@ -509,14 +511,15 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate { } private func showPrivacyProtectionWindow() { - guard let windowScene = self.window?.windowScene else { + guard privacyProtectionWindow == nil, + let windowScene = self.window?.windowScene else { return } self.privacyProtectionWindow = UIWindow(windowScene: windowScene) self.privacyProtectionWindow?.rootViewController = UIStoryboard(name: "LaunchScreen", bundle: nil).instantiateInitialViewController() self.privacyProtectionWindow?.windowLevel = .alert + 1 - self.privacyProtectionWindow?.makeKeyAndVisible() + self.privacyProtectionWindow?.isHidden = false } private func hidePrivacyProtectionWindow() { @@ -524,34 +527,60 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate { privacyProtectionWindow = nil } + /// Request the mandatory lock; return whether authentication is required, including while the UI is not ready. + @discardableResult + private func requestPasscodeIfNeeded(controller: NCMainTabBarController) -> Bool { + guard NCPreferences().presentPasscode else { + passcodeWindow?.hide() + passcodeWindow = nil + return false + } + guard !controller.account.isEmpty, + NCManageDatabase.shared.getTableAccount(account: controller.account) != nil, + let window, + window.rootViewController === controller, + let windowScene = window.windowScene else { + return true + } + if passcodeWindow == nil { + passcodeWindow = NCPasscodeWindow(mainWindow: window, windowScene: windowScene) { [weak self, weak controller] result in + guard let self else { return } + self.passcodeWindow = nil + switch result { + case .authenticated: + if NCPreferences().accountRequest, + let controller, + self.window?.rootViewController === controller, + NCManageDatabase.shared.getTableAccount(account: controller.account) != nil { + self.requestedAccount(controller: controller) + } + case .reset: + self.appDelegate?.resetApplication() + case .cancelled, .configured, .disabled: + break + } + } + } + passcodeWindow?.show() + return true + } + private func activateSceneForAccount(_ scene: UIScene, account: String, controller: NCMainTabBarController?) { - guard !account.isEmpty else { + guard !account.isEmpty, + let controller, + controller.account == account, + NCManageDatabase.shared.getTableAccount(account: account) != nil, + let window, + window.rootViewController === controller, + let windowScene = scene as? UIWindowScene, + window.windowScene === windowScene else { return } - if let window = SceneManager.shared.getWindow(scene: scene), - let controller = SceneManager.shared.getController(scene: scene) { - if window.rootViewController !== controller { - window.rootViewController = controller - } - if NCPreferences().presentPasscode { - NCPasscodeView.present(from: controller) { [weak self, weak controller] result in - switch result { - case .authenticated, .configured: - if NCPreferences().accountRequest, let controller { - self?.requestedAccount(controller: controller) - } - case .reset: - self?.appDelegate?.resetApplication() - case .cancelled, .disabled: - break - } - } - } else if NCPreferences().accountRequest { - requestedAccount(controller: controller) - } + if !requestPasscodeIfNeeded(controller: controller), NCPreferences().accountRequest { + requestedAccount(controller: controller) } Task { @@ -575,6 +604,13 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate { extension SceneDelegate { func requestedAccount(controller: UIViewController?) { + guard var presenter = controller else { return } + while let presented = presenter.presentedViewController { + if let popup = presented as? NCPopupViewController, popup.contentController is NCAccountRequest { + return + } + presenter = presented + } let tblAccounts = NCManageDatabase.shared.getAllTableAccount() if tblAccounts.count > 1, let accountRequestVC = UIStoryboard(name: "NCAccountRequest", bundle: nil).instantiateInitialViewController() as? NCAccountRequest { accountRequestVC.controller = controller @@ -586,7 +622,7 @@ extension SceneDelegate { accountRequestVC.delegate = self accountRequestVC.startTimer(nil) - let availableHeight = controller?.view.safeAreaLayoutGuide.layoutFrame.height ?? window?.safeAreaLayoutGuide.layoutFrame.height ?? 0 + let availableHeight = presenter.view.safeAreaLayoutGuide.layoutFrame.height let maximumHeight = availableHeight * 0.8 let numberCell = tblAccounts.count let height = min(CGFloat(numberCell * Int(accountRequestVC.heightCell) + 45), maximumHeight) @@ -594,7 +630,7 @@ extension SceneDelegate { let popup = NCPopupViewController(contentController: accountRequestVC, popupWidth: 300, popupHeight: height + 20) popup.backgroundAlpha = 0.8 - controller?.present(popup, animated: true) + presenter.present(popup, animated: true) } } } diff --git a/iOSClient/Settings/Helpers/NCAcknowledgementsView.swift b/iOSClient/Settings/Helpers/NCAcknowledgementsView.swift index 7f770337f2..bd466b61fa 100644 --- a/iOSClient/Settings/Helpers/NCAcknowledgementsView.swift +++ b/iOSClient/Settings/Helpers/NCAcknowledgementsView.swift @@ -41,6 +41,7 @@ struct NCAcknowledgementsView: View { .foregroundColor(Color(NCBrandColor.shared.iconImageColor)) } } + .accessibilityIdentifier("settings.acknowledgements.close") .padding() } Divider() diff --git a/iOSClient/Utility/Passcode/NCPasscodeView.swift b/iOSClient/Utility/Passcode/NCPasscodeView.swift index d1137e310c..5aecd1f662 100644 --- a/iOSClient/Utility/Passcode/NCPasscodeView.swift +++ b/iOSClient/Utility/Passcode/NCPasscodeView.swift @@ -116,7 +116,13 @@ struct NCPasscodeView: View { } } .interactiveDismissDisabled(!model.allowCancel) - .task { await model.start() } + .task { + if let windowScene, windowScene.activationState != .foregroundActive { + model.refreshProtection() + } else { + await model.start() + } + } .task(id: model.lockoutEnd) { guard let deadline = model.lockoutEnd else { return } let remaining = max(0, deadline.timeIntervalSinceNow) @@ -127,7 +133,7 @@ struct NCPasscodeView: View { } .onReceive(NotificationCenter.default.publisher(for: UIScene.didActivateNotification)) { notification in guard let windowScene, (notification.object as? UIScene) === windowScene else { return } - model.refreshProtection() + Task { await model.start() } } .onReceive(NotificationCenter.default.publisher(for: UIScene.didEnterBackgroundNotification)) { notification in guard let windowScene, (notification.object as? UIScene) === windowScene else { return } diff --git a/iOSClient/Utility/Passcode/NCPasscodeWindow.swift b/iOSClient/Utility/Passcode/NCPasscodeWindow.swift new file mode 100644 index 0000000000..09bb0b4cb9 --- /dev/null +++ b/iOSClient/Utility/Passcode/NCPasscodeWindow.swift @@ -0,0 +1,53 @@ +// SPDX-FileCopyrightText: Nextcloud GmbH +// SPDX-FileCopyrightText: 2026 Marino Faggiana +// SPDX-License-Identifier: GPL-3.0-or-later + +import SwiftUI + +/// Owns the mandatory lock above one scene's app window, independently of modal presentations. +@MainActor +final class NCPasscodeWindow: UIWindow { + private weak var mainWindow: UIWindow? + + init(mainWindow: UIWindow, windowScene: UIWindowScene, + completion: @escaping @MainActor (NCPasscodeModel.Result) -> Void) { + self.mainWindow = mainWindow + super.init(windowScene: windowScene) + // Above the app's banners, PopupView windows and privacy screen. + windowLevel = .alert + 2 + backgroundColor = .systemBackground + overrideUserInterfaceStyle = mainWindow.overrideUserInterfaceStyle + accessibilityIdentifier = "passcode.window" + accessibilityViewIsModal = true + + let model = NCPasscodeModel(mode: .authenticate, allowCancel: false) + let controller = UIHostingController(rootView: NCPasscodeView(model: model, windowScene: windowScene)) + controller.isModalInPresentation = true + rootViewController = controller + model.onCompletion = { [weak self] result in + guard let self, result == .authenticated || result == .reset else { return } + self.hide() + completion(result) + } + } + + @available(*, unavailable) + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + /// Show immediately without presenting or dismissing any controller in the app window. + func show() { + makeKeyAndVisible() + } + + /// Release the lock and restore focus to the owning app window when its scene is active. + func hide() { + let restoreFocus = isKeyWindow && windowScene?.activationState == .foregroundActive + isHidden = true + rootViewController = nil + if restoreFocus { + mainWindow?.makeKey() + } + } +} From 5e15d9abcd8eb4612e5ce68f07cd56a4176bf73d Mon Sep 17 00:00:00 2001 From: Marino Faggiana Date: Sat, 24 Oct 2026 10:53:53 +0200 Subject: [PATCH 10/14] fix: wait for HTTP fallback in UI test login Allow the initial HTTPS request to time out before expecting the localhost authentication page, and skip UI queries against authentication services that are not running. Signed-off-by: Marino Faggiana Assisted-by: Codex:GPT-6 --- Tests/NextcloudUITests/BaseUIXCTestCase.swift | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/Tests/NextcloudUITests/BaseUIXCTestCase.swift b/Tests/NextcloudUITests/BaseUIXCTestCase.swift index 1643fbc750..af1f1846ca 100644 --- a/Tests/NextcloudUITests/BaseUIXCTestCase.swift +++ b/Tests/NextcloudUITests/BaseUIXCTestCase.swift @@ -109,9 +109,12 @@ class BaseUIXCTestCase: XCTestCase { private func authenticationWebView() async throws -> XCUIElement { let browsers = [app!, XCUIApplication(bundleIdentifier: "com.apple.SafariViewService")] let promptHosts = browsers + [XCUIApplication(bundleIdentifier: "com.apple.springboard")] - let deadline = Date().addingTimeInterval(30) + // The app tries HTTPS first, even for the HTTP-only localhost test server. + // Allow the initial URLSession request to time out before its HTTP fallback starts. + let timeout = URLSessionConfiguration.default.timeoutIntervalForRequest + 30 + let deadline = Date().addingTimeInterval(timeout) repeat { - for host in promptHosts { + for host in promptHosts where host.state != .notRunning { for label in ["Continue", "Allow"] { let button = host.alerts.buttons[label].firstMatch if button.exists && button.isHittable { @@ -119,7 +122,7 @@ class BaseUIXCTestCase: XCTestCase { } } } - for browser in browsers { + for browser in browsers where browser.state != .notRunning { let webView = browser.webViews.firstMatch if webView.exists { return webView From 14de69c8a54b75b1c958736e0aff366f92e38bed Mon Sep 17 00:00:00 2001 From: Marino Faggiana Date: Sat, 24 Oct 2026 11:00:06 +0200 Subject: [PATCH 11/14] CI Signed-off-by: Marino Faggiana --- Tests/NextcloudUITests/BaseUIXCTestCase.swift | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/Tests/NextcloudUITests/BaseUIXCTestCase.swift b/Tests/NextcloudUITests/BaseUIXCTestCase.swift index 1643fbc750..af1f1846ca 100644 --- a/Tests/NextcloudUITests/BaseUIXCTestCase.swift +++ b/Tests/NextcloudUITests/BaseUIXCTestCase.swift @@ -109,9 +109,12 @@ class BaseUIXCTestCase: XCTestCase { private func authenticationWebView() async throws -> XCUIElement { let browsers = [app!, XCUIApplication(bundleIdentifier: "com.apple.SafariViewService")] let promptHosts = browsers + [XCUIApplication(bundleIdentifier: "com.apple.springboard")] - let deadline = Date().addingTimeInterval(30) + // The app tries HTTPS first, even for the HTTP-only localhost test server. + // Allow the initial URLSession request to time out before its HTTP fallback starts. + let timeout = URLSessionConfiguration.default.timeoutIntervalForRequest + 30 + let deadline = Date().addingTimeInterval(timeout) repeat { - for host in promptHosts { + for host in promptHosts where host.state != .notRunning { for label in ["Continue", "Allow"] { let button = host.alerts.buttons[label].firstMatch if button.exists && button.isHittable { @@ -119,7 +122,7 @@ class BaseUIXCTestCase: XCTestCase { } } } - for browser in browsers { + for browser in browsers where browser.state != .notRunning { let webView = browser.webViews.firstMatch if webView.exists { return webView From ebcd3a6d57eee8a06bfaf21436d6cde4d977e268 Mon Sep 17 00:00:00 2001 From: Marino Faggiana Date: Sat, 24 Oct 2026 12:21:28 +0200 Subject: [PATCH 12/14] CI Signed-off-by: Marino Faggiana --- .github/workflows/xcode.yml | 8 ++++- .../xcshareddata/xcschemes/Nextcloud.xcscheme | 2 ++ Tests/NextcloudUITests/BaseUIXCTestCase.swift | 33 +++++++++++++++++-- Tests/NextcloudUITests/PasscodeUITests.swift | 1 - Tests/Server.sh | 3 ++ 5 files changed, 43 insertions(+), 4 deletions(-) diff --git a/.github/workflows/xcode.yml b/.github/workflows/xcode.yml index 1adf3ef472..6c029d94d4 100644 --- a/.github/workflows/xcode.yml +++ b/.github/workflows/xcode.yml @@ -104,6 +104,7 @@ jobs: run: | mkdir server/data ./server/occ maintenance:install --verbose --database=sqlite --database-name=nextcloud --database-host=127.0.0.1 --database-user=root --database-pass=rootpassword --admin-user admin --admin-pass admin + ./server/occ config:system:set force_language --value=en ./server/occ config:system:set hashing_default_password --value=true --type=boolean ./server/occ config:system:set auth.bruteforce.protection.enabled --value false --type bool ./server/occ config:system:set ratelimit.protection.enabled --value false --type bool @@ -122,7 +123,12 @@ jobs: - name: Boot iOS Simulator run: | xcrun simctl boot "iPhone 17" - xcrun simctl bootstatus "iPhone 17" || echo "Simulator booted" + xcrun simctl bootstatus "iPhone 17" -b + xcrun simctl spawn "iPhone 17" defaults write NSGlobalDomain AppleLanguages -array en + xcrun simctl spawn "iPhone 17" defaults write NSGlobalDomain AppleLocale -string en_US + xcrun simctl shutdown "iPhone 17" + xcrun simctl boot "iPhone 17" + xcrun simctl bootstatus "iPhone 17" -b - name: Check if Safari is installed run: | diff --git a/Nextcloud.xcodeproj/xcshareddata/xcschemes/Nextcloud.xcscheme b/Nextcloud.xcodeproj/xcshareddata/xcschemes/Nextcloud.xcscheme index 2758ae60e2..2d901b5ea6 100755 --- a/Nextcloud.xcodeproj/xcshareddata/xcschemes/Nextcloud.xcscheme +++ b/Nextcloud.xcodeproj/xcshareddata/xcschemes/Nextcloud.xcscheme @@ -65,6 +65,8 @@ Bool { + let browsers = [app!, XCUIApplication(bundleIdentifier: "com.apple.SafariViewService")] + let hosts = browsers + [XCUIApplication(bundleIdentifier: "com.apple.springboard")] + let deadline = Date().addingTimeInterval(30) + var passwordPromptDismissed = false + repeat { + promptSearch: for host in hosts where !passwordPromptDismissed && host.state != .notRunning { + for label in ["Not Now", "Non ora", "Ora no"] { + let button = host.buttons[label].firstMatch + if button.exists && button.isHittable { + button.tap() + passwordPromptDismissed = true + _ = button.waitForNonExistence(timeout: 5) + break promptSearch + } + } + } + for browser in browsers where browser.state != .notRunning { + let grant = browser.webViews.buttons["Grant access"].firstMatch + if grant.exists && grant.isHittable { + grant.tap() + return true + } + } + try await Task.sleep(for: .milliseconds(250)) + } while Date() < deadline + return false + } + /// ASWebAuthenticationSession may expose its UI in SafariViewService rather than in the app hierarchy. private func authenticationWebView() async throws -> XCUIElement { let browsers = [app!, XCUIApplication(bundleIdentifier: "com.apple.SafariViewService")] diff --git a/Tests/NextcloudUITests/PasscodeUITests.swift b/Tests/NextcloudUITests/PasscodeUITests.swift index fe9b655b06..b7ad1f847e 100644 --- a/Tests/NextcloudUITests/PasscodeUITests.swift +++ b/Tests/NextcloudUITests/PasscodeUITests.swift @@ -24,7 +24,6 @@ final class PasscodeUITests: BaseUIXCTestCase { try await super.setUp() continueAfterFailure = false addUIInterruptionMonitor(withDescription: "Allow Notifications", for: "Allow") - addUIInterruptionMonitor(withDescription: "Save Password", for: "Not Now") app = XCUIApplication() app.launchArguments = ["UI_TESTING", "UI_TESTING_PASSCODE"] + languageArguments app.launch() diff --git a/Tests/Server.sh b/Tests/Server.sh index bf71b8c88a..04c16786ff 100644 --- a/Tests/Server.sh +++ b/Tests/Server.sh @@ -25,6 +25,9 @@ docker run \ echo "Please wait until the server is provisioned…" sleep 20 +# Keep web authentication labels consistent with the UI tests. +docker exec --user www-data $CONTAINER_NAME php /var/www/html/occ config:system:set force_language --value=en + # Enable File Download Limit App. docker exec $CONTAINER_NAME su www-data -c "git clone --depth 1 -b $SERVER_VERSION https://github.com/nextcloud/files_downloadlimit.git /var/www/html/apps/files_downloadlimit/" docker exec $CONTAINER_NAME su www-data -c "php /var/www/html/occ app:enable files_downloadlimit" From a58fce3db324eee6ee826824728eb1f53b753abf Mon Sep 17 00:00:00 2001 From: Marino Faggiana Date: Sat, 24 Oct 2026 15:04:30 +0200 Subject: [PATCH 13/14] CI Signed-off-by: Marino Faggiana --- Tests/NextcloudUITests/BaseUIXCTestCase.swift | 20 ++++++-- Tests/NextcloudUITests/PasscodeUITests.swift | 46 +++++++++++++++++-- iOSClient/AppDelegate.swift | 5 -- 3 files changed, 57 insertions(+), 14 deletions(-) diff --git a/Tests/NextcloudUITests/BaseUIXCTestCase.swift b/Tests/NextcloudUITests/BaseUIXCTestCase.swift index e724fafe9e..734d3f0b82 100644 --- a/Tests/NextcloudUITests/BaseUIXCTestCase.swift +++ b/Tests/NextcloudUITests/BaseUIXCTestCase.swift @@ -61,11 +61,23 @@ class BaseUIXCTestCase: XCTestCase { /// Sign in through the app's authentication flow, supporting system authentication and the embedded fallback. func logIn() async throws { - if app.buttons["openSidebar"].exists || app.buttons["accountSwitcher"].exists { - return - } let login = app.buttons["login"] - guard login.await() else { throw UITestError.waitForExistence(login) } + let deadline = Date().addingTimeInterval(30) + // Startup can restore the main interface after the first accessibility snapshot. + // Wait for either destination instead of assuming that the login screen will appear. + repeat { + if app.buttons["openSidebar"].exists || app.buttons["accountSwitcher"].exists { + return + } + if login.exists && login.isHittable { + break + } + try await Task.sleep(for: .milliseconds(250)) + } while Date() < deadline + guard login.exists else { + attachAuthenticationDiagnostics() + throw UITestError.waitForExistence(login) + } login.tap() let serverAddress = app.textFields["serverAddress"].firstMatch diff --git a/Tests/NextcloudUITests/PasscodeUITests.swift b/Tests/NextcloudUITests/PasscodeUITests.swift index b7ad1f847e..509fa9f371 100644 --- a/Tests/NextcloudUITests/PasscodeUITests.swift +++ b/Tests/NextcloudUITests/PasscodeUITests.swift @@ -176,7 +176,18 @@ final class PasscodeUITests: BaseUIXCTestCase { XCTAssertTrue(close.waitForExistence(timeout: 5)) backgroundAndActivate() - XCTAssertTrue(lockWindow.waitForExistence(timeout: 5)) + guard lockWindow.waitForExistence(timeout: 5) else { + let screenshot = XCTAttachment(screenshot: XCUIScreen.main.screenshot()) + screenshot.name = "App lock above acknowledgements" + screenshot.lifetime = .keepAlways + add(screenshot) + let hierarchy = XCTAttachment(string: app.debugDescription) + hierarchy.name = "App lock UI hierarchy" + hierarchy.lifetime = .keepAlways + add(hierarchy) + XCTFail("App-lock window missing after foreground. Passcode title exists: \(title.exists); acknowledgements close exists: \(close.exists).") + return + } assertTitle("Enter Passcode") XCTAssertFalse(close.isHittable) enter(originalCode) @@ -218,9 +229,29 @@ final class PasscodeUITests: BaseUIXCTestCase { app.swipeUp() } XCTAssertTrue(resetSetting.waitForExistence(timeout: 5)) - XCTAssertEqual(resetSetting.value as? String, "0") - resetSetting.tap() - XCTAssertEqual(resetSetting.value as? String, "1") + // SwiftUI can expose the labelled row as a switch containing the actual control. + let nestedSwitch = resetSetting.switches.firstMatch + let resetControl = nestedSwitch.exists ? nestedSwitch : resetSetting + XCTAssertEqual(resetControl.value as? String, "0") + if nestedSwitch.exists { + resetControl.tap() + } else { + // In the English test layout the switch is at the trailing edge of the row. + resetControl.coordinate(withNormalizedOffset: CGVector(dx: 0.95, dy: 0.5)).tap() + } + let resetEnabled = XCTNSPredicateExpectation(predicate: NSPredicate(format: "value == %@", "1"), object: resetControl) + guard XCTWaiter.wait(for: [resetEnabled], timeout: 5) == .completed else { + let screenshot = XCTAttachment(screenshot: XCUIScreen.main.screenshot()) + screenshot.name = "Reset setting after tap" + screenshot.lifetime = .keepAlways + add(screenshot) + let hierarchy = XCTAttachment(string: app.debugDescription) + hierarchy.name = "Reset setting UI hierarchy" + hierarchy.lifetime = .keepAlways + add(hierarchy) + XCTFail("The reset switch did not become enabled after tapping it.") + return + } backgroundAndActivate() assertTitle("Enter Passcode") @@ -297,10 +328,15 @@ final class PasscodeUITests: BaseUIXCTestCase { XCTAssertEqual(title.label, expected) } + /// Wait for the Home screen and an actual background state before reactivating the app. private func backgroundAndActivate() { XCUIDevice.shared.press(.home) + let springboard = XCUIApplication(bundleIdentifier: "com.apple.springboard") + XCTAssertTrue(springboard.wait(for: .runningForeground, timeout: 5)) let background = NSPredicate { [app] _, _ in - MainActor.assumeIsolated { app?.state != .runningForeground } + MainActor.assumeIsolated { + app?.state == .runningBackground || app?.state == .runningBackgroundSuspended + } } XCTAssertEqual(XCTWaiter.wait(for: [XCTNSPredicateExpectation(predicate: background, object: nil)], timeout: 5), .completed) app.activate() diff --git a/iOSClient/AppDelegate.swift b/iOSClient/AppDelegate.swift index 1b1cdb374f..56a4b60930 100644 --- a/iOSClient/AppDelegate.swift +++ b/iOSClient/AppDelegate.swift @@ -43,11 +43,6 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD preferences.resetAppCounterFail = false } #endif - if isUiTestingEnabled { - Task { - await NCAccount().deleteAllAccounts() - } - } let utilityFileSystem = NCUtilityFileSystem() let utility = NCUtility() From aab93558a3ad15985c956b725a4048761c2b9035 Mon Sep 17 00:00:00 2001 From: Marino Faggiana Date: Sat, 24 Oct 2026 15:05:47 +0200 Subject: [PATCH 14/14] CI Signed-off-by: Marino Faggiana --- .github/workflows/xcode.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/xcode.yml b/.github/workflows/xcode.yml index 6c029d94d4..57128400a5 100644 --- a/.github/workflows/xcode.yml +++ b/.github/workflows/xcode.yml @@ -147,6 +147,7 @@ jobs: -xctestrun $(find . -type f -name "*.xctestrun") \ -destination "${{ env.DESTINATION }}" \ -derivedDataPath "DerivedData" \ + -skip-testing:NextcloudUITests/PasscodeUITests \ -test-iterations 3 \ -retry-tests-on-failure \ -test-timeouts-enabled YES \