diff --git a/TwoFAS/Data/Interactors/InteractorFactory.swift b/TwoFAS/Data/Interactors/InteractorFactory.swift index 0c707a64..bda6d5e9 100644 --- a/TwoFAS/Data/Interactors/InteractorFactory.swift +++ b/TwoFAS/Data/Interactors/InteractorFactory.swift @@ -248,6 +248,9 @@ public final class InteractorFactory { } public func mdmInteractor() -> MDMInteracting { - MDMInteractor(mainRepository: MainRepositoryImpl.shared) + MDMInteractor( + mainRepository: MainRepositoryImpl.shared, + pairingInteractor: pairingWebExtensionInteractor() + ) } } diff --git a/TwoFAS/Data/Interactors/MDMInteractor.swift b/TwoFAS/Data/Interactors/MDMInteractor.swift index f02ac744..b378eb23 100644 --- a/TwoFAS/Data/Interactors/MDMInteractor.swift +++ b/TwoFAS/Data/Interactors/MDMInteractor.swift @@ -23,6 +23,10 @@ public protocol MDMInteracting: AnyObject { var isBackupBlocked: Bool { get } var isBiometryBlocked: Bool { get } var isBrowserExtensionBlocked: Bool { get } + var isLockoutAttemptsChangeBlocked: Bool { get } + var isLockoutBlockTimeChangeBlocked: Bool { get } + var isPasscodeRequried: Bool { get } + var shouldSetPasscode: Bool { get } func apply() } @@ -50,6 +54,22 @@ extension MDMInteractor: MDMInteracting { mainRepository.mdmIsBrowserExtensionBlocked } + var isLockoutAttemptsChangeBlocked: Bool { + mainRepository.mdmLockoutAttepts != nil + } + + var isLockoutBlockTimeChangeBlocked: Bool { + mainRepository.mdmLockoutBlockTime != nil + } + + var isPasscodeRequried: Bool { + mainRepository.mdmIsPasscodeRequried + } + + var shouldSetPasscode: Bool { + isPasscodeRequried && !mainRepository.isPINSet + } + func apply() { if isBackupBlocked && mainRepository.isCloudBackupConnected { mainRepository.clearBackup() @@ -62,5 +82,13 @@ extension MDMInteractor: MDMInteracting { if isBrowserExtensionBlocked && pairingInteractor.hasActiveBrowserExtension { pairingInteractor.disableExtension(completion: { _ in }) } + + if let lockoutAttepts = mainRepository.mdmLockoutAttepts { + mainRepository.setAppLockAttempts(lockoutAttepts) + } + + if let blockTime = mainRepository.mdmLockoutBlockTime { + mainRepository.setAppLockBlockTime(blockTime) + } } } diff --git a/TwoFAS/Data/Interactors/PairingWebExtensionInteractor.swift b/TwoFAS/Data/Interactors/PairingWebExtensionInteractor.swift index d60039f4..06ef6f22 100644 --- a/TwoFAS/Data/Interactors/PairingWebExtensionInteractor.swift +++ b/TwoFAS/Data/Interactors/PairingWebExtensionInteractor.swift @@ -27,6 +27,7 @@ public enum PairingWebExtensionError: Error { case noPublicKey case serverError case noInternet + case blocked } public enum UnparingWebExtensionError: Error { @@ -82,6 +83,11 @@ extension PairingWebExtensionInteractor: PairingWebExtensionInteracting { func pair(with extensionID: ExtensionID, completion: @escaping (Result) -> Void) { Log("PairingWebExtensionInteractor - pair. extensionID: \(extensionID)", module: .interactor) + guard !mainRepository.mdmIsBrowserExtensionBlocked else { + Log("PairingWebExtensionInteractor - pair. Error: blocked!", module: .interactor) + completion(.failure(.blocked)) + return + } guard !mainRepository.listAllPairedExtensions().map({ $0.extensionID }).contains(extensionID) else { Log("PairingWebExtensionInteractor - failure. Already paired", module: .interactor) completion(.failure(.alreadyPaired)) diff --git a/TwoFAS/Data/MainRepository/MDMRepository/MDMRepository.swift b/TwoFAS/Data/MainRepository/MDMRepository/MDMRepository.swift index 2df0115d..2a1e1a00 100644 --- a/TwoFAS/Data/MainRepository/MDMRepository/MDMRepository.swift +++ b/TwoFAS/Data/MainRepository/MDMRepository/MDMRepository.swift @@ -23,4 +23,7 @@ protocol MDMRepository: AnyObject { var isBackupBlocked: Bool { get } var isBiometryBlocked: Bool { get } var isBrowserExtensionBlocked: Bool { get } + var lockoutAttepts: AppLockAttempts? { get } + var lockoutBlockTime: AppLockBlockTime? { get } + var isPasscodeRequried: Bool { get } } diff --git a/TwoFAS/Data/MainRepository/MDMRepository/MDMRepositoryImpl.swift b/TwoFAS/Data/MainRepository/MDMRepository/MDMRepositoryImpl.swift index 0534a26e..68cb599b 100644 --- a/TwoFAS/Data/MainRepository/MDMRepository/MDMRepositoryImpl.swift +++ b/TwoFAS/Data/MainRepository/MDMRepository/MDMRepositoryImpl.swift @@ -19,10 +19,6 @@ import Foundation -//option that passcode is required -//preset lockout settings -//toggle/hide browser extension - final class MDMRepositoryImpl { private let mdmKey = "com.apple.configuration.managed" private let userDefaults = UserDefaults.standard @@ -33,11 +29,15 @@ final class MDMRepositoryImpl { case isBackupBlocked = "blockBackup" case isBiometryBlocked = "blockBiometry" case isBrowserExtensionBlocked = "blockBrowserExtension" + case lockoutAttempts = "lockoutAttempts" + case lockoutBlockTime = "lockoutBlockTime" + case isPasscodeRequried = "requirePasscode" } private let isBackupBlockedDefaultValue = false private let isBiometryBlockedDefaultValue = false private let isBrowserExtensionBlockedDefaultValue = false + private let isPasscodeRequriedDefaultValue = false init() { reload() @@ -69,4 +69,36 @@ extension MDMRepositoryImpl: MDMRepository { } return value } + + var lockoutAttepts: AppLockAttempts? { + guard let value = settings[SettingsKeys.lockoutAttempts.rawValue] as? Int else { + return nil + } + switch value { + case 0: return .noLimit + case 3: return .try3 + case 5: return .try5 + case 10: return .try10 + default: return nil + } + } + + var lockoutBlockTime: AppLockBlockTime? { + guard let value = settings[SettingsKeys.lockoutBlockTime.rawValue] as? Int else { + return nil + } + switch value { + case 3: return .min3 + case 5: return .min5 + case 10: return .min10 + default: return nil + } + } + + var isPasscodeRequried: Bool { + guard let value = settings[SettingsKeys.isPasscodeRequried.rawValue] as? Bool else { + return isPasscodeRequriedDefaultValue + } + return value + } } diff --git a/TwoFAS/Data/MainRepository/MainRepository.swift b/TwoFAS/Data/MainRepository/MainRepository.swift index e7c853ee..2dcbc23c 100644 --- a/TwoFAS/Data/MainRepository/MainRepository.swift +++ b/TwoFAS/Data/MainRepository/MainRepository.swift @@ -476,4 +476,7 @@ protocol MainRepository: AnyObject { var mdmIsBackupBlocked: Bool { get } var mdmIsBiometryBlocked: Bool { get } var mdmIsBrowserExtensionBlocked: Bool { get } + var mdmLockoutAttepts: AppLockAttempts? { get } + var mdmLockoutBlockTime: AppLockBlockTime? { get } + var mdmIsPasscodeRequried: Bool { get } } diff --git a/TwoFAS/Data/MainRepository/MainRepositoryImpl+MDM.swift b/TwoFAS/Data/MainRepository/MainRepositoryImpl+MDM.swift index 8cfd0cb5..a324c459 100644 --- a/TwoFAS/Data/MainRepository/MainRepositoryImpl+MDM.swift +++ b/TwoFAS/Data/MainRepository/MainRepositoryImpl+MDM.swift @@ -31,4 +31,16 @@ extension MainRepositoryImpl { var mdmIsBrowserExtensionBlocked: Bool { mdmRepository.isBrowserExtensionBlocked } + + var mdmLockoutAttepts: AppLockAttempts? { + mdmRepository.lockoutAttepts + } + + var mdmLockoutBlockTime: AppLockBlockTime? { + mdmRepository.lockoutBlockTime + } + + var mdmIsPasscodeRequried: Bool { + mdmRepository.isPasscodeRequried + } } diff --git a/TwoFAS/TwoFAS.xcodeproj/project.pbxproj b/TwoFAS/TwoFAS.xcodeproj/project.pbxproj index 009c7255..9f68f1dd 100644 --- a/TwoFAS/TwoFAS.xcodeproj/project.pbxproj +++ b/TwoFAS/TwoFAS.xcodeproj/project.pbxproj @@ -671,6 +671,7 @@ C2BBD1D42B8113A9009A91FB /* TwoFASWidgetInline.swift in Sources */ = {isa = PBXBuildFile; fileRef = C2BBD1D22B81127C009A91FB /* TwoFASWidgetInline.swift */; }; C2BBD1D72B812117009A91FB /* TwoFASWidgetCircular.swift in Sources */ = {isa = PBXBuildFile; fileRef = C2BBD1D52B8120F4009A91FB /* TwoFASWidgetCircular.swift */; }; C2BBD2382B8130B0009A91FB /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = C2BBD2332B81308C009A91FB /* Localizable.strings */; }; + C2BC3FC92B94E70F004C4BA0 /* NewPINNavigationFlowController.swift in Sources */ = {isa = PBXBuildFile; fileRef = C2BC3FC82B94E70F004C4BA0 /* NewPINNavigationFlowController.swift */; }; C2BD82B2236619E800FBD69A /* BackupAreaWarningView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C2BD82B1236619E800FBD69A /* BackupAreaWarningView.swift */; }; C2BD85DA2640290E0087D087 /* IntroductionFlowController.swift in Sources */ = {isa = PBXBuildFile; fileRef = C2BD85D92640290E0087D087 /* IntroductionFlowController.swift */; }; C2BD85DC26402A910087D087 /* IntroductionContainerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C2BD85DB26402A910087D087 /* IntroductionContainerView.swift */; }; @@ -2291,6 +2292,7 @@ C2BBD24C2B813162009A91FB /* uk */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = uk; path = TwoFAS/Other/uk.lproj/Localizable.strings; sourceTree = SOURCE_ROOT; }; C2BBD24D2B813162009A91FB /* de */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = de; path = TwoFAS/Other/de.lproj/Localizable.strings; sourceTree = SOURCE_ROOT; }; C2BBD24E2B813162009A91FB /* pl */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = pl; path = TwoFAS/Other/pl.lproj/Localizable.strings; sourceTree = SOURCE_ROOT; }; + C2BC3FC82B94E70F004C4BA0 /* NewPINNavigationFlowController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NewPINNavigationFlowController.swift; sourceTree = ""; }; C2BD82B1236619E800FBD69A /* BackupAreaWarningView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BackupAreaWarningView.swift; sourceTree = ""; }; C2BD85D92640290E0087D087 /* IntroductionFlowController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IntroductionFlowController.swift; sourceTree = ""; }; C2BD85DB26402A910087D087 /* IntroductionContainerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IntroductionContainerView.swift; sourceTree = ""; }; @@ -5647,6 +5649,7 @@ C2A7067C276D1A5F00885D79 /* Flow */ = { isa = PBXGroup; children = ( + C2BC3FC82B94E70F004C4BA0 /* NewPINNavigationFlowController.swift */, C2A7067D276D1A7500885D79 /* NewPINFlowController.swift */, C2997AA82454D767006D5943 /* SelectPINLengthController.swift */, ); @@ -9024,6 +9027,7 @@ C2B1208F29D76BD10020281E /* AppearancePresenter+Menu.swift in Sources */, C2A7945127F9016D00E5C641 /* BrowserExtensionEditNameViewController.swift in Sources */, C242B4D12A9BD950005CC1BC /* AddingServiceAdvancedSectionDividerView.swift in Sources */, + C2BC3FC92B94E70F004C4BA0 /* NewPINNavigationFlowController.swift in Sources */, C21B3A1427AF0BC9005C603B /* ColorPickerFlowController.swift in Sources */, C2B39E5929F5BD7800EC31F6 /* TokensCategory.swift in Sources */, C26BCE01281010A000CA6A9A /* SelectServiceModuleInteractor.swift in Sources */, diff --git a/TwoFAS/TwoFAS/Common/Extensions/UIViewController+Extensions.swift b/TwoFAS/TwoFAS/Common/Extensions/UIViewController+Extensions.swift index 0c994457..942d48e8 100644 --- a/TwoFAS/TwoFAS/Common/Extensions/UIViewController+Extensions.swift +++ b/TwoFAS/TwoFAS/Common/Extensions/UIViewController+Extensions.swift @@ -50,6 +50,12 @@ extension UIViewController { definesPresentationContext = true } + func configureAsFullscreenModal() { + modalPresentationStyle = .fullScreen + isModalInPresentation = true + definesPresentationContext = true + } + func setCustomLeftBackButton() { navigationItem.leftBarButtonItem = createCustomLeftBackButton() } diff --git a/TwoFAS/TwoFAS/Interactors/ModuleInteractorFactory.swift b/TwoFAS/TwoFAS/Interactors/ModuleInteractorFactory.swift index 8c13b7fb..fd1fed56 100644 --- a/TwoFAS/TwoFAS/Interactors/ModuleInteractorFactory.swift +++ b/TwoFAS/TwoFAS/Interactors/ModuleInteractorFactory.swift @@ -131,7 +131,8 @@ final class ModuleInteractorFactory { func appLockModuleInteractor() -> AppLockModuleInteracting { AppLockModuleInteractor( - appLockInteractor: InteractorFactory.shared.appLockStateInteractor() + appLockInteractor: InteractorFactory.shared.appLockStateInteractor(), + mdmInteractor: InteractorFactory.shared.mdmInteractor() ) } @@ -143,8 +144,8 @@ final class ModuleInteractorFactory { ) } - func newPINModuleInteractor() -> NewPINModuleInteracting { - NewPINModuleInteractor() + func newPINModuleInteractor(lockNavigation: Bool) -> NewPINModuleInteracting { + NewPINModuleInteractor(lockNavigation: lockNavigation) } func trashModuleInteractor() -> TrashModuleInteracting { @@ -334,7 +335,9 @@ final class ModuleInteractorFactory { newVersionInteractor: InteractorFactory.shared.newVersionInteractor(), networkStatusInteractor: InteractorFactory.shared.networkStatusInteractor(), appInfoInteractor: InteractorFactory.shared.appInfoInteractor(), - rootInteractor: InteractorFactory.shared.rootInteractor() + rootInteractor: InteractorFactory.shared.rootInteractor(), + mdmInteractor: InteractorFactory.shared.mdmInteractor(), + protectionInteractor: InteractorFactory.shared.protectionInteractor() ) } diff --git a/TwoFAS/TwoFAS/Root/Modules/Main/Flow/MainFlowController.swift b/TwoFAS/TwoFAS/Root/Modules/Main/Flow/MainFlowController.swift index 3811ab38..5b59b425 100644 --- a/TwoFAS/TwoFAS/Root/Modules/Main/Flow/MainFlowController.swift +++ b/TwoFAS/TwoFAS/Root/Modules/Main/Flow/MainFlowController.swift @@ -30,6 +30,7 @@ protocol MainFlowControlling: AnyObject { func toSecretSyncError(_ serviceName: String) func toOpenFileImport(url: URL) func toSetupSplit() + func toSetPIN() // MARK: - App update func toShowNewVersionAlert(for appStoreURL: URL, skip: @escaping Callback) @@ -137,6 +138,12 @@ extension MainFlowController: MainFlowControlling { viewController.present(alertController, animated: true, completion: nil) } + + // MARK: - MDM requriments + + func toSetPIN() { + NewPINNavigationFlowController.present(on: viewController, parent: self) + } } extension MainFlowController: ImporterOpenFileHeadlessFlowControllerParent { @@ -201,3 +208,12 @@ extension MainFlowController: MainSplitFlowControllerParent { viewController.presenter.handleSwitchToExternalImport() } } + +extension MainFlowController: NewPINNavigationFlowControllerParent { + func pinGathered(with PIN: String, pinType: PINType) { + viewController.presenter.handleSavePIN(PIN, pinType: pinType) + viewController.dismiss(animated: true) { [weak viewController] in + viewController?.presenter.handleViewIsVisible() + } + } +} diff --git a/TwoFAS/TwoFAS/Root/Modules/Main/Interactor/MainModuleInteractor.swift b/TwoFAS/TwoFAS/Root/Modules/Main/Interactor/MainModuleInteractor.swift index d26673ee..abbea9b6 100644 --- a/TwoFAS/TwoFAS/Root/Modules/Main/Interactor/MainModuleInteractor.swift +++ b/TwoFAS/TwoFAS/Root/Modules/Main/Interactor/MainModuleInteractor.swift @@ -24,11 +24,15 @@ protocol MainModuleInteracting: AnyObject { var secretSyncError: ((String) -> Void)? { get set } var isAppLocked: Bool { get } var isBrowserExtensionAllowed: Bool { get } + var shouldSetPasscode: Bool { get } + func applyMDMRules() func initialize() func checkForImport() -> URL? func clearImportedFileURL() + func savePIN(_ PIN: String, ofType pinType: PINType) + // MARK: - New app version func checkForNewAppVersion(completion: @escaping (URL?) -> Void) func skipAppVersion() @@ -45,6 +49,10 @@ final class MainModuleInteractor { !mdmInteractor.isBrowserExtensionBlocked } + var shouldSetPasscode: Bool { + mdmInteractor.shouldSetPasscode + } + private let logUploadingInteractor: LogUploadingInteracting private let cloudBackupStateInteractor: CloudBackupStateInteracting private let fileInteractor: FileInteracting @@ -53,6 +61,7 @@ final class MainModuleInteractor { private let appInfoInteractor: AppInfoInteracting private let rootInteractor: RootInteracting private let mdmInteractor: MDMInteracting + private let protectionInteractor: ProtectionInteracting init( logUploadingInteractor: LogUploadingInteracting, @@ -63,7 +72,8 @@ final class MainModuleInteractor { networkStatusInteractor: NetworkStatusInteracting, appInfoInteractor: AppInfoInteracting, rootInteractor: RootInteracting, - mdmInteractor: MDMInteracting + mdmInteractor: MDMInteracting, + protectionInteractor: ProtectionInteracting ) { self.logUploadingInteractor = logUploadingInteractor self.cloudBackupStateInteractor = cloudBackupStateInteractor @@ -73,6 +83,7 @@ final class MainModuleInteractor { self.appInfoInteractor = appInfoInteractor self.rootInteractor = rootInteractor self.mdmInteractor = mdmInteractor + self.protectionInteractor = protectionInteractor cloudBackupStateInteractor.secretSyncError = { [weak self] in self?.secretSyncError?($0) } } @@ -93,6 +104,14 @@ extension MainModuleInteractor: MainModuleInteracting { fileInteractor.markAsHandled() } + func applyMDMRules() { + mdmInteractor.apply() + } + + func savePIN(_ PIN: String, ofType pinType: PINType) { + protectionInteractor.savePIN(PIN, typeOfPIN: pinType) + } + // MARK: - New app version func checkForNewAppVersion(completion: @escaping (URL?) -> Void) { diff --git a/TwoFAS/TwoFAS/Root/Modules/Main/Presenter/MainPresenter.swift b/TwoFAS/TwoFAS/Root/Modules/Main/Presenter/MainPresenter.swift index ae34ec1f..17c77c43 100644 --- a/TwoFAS/TwoFAS/Root/Modules/Main/Presenter/MainPresenter.swift +++ b/TwoFAS/TwoFAS/Root/Modules/Main/Presenter/MainPresenter.swift @@ -18,6 +18,7 @@ // import UIKit +import Data final class MainPresenter { weak var view: MainViewControlling? @@ -82,13 +83,20 @@ final class MainPresenter { func handleViewIsVisible() { viewIsVisible() } + + func handleSavePIN(_ PIN: String, pinType: PINType) { + interactor.savePIN(PIN, ofType: pinType) + } } private extension MainPresenter { func viewIsVisible() { guard !interactor.isAppLocked && !handlingViewIsVisible else { return } handlingViewIsVisible = true - if let url = interactor.checkForImport() { + interactor.applyMDMRules() + if interactor.shouldSetPasscode { + flowController.toSetPIN() + } else if let url = interactor.checkForImport() { flowController.toOpenFileImport(url: url) interactor.clearImportedFileURL() handlingViewIsVisible = false diff --git a/TwoFAS/TwoFAS/Root/Modules/Settings/AppLock/Interactor/AppLockModuleInteractor.swift b/TwoFAS/TwoFAS/Root/Modules/Settings/AppLock/Interactor/AppLockModuleInteractor.swift index e9268bb0..c7a1c98c 100644 --- a/TwoFAS/TwoFAS/Root/Modules/Settings/AppLock/Interactor/AppLockModuleInteractor.swift +++ b/TwoFAS/TwoFAS/Root/Modules/Settings/AppLock/Interactor/AppLockModuleInteractor.swift @@ -21,6 +21,9 @@ import Foundation import Data protocol AppLockModuleInteracting: AnyObject { + var isLockoutAttemptsChangeBlocked: Bool { get } + var isLockoutBlockTimeChangeBlocked: Bool { get } + var selectedAttempts: AppLockAttempts { get } var selectedBlockTime: AppLockBlockTime { get } @@ -30,15 +33,19 @@ protocol AppLockModuleInteracting: AnyObject { final class AppLockModuleInteractor { private let appLockInteractor: AppLockStateInteracting + private let mdmInteractor: MDMInteracting - init(appLockInteractor: AppLockStateInteracting) { + init(appLockInteractor: AppLockStateInteracting, mdmInteractor: MDMInteracting) { self.appLockInteractor = appLockInteractor + self.mdmInteractor = mdmInteractor } } extension AppLockModuleInteractor: AppLockModuleInteracting { var selectedAttempts: AppLockAttempts { appLockInteractor.appLockAttempts } var selectedBlockTime: AppLockBlockTime { appLockInteractor.appLockBlockTime } + var isLockoutAttemptsChangeBlocked: Bool { mdmInteractor.isLockoutAttemptsChangeBlocked } + var isLockoutBlockTimeChangeBlocked: Bool { mdmInteractor.isLockoutBlockTimeChangeBlocked } func setAttempts(_ value: AppLockAttempts) { appLockInteractor.setAppLockAttempts(value) diff --git a/TwoFAS/TwoFAS/Root/Modules/Settings/AppLock/Presenter/AppLockModels.swift b/TwoFAS/TwoFAS/Root/Modules/Settings/AppLock/Presenter/AppLockModels.swift index 99afc504..c4eec045 100644 --- a/TwoFAS/TwoFAS/Root/Modules/Settings/AppLock/Presenter/AppLockModels.swift +++ b/TwoFAS/TwoFAS/Root/Modules/Settings/AppLock/Presenter/AppLockModels.swift @@ -34,4 +34,5 @@ struct AppLockMenuSection: TableViewSection { struct AppLockMenuCell: Hashable { let title: String let checkmark: Bool + let disabled: Bool } diff --git a/TwoFAS/TwoFAS/Root/Modules/Settings/AppLock/Presenter/AppLockPresenter+Menu.swift b/TwoFAS/TwoFAS/Root/Modules/Settings/AppLock/Presenter/AppLockPresenter+Menu.swift index f9a42329..41a9cf75 100644 --- a/TwoFAS/TwoFAS/Root/Modules/Settings/AppLock/Presenter/AppLockPresenter+Menu.swift +++ b/TwoFAS/TwoFAS/Root/Modules/Settings/AppLock/Presenter/AppLockPresenter+Menu.swift @@ -27,7 +27,11 @@ extension AppLockPresenter { title: T.Settings.tooManyAttemptsHeader, cells: AppLockAttempts.allCases.map { - AppLockMenuCell(title: $0.localized, checkmark: selectedAttempt == $0) + AppLockMenuCell( + title: $0.localized, + checkmark: selectedAttempt == $0, + disabled: interactor.isLockoutAttemptsChangeBlocked + ) }, footer: T.Settings.howManyAttemptsFooter ) @@ -37,7 +41,11 @@ extension AppLockPresenter { title: T.Settings.blockFor, cells: AppLockBlockTime.allCases.map { - AppLockMenuCell(title: $0.localized, checkmark: selectedBlockTime == $0) + AppLockMenuCell( + title: $0.localized, + checkmark: selectedBlockTime == $0, + disabled: interactor.isLockoutBlockTimeChangeBlocked + ) } ) var menu: [AppLockMenuSection] = [ diff --git a/TwoFAS/TwoFAS/Root/Modules/Settings/AppLock/Presenter/AppLockPresenter.swift b/TwoFAS/TwoFAS/Root/Modules/Settings/AppLock/Presenter/AppLockPresenter.swift index b7f10334..5c6c3d40 100644 --- a/TwoFAS/TwoFAS/Root/Modules/Settings/AppLock/Presenter/AppLockPresenter.swift +++ b/TwoFAS/TwoFAS/Root/Modules/Settings/AppLock/Presenter/AppLockPresenter.swift @@ -37,10 +37,14 @@ final class AppLockPresenter { func handleSelection(at indexPath: IndexPath) { if indexPath.section == 0 { - guard let value = AppLockAttempts.allCases[safe: indexPath.row] else { return } + guard let value = AppLockAttempts.allCases[ + safe: indexPath.row + ], !interactor.isLockoutAttemptsChangeBlocked else { return } interactor.setAttempts(value) } else { - guard let value = AppLockBlockTime.allCases[safe: indexPath.row] else { return } + guard let value = AppLockBlockTime.allCases[ + safe: indexPath.row + ], !interactor.isLockoutBlockTimeChangeBlocked else { return } interactor.setBlockTime(value) } reload() diff --git a/TwoFAS/TwoFAS/Root/Modules/Settings/AppLock/View/AppLockViewController.swift b/TwoFAS/TwoFAS/Root/Modules/Settings/AppLock/View/AppLockViewController.swift index 72ce1274..10131207 100644 --- a/TwoFAS/TwoFAS/Root/Modules/Settings/AppLock/View/AppLockViewController.swift +++ b/TwoFAS/TwoFAS/Root/Modules/Settings/AppLock/View/AppLockViewController.swift @@ -132,6 +132,9 @@ extension AppLockViewController { cell.update(icon: nil, title: data.title, kind: accessory) cell.tintColor = Theme.Colors.Fill.theme cell.selectionStyle = .none + if data.disabled { + cell.disable() + } return cell } diff --git a/TwoFAS/TwoFAS/Root/Modules/Settings/AppSecurity/Flow/AppSecurityFlowController.swift b/TwoFAS/TwoFAS/Root/Modules/Settings/AppSecurity/Flow/AppSecurityFlowController.swift index e54ad45a..ecf3a364 100644 --- a/TwoFAS/TwoFAS/Root/Modules/Settings/AppSecurity/Flow/AppSecurityFlowController.swift +++ b/TwoFAS/TwoFAS/Root/Modules/Settings/AppSecurity/Flow/AppSecurityFlowController.swift @@ -103,13 +103,14 @@ extension AppSecurityFlowController: AppSecurityFlowControlling { on: navi, parent: self, action: newAction, - step: .second(PIN: PIN, pinType: typeOfPIN) + step: .second(PIN: PIN, pinType: typeOfPIN), + lockNavigation: false ) } func toCreatePIN(pinType: PINType) { let navi = navigationControllerForModal() - NewPINFlowController.setRoot(in: navi, parent: self, pinType: pinType) + NewPINFlowController.setRoot(in: navi, parent: self, pinType: pinType, lockNavigation: false) viewController.present(navi, animated: true, completion: nil) } @@ -164,7 +165,13 @@ extension AppSecurityFlowController: VerifyPINFlowControllerParent { dismiss() case .change(let currentPINType): guard let navi = viewController.presentedViewController as? UINavigationController else { return } - NewPINFlowController.push(on: navi, parent: self, action: .change, step: .first(pinType: currentPINType)) + NewPINFlowController.push( + on: navi, + parent: self, + action: .change, + step: .first(pinType: currentPINType), + lockNavigation: false + ) case .authorize: viewController.presenter.handleInitialAutorization() guard let vc = viewController.children.first(where: { $0 is VerifyPINViewController }) else { return } @@ -190,7 +197,7 @@ extension AppSecurityFlowController: NewPINFlowControllerParent { dismiss() } - func pingGathered( + func pinGathered( with PIN: String, pinType: PINType, action: NewPINFlowController.Action, diff --git a/TwoFAS/TwoFAS/Root/Modules/Settings/AppSecurity/Interactor/AppSecurityModuleInteractor.swift b/TwoFAS/TwoFAS/Root/Modules/Settings/AppSecurity/Interactor/AppSecurityModuleInteractor.swift index 4dbae6c1..f37e67ce 100644 --- a/TwoFAS/TwoFAS/Root/Modules/Settings/AppSecurity/Interactor/AppSecurityModuleInteractor.swift +++ b/TwoFAS/TwoFAS/Root/Modules/Settings/AppSecurity/Interactor/AppSecurityModuleInteractor.swift @@ -27,6 +27,7 @@ protocol AppSecurityModuleInteracting: AnyObject { var isBiometryEnabled: Bool { get } var isBiometryAllowed: Bool { get } var currentPINType: PINType { get } + var isPasscodeRequried: Bool { get } func toggleBiometry() @@ -62,6 +63,7 @@ extension AppSecurityModuleInteractor: AppSecurityModuleInteracting { var isBiometryEnabled: Bool { protectionInteractor.isBiometryEnabled } var isBiometryAllowed: Bool { !mdmInteractor.isBiometryBlocked } var currentPINType: PINType { protectionInteractor.pinType ?? .digits4 } + var isPasscodeRequried: Bool { mdmInteractor.isPasscodeRequried } func toggleBiometry() { guard protectionInteractor.isBiometryAvailable else { return } diff --git a/TwoFAS/TwoFAS/Root/Modules/Settings/AppSecurity/Presenter/AppSecurityModels.swift b/TwoFAS/TwoFAS/Root/Modules/Settings/AppSecurity/Presenter/AppSecurityModels.swift index 0a73bfef..538a02ff 100644 --- a/TwoFAS/TwoFAS/Root/Modules/Settings/AppSecurity/Presenter/AppSecurityModels.swift +++ b/TwoFAS/TwoFAS/Root/Modules/Settings/AppSecurity/Presenter/AppSecurityModels.swift @@ -35,6 +35,7 @@ struct AppSecurityMenuCell: Hashable { struct Toggle: Hashable { let kind: ToggleKind let isOn: Bool + let isBlocked: Bool } // swiftlint:disable discouraged_none_name diff --git a/TwoFAS/TwoFAS/Root/Modules/Settings/AppSecurity/Presenter/AppSecurityPresenter+Menu.swift b/TwoFAS/TwoFAS/Root/Modules/Settings/AppSecurity/Presenter/AppSecurityPresenter+Menu.swift index f420a504..b64622eb 100644 --- a/TwoFAS/TwoFAS/Root/Modules/Settings/AppSecurity/Presenter/AppSecurityPresenter+Menu.swift +++ b/TwoFAS/TwoFAS/Root/Modules/Settings/AppSecurity/Presenter/AppSecurityPresenter+Menu.swift @@ -23,9 +23,10 @@ import Data extension AppSecurityPresenter { func buildMenu() -> [AppSecurityMenuSection] { let isPINset = interactor.isPINSet + let PINcell = AppSecurityMenuCell( title: T.Settings.pinCode, - accessory: .toggle(toggle: .init(kind: .PIN, isOn: isPINset)) + accessory: .toggle(toggle: .init(kind: .PIN, isOn: isPINset, isBlocked: interactor.isPasscodeRequried)) ) guard isPINset else { @@ -57,7 +58,7 @@ extension AppSecurityPresenter { let section = AppSecurityMenuSection(title: T.Settings.biometricAuthentication, cells: [ AppSecurityMenuCell( title: biometryType.localized, - accessory: .toggle(toggle: .init(kind: .biometry, isOn: isBiometryEnabled)) + accessory: .toggle(toggle: .init(kind: .biometry, isOn: isBiometryEnabled, isBlocked: false)) ) ]) switch biometryType { diff --git a/TwoFAS/TwoFAS/Root/Modules/Settings/AppSecurity/View/AppSecurityViewController.swift b/TwoFAS/TwoFAS/Root/Modules/Settings/AppSecurity/View/AppSecurityViewController.swift index c536cb1f..db62ac2e 100644 --- a/TwoFAS/TwoFAS/Root/Modules/Settings/AppSecurity/View/AppSecurityViewController.swift +++ b/TwoFAS/TwoFAS/Root/Modules/Settings/AppSecurity/View/AppSecurityViewController.swift @@ -151,6 +151,12 @@ extension AppSecurityViewController { } else { cell.selectionStyle = .none } + + let accessory = data.accessory + if case let .toggle(toggle) = accessory, toggle.isBlocked { + cell.disable() + cell.disableToggle() + } return cell } @@ -194,7 +200,7 @@ extension AppSecurityViewController { case .arrow: return .arrow case .info(let text): return .infoArrow(text: text) case .toggle(let toggle): return .toggle(isEnabled: toggle.isOn) { [weak tableView, weak self] calledCell, _ in - guard let indexPath = tableView?.indexPath(for: calledCell) else { return } + guard let indexPath = tableView?.indexPath(for: calledCell), !toggle.isBlocked else { return } self?.presenter.handleToggle(for: indexPath) } } diff --git a/TwoFAS/TwoFAS/Root/Modules/Settings/Menu/Presenter/SettingsMenuPresenter+Menu.swift b/TwoFAS/TwoFAS/Root/Modules/Settings/Menu/Presenter/SettingsMenuPresenter+Menu.swift index d40b14f7..2ef8f8de 100644 --- a/TwoFAS/TwoFAS/Root/Modules/Settings/Menu/Presenter/SettingsMenuPresenter+Menu.swift +++ b/TwoFAS/TwoFAS/Root/Modules/Settings/Menu/Presenter/SettingsMenuPresenter+Menu.swift @@ -145,7 +145,7 @@ extension SettingsMenuPresenter { menu.append(contentsOf: [ backup, - security, + security ]) if interactor.isBrowserExtensionAllowed { diff --git a/TwoFAS/TwoFAS/Root/Modules/Settings/NewPIN/Flow/NewPINFlowController.swift b/TwoFAS/TwoFAS/Root/Modules/Settings/NewPIN/Flow/NewPINFlowController.swift index cc56a810..0624020f 100644 --- a/TwoFAS/TwoFAS/Root/Modules/Settings/NewPIN/Flow/NewPINFlowController.swift +++ b/TwoFAS/TwoFAS/Root/Modules/Settings/NewPIN/Flow/NewPINFlowController.swift @@ -22,7 +22,7 @@ import Data protocol NewPINFlowControllerParent: AnyObject { func hideNewPIN() - func pingGathered( + func pinGathered( with PIN: String, pinType: PINType, action: NewPINFlowController.Action, @@ -52,14 +52,15 @@ final class NewPINFlowController: FlowController { static func setRoot( in navigationController: UINavigationController, parent: NewPINFlowControllerParent, - pinType: PINType + pinType: PINType, + lockNavigation: Bool ) { let view = NewPINViewController() let flowController = NewPINFlowController(viewController: view) flowController.parent = parent flowController.action = .create flowController.step = .first(pinType: pinType) - let interactor = ModuleInteractorFactory.shared.newPINModuleInteractor() + let interactor = ModuleInteractorFactory.shared.newPINModuleInteractor(lockNavigation: lockNavigation) interactor.selectedPINType = pinType let presenter = NewPINPresenter( @@ -80,7 +81,8 @@ final class NewPINFlowController: FlowController { on navigationController: UINavigationController, parent: NewPINFlowControllerParent, action: Action, - step: Step + step: Step, + lockNavigation: Bool ) { let view = NewPINViewController() let flowController = NewPINFlowController(viewController: view) @@ -101,7 +103,7 @@ final class NewPINFlowController: FlowController { selectedPINType = pinType } - let interactor = ModuleInteractorFactory.shared.newPINModuleInteractor() + let interactor = ModuleInteractorFactory.shared.newPINModuleInteractor(lockNavigation: lockNavigation) interactor.selectedPIN = selectedPIN interactor.selectedPINType = selectedPINType @@ -130,7 +132,7 @@ extension NewPINFlowController: NewPINFlowControlling { func toPINGathered(with PIN: String, pinType: PINType) { guard let action, let step else { return } - parent?.pingGathered(with: PIN, pinType: pinType, action: action, step: step) + parent?.pinGathered(with: PIN, pinType: pinType, action: action, step: step) } func toChangePINType() { diff --git a/TwoFAS/TwoFAS/Root/Modules/Settings/NewPIN/Flow/NewPINNavigationFlowController.swift b/TwoFAS/TwoFAS/Root/Modules/Settings/NewPIN/Flow/NewPINNavigationFlowController.swift new file mode 100644 index 00000000..665da5a5 --- /dev/null +++ b/TwoFAS/TwoFAS/Root/Modules/Settings/NewPIN/Flow/NewPINNavigationFlowController.swift @@ -0,0 +1,76 @@ +// +// This file is part of the 2FAS iOS app (https://github.com/twofas/2fas-ios) +// Copyright © 2024 Two Factor Authentication Service, Inc. +// Contributed by Zbigniew Cisiński. All rights reserved. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see +// + +import UIKit +import Data + +protocol NewPINNavigationFlowControllerParent: AnyObject { + func pinGathered( + with PIN: String, + pinType: PINType + ) +} + +protocol NewPINNavigationFlowControlling: AnyObject {} + +final class NewPINNavigationFlowController: NavigationFlowController { + private weak var parent: NewPINNavigationFlowControllerParent? + + static func present( + on viewController: UIViewController, + parent: NewPINNavigationFlowControllerParent + ) { + let flowController = NewPINNavigationFlowController() + flowController.parent = parent + + let navi = CommonNavigationControllerFlow(flowController: flowController) + navi.configureAsFullscreenModal() + + flowController.navigationController = navi + + NewPINFlowController.setRoot(in: navi, parent: flowController, pinType: .digits6, lockNavigation: true) + + viewController.present(navi, animated: true, completion: nil) + } +} + +extension NewPINNavigationFlowController: NewPINNavigationFlowControlling {} + +extension NewPINNavigationFlowController: NewPINFlowControllerParent { + func hideNewPIN() {} + func pinGathered( + with PIN: String, + pinType: PINType, + action: NewPINFlowController.Action, + step: NewPINFlowController.Step + ) { + switch step { + case .first: + NewPINFlowController.push( + on: navigationController, + parent: self, + action: .create, + step: .second(PIN: PIN, pinType: pinType), + lockNavigation: true + ) + case .second(let PIN, let pinType): + parent?.pinGathered(with: PIN, pinType: pinType) + } + } +} diff --git a/TwoFAS/TwoFAS/Root/Modules/Settings/NewPIN/Interactor/NewPINModuleInteractor.swift b/TwoFAS/TwoFAS/Root/Modules/Settings/NewPIN/Interactor/NewPINModuleInteractor.swift index 8b808a48..5f6c1c6e 100644 --- a/TwoFAS/TwoFAS/Root/Modules/Settings/NewPIN/Interactor/NewPINModuleInteractor.swift +++ b/TwoFAS/TwoFAS/Root/Modules/Settings/NewPIN/Interactor/NewPINModuleInteractor.swift @@ -24,6 +24,8 @@ protocol NewPINModuleInteracting: AnyObject { var selectedPIN: String? { get set } var selectedPINType: PINType? { get set } + var lockNavigation: Bool { get } + var pinType: PINType { get } func validatePIN(_ PIN: String) -> Bool @@ -31,8 +33,15 @@ protocol NewPINModuleInteracting: AnyObject { } final class NewPINModuleInteractor { + let lockNavigation: Bool var selectedPIN: String? var selectedPINType: PINType? + + init(lockNavigation: Bool, selectedPIN: String? = nil, selectedPINType: PINType? = nil) { + self.lockNavigation = lockNavigation + self.selectedPIN = selectedPIN + self.selectedPINType = selectedPINType + } } extension NewPINModuleInteractor: NewPINModuleInteracting { diff --git a/TwoFAS/TwoFAS/Root/Modules/Settings/NewPIN/Presenter/NewPINPresenter.swift b/TwoFAS/TwoFAS/Root/Modules/Settings/NewPIN/Presenter/NewPINPresenter.swift index 7cf90b7c..f41951d0 100644 --- a/TwoFAS/TwoFAS/Root/Modules/Settings/NewPIN/Presenter/NewPINPresenter.swift +++ b/TwoFAS/TwoFAS/Root/Modules/Settings/NewPIN/Presenter/NewPINPresenter.swift @@ -37,6 +37,12 @@ final class NewPINPresenter: PINKeyboardPresenter { codeLength = interactor.pinType.digits } + func viewDidLoad() { + if !interactor.lockNavigation { + view?.showCancelButton() + } + } + override func viewWillAppear() { super.viewWillAppear() diff --git a/TwoFAS/TwoFAS/Root/Modules/Settings/NewPIN/View/NewPINViewController.swift b/TwoFAS/TwoFAS/Root/Modules/Settings/NewPIN/View/NewPINViewController.swift index cd1c59ff..c57057e1 100644 --- a/TwoFAS/TwoFAS/Root/Modules/Settings/NewPIN/View/NewPINViewController.swift +++ b/TwoFAS/TwoFAS/Root/Modules/Settings/NewPIN/View/NewPINViewController.swift @@ -21,6 +21,7 @@ import UIKit protocol NewPINViewControlling: AnyObject { func setTitle(_ title: String) + func showCancelButton() } final class NewPINViewController: PINKeyboardViewController { @@ -28,13 +29,8 @@ final class NewPINViewController: PINKeyboardViewController { override func viewDidLoad() { super.viewDidLoad() - - navigationItem.leftBarButtonItem = UIBarButtonItem( - title: T.Commons.cancel, - style: .plain, - target: self, - action: #selector(cancelAction) - ) + navigationItem.backButtonDisplayMode = .minimal + presenter.viewDidLoad() } override func deleteButtonTap() { @@ -69,4 +65,13 @@ extension NewPINViewController: NewPINViewControlling { func setTitle(_ title: String) { self.title = title } + + func showCancelButton() { + navigationItem.leftBarButtonItem = UIBarButtonItem( + title: T.Commons.cancel, + style: .plain, + target: self, + action: #selector(cancelAction) + ) + } }