diff --git a/PasscodeLock.xcodeproj/project.pbxproj b/PasscodeLock.xcodeproj/project.pbxproj index efd84a3e..3579d067 100644 --- a/PasscodeLock.xcodeproj/project.pbxproj +++ b/PasscodeLock.xcodeproj/project.pbxproj @@ -58,6 +58,8 @@ C9DC08161B90DF4E007A4DD0 /* PasscodeLockViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = C9DC08151B90DF4E007A4DD0 /* PasscodeLockViewController.swift */; }; DE6F8E1C1C24BF7500D3EFCF /* LockSplashView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DE6F8E1B1C24BF7500D3EFCF /* LockSplashView.swift */; }; DE6F8E1E1C24C09400D3EFCF /* CustomPasscodeLockPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = DE6F8E1D1C24C09400D3EFCF /* CustomPasscodeLockPresenter.swift */; }; + DEC461451D0B4E0A008673A0 /* CustomPasscodeLockViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DEC461441D0B4E0A008673A0 /* CustomPasscodeLockViewController.swift */; }; + DEC461471D0B4FCD008673A0 /* PasscodeLockView.xib in Resources */ = {isa = PBXBuildFile; fileRef = DEC461461D0B4FCD008673A0 /* PasscodeLockView.xib */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -154,6 +156,8 @@ C9DC08151B90DF4E007A4DD0 /* PasscodeLockViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PasscodeLockViewController.swift; sourceTree = ""; }; DE6F8E1B1C24BF7500D3EFCF /* LockSplashView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LockSplashView.swift; sourceTree = ""; }; DE6F8E1D1C24C09400D3EFCF /* CustomPasscodeLockPresenter.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CustomPasscodeLockPresenter.swift; sourceTree = ""; }; + DEC461441D0B4E0A008673A0 /* CustomPasscodeLockViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CustomPasscodeLockViewController.swift; sourceTree = ""; }; + DEC461461D0B4FCD008673A0 /* PasscodeLockView.xib */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.xib; path = PasscodeLockView.xib; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -263,6 +267,8 @@ C9D3DF431B91B9CD008561EB /* UserDefaultsPasscodeRepository.swift */, C9D3DF451B91BD0E008561EB /* PasscodeLockConfiguration.swift */, DE6F8E1D1C24C09400D3EFCF /* CustomPasscodeLockPresenter.swift */, + DEC461441D0B4E0A008673A0 /* CustomPasscodeLockViewController.swift */, + DEC461461D0B4FCD008673A0 /* PasscodeLockView.xib */, ); path = PasscodeLockDemo; sourceTree = ""; @@ -521,6 +527,7 @@ C9D3DF1D1B91AD11008561EB /* LaunchScreen.storyboard in Resources */, C9D3DF1A1B91AD11008561EB /* Assets.xcassets in Resources */, C9D3DF181B91AD11008561EB /* Main.storyboard in Resources */, + DEC461471D0B4FCD008673A0 /* PasscodeLockView.xib in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -598,6 +605,7 @@ C9D3DF461B91BD0E008561EB /* PasscodeLockConfiguration.swift in Sources */, C9D3DF151B91AD11008561EB /* PasscodeSettingsViewController.swift in Sources */, DE6F8E1E1C24C09400D3EFCF /* CustomPasscodeLockPresenter.swift in Sources */, + DEC461451D0B4E0A008673A0 /* CustomPasscodeLockViewController.swift in Sources */, C9D3DF131B91AD11008561EB /* AppDelegate.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; diff --git a/PasscodeLock/PasscodeLock/EnterPasscodeState.swift b/PasscodeLock/PasscodeLock/EnterPasscodeState.swift index 60f58bc8..6c536b9e 100644 --- a/PasscodeLock/PasscodeLock/EnterPasscodeState.swift +++ b/PasscodeLock/PasscodeLock/EnterPasscodeState.swift @@ -17,7 +17,7 @@ struct EnterPasscodeState: PasscodeLockStateType { let isCancellableAction: Bool var isTouchIDAllowed = true - private var inccorectPasscodeAttempts = 0 + private var incorrectPasscodeAttempts = 0 private var isNotificationSent = false init(allowCancellation: Bool = false) { @@ -35,13 +35,15 @@ struct EnterPasscodeState: PasscodeLockStateType { if passcode == currentPasscode { + incorrectPasscodeAttempts = 0 + lock.delegate?.passcodeLockDidSucceed(lock) } else { - inccorectPasscodeAttempts += 1 + incorrectPasscodeAttempts += 1 - if inccorectPasscodeAttempts >= lock.configuration.maximumInccorectPasscodeAttempts { + if incorrectPasscodeAttempts >= lock.configuration.maximumInccorectPasscodeAttempts { postNotification() } diff --git a/PasscodeLock/Views/PasscodeLockView.xib b/PasscodeLock/Views/PasscodeLockView.xib index c17dae99..28464fea 100644 --- a/PasscodeLock/Views/PasscodeLockView.xib +++ b/PasscodeLock/Views/PasscodeLockView.xib @@ -1,8 +1,8 @@ - + - + diff --git a/PasscodeLockDemo/CustomPasscodeLockPresenter.swift b/PasscodeLockDemo/CustomPasscodeLockPresenter.swift index 04914a00..f7bfb6d1 100644 --- a/PasscodeLockDemo/CustomPasscodeLockPresenter.swift +++ b/PasscodeLockDemo/CustomPasscodeLockPresenter.swift @@ -2,6 +2,10 @@ // CustomPasscodeLockPresenter.swift // PasscodeLock // +// Adds: +// * Splash view +// * Handling of IncorrectPasscodeAttempts +// // Created by Chris Ziogas on 19/12/15. // Copyright © 2015 Yanko Dimitrov. All rights reserved. // @@ -9,14 +13,16 @@ import Foundation import PasscodeLock +// this constant is set here so as it can be used from CustomPasscodeLockViewController +// it is not the best practice, but it set like this for demo purposes +let UserDefaultsIncorrectPasscodeAttemptsReachedName = "passcode.incorrectPasscodeAttemptsReached" + class CustomPasscodeLockPresenter: PasscodeLockPresenter { private let notificationCenter: NSNotificationCenter private let splashView: UIView - var isFreshAppLaunch = true - init(mainWindow window: UIWindow?, configuration: PasscodeLockConfigurationType) { notificationCenter = NSNotificationCenter.defaultCenter() @@ -24,29 +30,36 @@ class CustomPasscodeLockPresenter: PasscodeLockPresenter { splashView = LockSplashView() // TIP: you can set your custom viewController that has added functionality in a custom .xib too - let passcodeLockVC = PasscodeLockViewController(state: .EnterPasscode, configuration: configuration) + let passcodeLockVC = CustomPasscodeLockViewController(state: .EnterPasscode, configuration: configuration) super.init(mainWindow: window, configuration: configuration, viewController: passcodeLockVC) // add notifications observers notificationCenter.addObserver( self, - selector: "applicationDidLaunched", + selector: #selector(self.applicationDidLaunched), name: UIApplicationDidFinishLaunchingNotification, object: nil ) notificationCenter.addObserver( self, - selector: "applicationDidEnterBackground", + selector: #selector(self.applicationDidEnterBackground), name: UIApplicationDidEnterBackgroundNotification, object: nil ) notificationCenter.addObserver( self, - selector: "applicationDidBecomeActive", - name: UIApplicationDidBecomeActiveNotification, + selector: #selector(self.applicationWillEnterForeground), + name: UIApplicationWillEnterForegroundNotification, + object: nil + ) + + notificationCenter.addObserver( + self, + selector: #selector(self.incorrectPasscodeAttemptsReached), + name: PasscodeLockIncorrectPasscodeNotification, object: nil ) } @@ -58,13 +71,7 @@ class CustomPasscodeLockPresenter: PasscodeLockPresenter { dynamic func applicationDidLaunched() -> Void { - // start the Pin Lock presenter - passcodeLockVC.successCallback = { [weak self] _ in - - // we can set isFreshAppLaunch to false - self?.isFreshAppLaunch = false - } - + // show the Pin Lock presenter presentPasscodeLock() } @@ -77,10 +84,28 @@ class CustomPasscodeLockPresenter: PasscodeLockPresenter { addSplashView() } - dynamic func applicationDidBecomeActive() -> Void { + dynamic func applicationWillEnterForeground() -> Void { // remove splashView for iOS app background swithcer removeSplashView() + + // TODO: handle how user will continue + if NSUserDefaults.standardUserDefaults().boolForKey(UserDefaultsIncorrectPasscodeAttemptsReachedName), + let customPasscodeLockVC = passcodeLockVC as? CustomPasscodeLockViewController + { + customPasscodeLockVC.showLockedAlert() + } + } + + dynamic func incorrectPasscodeAttemptsReached() -> Void { + + // store that incorrect passcode attempts has reached + // so as app knows even if app is being force-quited + NSUserDefaults.standardUserDefaults().setBool(true, forKey: UserDefaultsIncorrectPasscodeAttemptsReachedName) + + if let customPasscodeLockVC = passcodeLockVC as? CustomPasscodeLockViewController { + customPasscodeLockVC.showLockedAlert() + } } private func addSplashView() { diff --git a/PasscodeLockDemo/CustomPasscodeLockViewController.swift b/PasscodeLockDemo/CustomPasscodeLockViewController.swift new file mode 100644 index 00000000..7f90e685 --- /dev/null +++ b/PasscodeLockDemo/CustomPasscodeLockViewController.swift @@ -0,0 +1,175 @@ +// +// CustomPasscodeLockViewController.swift +// PasscodeLock +// +// Adds: +// * Exponential backoff after a failed PIN entry +// +// Created by Chris Ziogas on 10/06/16. +// Copyright © 2015 Yanko Dimitrov. All rights reserved. +// + +import UIKit +import PasscodeLock + +class CustomPasscodeLockViewController: PasscodeLockViewController { + + @IBOutlet var buttons: [PasscodeSignButton] = [PasscodeSignButton]() + + private let defaults = NSUserDefaults.standardUserDefaults() + var UserDefaultsPINExponentialBackoffName = "PasscodeLock.PINExponentialBackoffDict" + + // is the max time a user can be blocked + let exponentialBackoffMaxTime = 60.0 * 60.0 // secs * mins + + // is the Base for exponential backoff + var exponentialBackoffTimeBase = 2.0 + + // this is actually the number of retries + var exponentialBackoffTimePower = 0.0 + + override func viewDidAppear(animated: Bool) { + super.viewDidAppear(animated) + + // check if user is blocked from entering a PIN because + // of wrong attemps and exponential backoff + let now = NSDate() + + if let backoffDict = defaults.objectForKey(UserDefaultsPINExponentialBackoffName) as? [String: AnyObject], + + // get until what time user is blocked + let backoffUntilDate = backoffDict["until"] as? NSDate + where backoffUntilDate.compare(now) == NSComparisonResult.OrderedDescending, + + // get number of wrong retries, so as we can calculate the backoff time + // on the next wrong attempt + let backoffNumberOfRetries = backoffDict["numberOfRetries"] as? Double + { + // set exponentialBackoffTimePower for next failed attempt + exponentialBackoffTimePower = backoffNumberOfRetries + + // stop user interactions + togglePinEntry(false) + + // unblock user input, after backoffUntilDate + unblockUserInputAfterSeconds(backoffUntilDate.timeIntervalSinceNow) + + } else if defaults.boolForKey(UserDefaultsIncorrectPasscodeAttemptsReachedName) { + // TODO: handle how user will continue + showLockedAlert() + } + } + + override func passcodeLock(lock: PasscodeLockType, addedSignAtIndex index: Int) { + super.passcodeLock(lock, addedSignAtIndex: index) + + deleteSignButton?.hidden = false + } + + override func passcodeLock(lock: PasscodeLockType, removedSignAtIndex index: Int) { + super.passcodeLock(lock, removedSignAtIndex: index) + + if index == 0 { + deleteSignButton?.hidden = true + } + } + + // MARK: - PasscodeLockDelegate + override func passcodeLockDidSucceed(lock: PasscodeLockType) { + super.passcodeLockDidSucceed(lock) + + // hide delete button + deleteSignButton?.hidden = true + + // reset exponential backoff time + exponentialBackoffTimePower = 0.0 + defaults.removeObjectForKey(UserDefaultsPINExponentialBackoffName) + + // start user interactions + self.togglePinEntry(true) + } + + override func passcodeLockDidFail(lock: PasscodeLockType) { + super.passcodeLockDidFail(lock) + + // hide delete button + deleteSignButton?.hidden = true + + // stop user interactions + togglePinEntry(false) + + // using exponential backoff re-enable user-interactions after some seconds + let backoffTime = pow(exponentialBackoffTimeBase, exponentialBackoffTimePower) + + // persist value in NSUserDefaults so as if user force-quits the app + // we know that user was blocked + let backoffUntil = NSDate().dateByAddingTimeInterval(backoffTime) + let backoffDict = [ + "until": backoffUntil, + "numberOfRetries": exponentialBackoffTimePower + ] + defaults.setObject(backoffDict, forKey: UserDefaultsPINExponentialBackoffName) + + // unblock user input + unblockUserInputAfterSeconds(backoffTime) + } + + func unblockUserInputAfterSeconds(seconds: Double) { + + delay(min(seconds, exponentialBackoffMaxTime)) { + + // start user interactions + self.togglePinEntry(true) + + self.exponentialBackoffTimePower += 1.0 + } + } + + func togglePinEntry(enabled: Bool) { + + // toggle buttons enabled + for button in buttons { + button.enabled = enabled + + // make the button transparent when disabled + button.alpha = enabled ? 1.0 : 0.5 + } + + // toggle user interactions, which will limit more things + // at the moment we prefer to continue allowing login with TouchID + // self.view.userInteractionEnabled = enabled + } + + // for demo purposes + func showLockedAlert() { + + // stop user interactions + togglePinEntry(false) + + let alertController = UIAlertController(title: "You have been locked out", message: "", preferredStyle: .Alert) + + let giveAccessAction = UIAlertAction(title: "Let me in", style: .Default, handler: { [weak self] action in + + // unset that incorect password attempts has been reached + self?.defaults.removeObjectForKey(UserDefaultsIncorrectPasscodeAttemptsReachedName) + + // start user interactions + self?.togglePinEntry(true) + }) + alertController.addAction(giveAccessAction) + + let okAction = UIAlertAction(title: "OK", style: .Default, handler: nil) + alertController.addAction(okAction) + + presentViewController(alertController, animated: true, completion: nil) + } + + func delay(delay:Double, closure:()->()) { + dispatch_after( + dispatch_time( + DISPATCH_TIME_NOW, + Int64(delay * Double(NSEC_PER_SEC)) + ), + dispatch_get_main_queue(), closure) + } +} \ No newline at end of file diff --git a/PasscodeLockDemo/PasscodeLockConfiguration.swift b/PasscodeLockDemo/PasscodeLockConfiguration.swift index 5a176bd7..cce490c0 100644 --- a/PasscodeLockDemo/PasscodeLockConfiguration.swift +++ b/PasscodeLockDemo/PasscodeLockConfiguration.swift @@ -15,7 +15,7 @@ struct PasscodeLockConfiguration: PasscodeLockConfigurationType { let passcodeLength = 4 var isTouchIDAllowed = true let shouldRequestTouchIDImmediately = true - let maximumInccorectPasscodeAttempts = -1 + let maximumInccorectPasscodeAttempts = 4 // use -1 to disable this feature init(repository: PasscodeRepositoryType) { diff --git a/PasscodeLockDemo/PasscodeLockView.xib b/PasscodeLockDemo/PasscodeLockView.xib new file mode 100644 index 00000000..a3822740 --- /dev/null +++ b/PasscodeLockDemo/PasscodeLockView.xib @@ -0,0 +1,458 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/PasscodeLockDemo/PasscodeSettingsViewController.swift b/PasscodeLockDemo/PasscodeSettingsViewController.swift index 36ac6123..41e29372 100644 --- a/PasscodeLockDemo/PasscodeSettingsViewController.swift +++ b/PasscodeLockDemo/PasscodeSettingsViewController.swift @@ -53,15 +53,15 @@ class PasscodeSettingsViewController: UIViewController { @IBAction func passcodeSwitchValueChange(sender: UISwitch) { - let passcodeVC: PasscodeLockViewController + let passcodeVC: CustomPasscodeLockViewController if passcodeSwitch.on { - passcodeVC = PasscodeLockViewController(state: .SetPasscode, configuration: configuration) + passcodeVC = CustomPasscodeLockViewController(state: .SetPasscode, configuration: configuration) } else { - passcodeVC = PasscodeLockViewController(state: .RemovePasscode, configuration: configuration) + passcodeVC = CustomPasscodeLockViewController(state: .RemovePasscode, configuration: configuration) passcodeVC.successCallback = { lock in @@ -77,7 +77,7 @@ class PasscodeSettingsViewController: UIViewController { let repo = UserDefaultsPasscodeRepository() let config = PasscodeLockConfiguration(repository: repo) - let passcodeLock = PasscodeLockViewController(state: .ChangePasscode, configuration: config) + let passcodeLock = CustomPasscodeLockViewController(state: .ChangePasscode, configuration: config) presentViewController(passcodeLock, animated: true, completion: nil) }