We have a requirement where we have to block the user for 24 hours if Face ID or Touch ID fails for 5 consecutive time.
In Face ID, I am using the fallback to call authentication manager again, and after total of 6 attempts (2 in each call), I block the user in the fallback method itself.
In Touch ID, on third failure, I get a call back to Authentication failed. I call authentication manager again, and on 5th try, I get a call back to Lockout in which I can block the user.
Is there any way common in both Face ID and Touch ID where I can get a callback after every single individual failure so that I can block the user on 5th failure itself?
//MARK:- Check if user has valid biometry, if yes, validate, else, show error
fileprivate func biometricAuthentication(completion: #escaping ((Bool) -> ())){
// addBlurredBackground()
//Check if device have Biometric sensor
if biometryRetryCount == 2 {
authenticationContext.localizedFallbackTitle = "Ok"
} else {
authenticationContext.localizedFallbackTitle = "Retry"
}
let isValidSensor : Bool = authenticationContext.canEvaluatePolicy(.deviceOwnerAuthenticationWithBiometrics, error: &error)
if isValidSensor {
//Device have BiometricSensor
authenticationContext.evaluatePolicy(
.deviceOwnerAuthenticationWithBiometrics,
localizedReason: biometryRetryCount == 2 ? "You have been blocked from using the application for next 24 hours. Please come back later" : "Touch / Face ID authentication",
reply: { [unowned self] (success, error) -> Void in
if(success) {
// Touch or Face ID recognized
// self.removeBlurredBackground()
completion(true)
} else {
//If not recognized then
if let error = error {
let msgAndAction = self.errorMessage(errorCode: error._code)
if msgAndAction.0 != "" {
UIApplication.topViewController()?.showAlert(withTitle: "Error", andMessage: msgAndAction.0, andActions: msgAndAction.1)
}
}
completion(false)
}
})
} else {
let msgAndAction = self.errorMessage(errorCode: (error?._code)!)
if msgAndAction.0 != ""{
UIApplication.topViewController()?.showAlert(withTitle: "Error", andMessage: msgAndAction.0, andActions: msgAndAction.1)
}
}
}
The error methods:
//MARK: TouchID error
fileprivate func errorMessage(errorCode:Int) -> (strMessage: String, action: [UIAlertAction]){
var strMessage = ""
let cancelAction = UIAlertAction.init(title: Const.Localize.Common().kCancel, style: .cancel) { (cancelAction) in
}
var actions: [UIAlertAction] = [cancelAction]
switch errorCode {
case LAError.Code.authenticationFailed.rawValue:
biometricAuthentication { (success) in
//
}
case LAError.Code.userCancel.rawValue:
if biometryRetryCount == 2 {
blockUserFor24Hours()
} else {
showAlertOnCancelTapAction()
}
case LAError.Code.passcodeNotSet.rawValue:
strMessage = "Please goto the Settings & Turn On Passcode"
case LAError.Code.userFallback.rawValue:
biometryRetryCount -= 2
if biometryRetryCount == 0 {
blockUserFor24Hours()
} else {
biometricAuthentication { (success) in
//
}
}
default:
strMessage = evaluatePolicyFailErrorMessageForLA(errorCode: errorCode).strMessage
actions = evaluatePolicyFailErrorMessageForLA(errorCode: errorCode).action
}
return (strMessage, actions)
}
func evaluatePolicyFailErrorMessageForLA(errorCode: Int) -> (strMessage: String, action: [UIAlertAction]){
let cancelAction = UIAlertAction.init(title: Const.Localize.Common().kCancel, style: .cancel) { (cancelAction) in
}
var actions: [UIAlertAction] = [cancelAction]
var message = ""
if #available(iOS 11.0, macOS 10.13, *) {
switch errorCode {
case LAError.biometryNotAvailable.rawValue:
message = "Authentication could not start because the device does not support biometric authentication."
case LAError.biometryLockout.rawValue:
showPasscodeScreen()
case LAError.biometryNotEnrolled.rawValue:
message = "You do not have a registered biometric authentication. Kindly go to the settings and setup one"
let settingsAction = UIAlertAction.init(title: Const.Localize.Common().kSetting, style: .default) { (settingsAction) in
UIApplication.shared.openURL(URL(string: UIApplicationOpenSettingsURLString)!)
}
actions.append(settingsAction)
default:
message = "Did not find error code on LAError object"
}
} else {
switch errorCode {
case LAError.touchIDLockout.rawValue:
showPasscodeScreen()
case LAError.touchIDNotAvailable.rawValue:
message = "TouchID is not available on the device"
case LAError.touchIDNotEnrolled.rawValue:
message = "You do not have a registered biometric authentication. Kindly go to the settings and setup one"
let settingsAction = UIAlertAction.init(title: Const.Localize.Common().kSetting, style: .default) { (settingsAction) in
UIApplication.shared.openURL(URL(string: UIApplicationOpenSettingsURLString)!)
}
actions.append(settingsAction)
default:
message = "Did not find error code on LAError object"
}
}
return (message, actions)
}
Related
I have issue, even when i put wrong password, my app showing me info about Success login, but in reality im not. How to make work it properly?
Auth.auth().fetchSignInMethods(forEmail: userEmail, completion: {
(providers, error) in
if error != nil {
self.displayAlertMessage(alertTitle: "Unhandled error", alertMessage: "Undefined error #SignUpViewController_0001");
return;
} else if providers == nil {
self.displayAlertMessage(alertTitle: "Error", alertMessage: "This account is not exist.");
return;
}
})
// Login
Auth.auth().signIn(withEmail: userEmail, password: userPassword) { [weak self] authResult, error in
guard self != nil else {
self?.displayAlertMessage(alertTitle: "Alert", alertMessage: "Wrong password.");
return }
}
self.displayAlertMessage(alertTitle: "Success", alertMessage: "You are successfuly sign in.", dismiss: true);
// Return to initial view
Auth.auth().signIn() is asynchronous and returns immediately. The callback you pass will be invoked some time later with the results of the sign in. What your code is doing is immediately calling self.displayAlertMessage(alertTitle: "Success",...) before the sign in is complete. You should only expect sign in results inside the callback, not on the next line of code after you call signIn().
This function I wrote for my current application.
func loginButtonTapped() {
indicator.setupIndicatorView(view, containerColor: .white, indicatorColor: .CustomGreen())
view.alpha = 0.7
let email = mainView.userEmail
let password = mainView.userPassword
Auth.auth().signIn(withEmail: email, password: password) { (user, error) in
if error == nil {
if Auth.auth().currentUser?.isEmailVerified == true {
self.view.alpha = 1.0
self.indicator.hideIndicatorView()
print("Logined")
} else {
Auth.auth().currentUser?.sendEmailVerification(completion: { (error) in
if error == nil {
self.view.alpha = 1.0
self.indicator.hideIndicatorView()
Alert.showAlert(title: "Warning", subtitle: "You have not activate your account yet. We have sent you an email to activate it.", leftView: UIImageView(image: #imageLiteral(resourceName: "isWarningIcon")), style: .warning)
} else {
self.view.alpha = 1.0
self.indicator.hideIndicatorView()
Alert.showAlert(title: "Error", subtitle: "Incorrect email.", leftView: UIImageView(image: #imageLiteral(resourceName: "isErrorIcon")), style: .danger)
}
})
}
} else {
self.view.alpha = 1.0
self.indicator.hideIndicatorView()
Alert.showAlert(title: "Error", subtitle: "Incorrect email or password.", leftView: UIImageView(assetIdentifier: AssetIdentifier.error)!, style: .danger)
}
}
}
As Doug mentioned in his answer, Firebase is asynchronous and data is only valid within the closure following the function call. It takes time for that data to become valid so any code outside the function call following the closure will be called before the code inside the closure.
So what that means for your code is
Auth.auth().signIn(withEmail: userEmail, password: userPassword) { [weak self] authResult, error in
//this code will execute *after* the code that displays success
}
//this code will execute *before* the code within the closure following the signIn
self.displayAlertMessage(alertTitle: "Success"
Here's some sample code that handles errors and provides the proper sequence for code flow.
Auth.auth().signIn(withEmail: user, password: pw, completion: { (auth, error) in
if let x = error {
let err = x as NSError
switch err.code {
case AuthErrorCode.wrongPassword.rawValue:
print("wrong password")
case AuthErrorCode.invalidEmail.rawValue:
print("invalued email")
case AuthErrorCode.accountExistsWithDifferentCredential.rawValue:
print("accountExistsWithDifferentCredential")
default:
print("unknown error: \(err.localizedDescription)")
}
} else {
if let _ = auth?.user {
print("authd") //user is auth'd proceed to next step
} else {
print("authentication failed - no auth'd user")
}
}
})
We have .zip file in app, tagged as on demand resource. On downloading it we get success in NSBundleResourceRequest completion handler, but unable to find path of downloaded file(.zip). It works fine for png and jpg file but fails for .zip files. Also .zip file download works fine in our testing devices and fails only on App Reviewer devices.
Any alternative for .zip in iOS will work on ODR?
Are you using conditionalyBeginAccessingResources method before beginAccessingResources ?
Resources:
Check this nice ODR ios tutorial from Ray, and this book from Vandad (it contains a section for propper ODR fetching).
From the Ray's tutorial:
class ODRManager {
// MARK: - Properties
static let shared = ODRManager()
var currentRequest: NSBundleResourceRequest?
// MARK: - Methods
func requestFileWith(tag: String,
onSuccess: #escaping () -> Void,
onFailure: #escaping (NSError) -> Void) {
currentRequest = NSBundleResourceRequest(tags: [tag])
guard let request = currentRequest else { return }
request.endAccessingResources()
request.loadingPriority =
NSBundleResourceRequestLoadingPriorityUrgent
request.beginAccessingResources { (error: Error?) in
if let error = error {
onFailure(error as NSError)
return
}
onSuccess()
}
}
}
In use:
ODRManager.shared.requestFileWith(tag: "<#Your tag#>", onSuccess: {
// load it through Bundle
}, onFailure: { (error) in
let controller = UIAlertController(title: "Error", message: "There was a problem.", preferredStyle: .alert)
switch error.code {
case NSBundleOnDemandResourceOutOfSpaceError:
controller.message = "You don't have enough space available to download this resource."
case NSBundleOnDemandResourceExceededMaximumSizeError:
controller.message = "The bundle resource was too big."
case NSBundleOnDemandResourceInvalidTagError:
controller.message = "The requested tag does not exist."
default:
controller.message = error.description
}
controller.addAction(UIAlertAction(title: "Dismiss", style: .default, handler: nil))
guard let rootViewController = self.view?.window?.rootViewController else { return }
rootViewController.present(controller, animated: true)
})
From the book:
let tag = "<#tagString#>"
var currentResourcePack: NSBundleResourceRequest? = NSBundleResourceRequest(tags: [tag])
guard let req = currentResourcePack else { return }
req.conditionallyBeginAccessingResources { available in
if available {
self.displayImagesForResourceTag(tag)
} else {
// this usualy means that the resources are not downloaded so you need to download them first
req.beginAccessingResources { error in
guard error == nil else {
<#/* TODO: you can handle the error here*/#>
return
}
self.displayImagesForResourceTag(tag)
}
}
}
func displayImagesForResourceTag(_ tag: String) {
OperationQueue.main.addOperation {
for n in 0..<self.imageViews.count {
self.imageViews[n].image = UIImage(named: tag + "-\(n+1)")
}
}
}
So, maybe you can dig out the source of zip there?
Alternative way
Another solution is to download the zip, extract it and start using resources from the extract sandbox destination folder by using the FileManager, and use Bundle only when ODR are not or can't be downloaded.
GL
How to resolve the TouchId error: Domain=com.apple.LocalAuthentication Code=-2 "Canceled by user."
I tried to add local context again:
let myContext = LAContext()
let myLocalizedReasonString = "Please use your last login for Inspyrus Supplier Central."
var authError: NSError?
if #available(iOS 8.0, macOS 10.12.1, *) {
if myContext.canEvaluatePolicy(.deviceOwnerAuthenticationWithBiometrics, error: &authError) {
myContext.evaluatePolicy(.deviceOwnerAuthenticationWithBiometrics, localizedReason: myLocalizedReasonString) { success, evaluateError in
DispatchQueue.main.async {
if success {
self.btnLoginClicked(UIButton())
} else {
print(evaluateError?.localizedDescription ?? "Failed to authenticate")
// Fall back to a asking for username and password.
// ...
}
}
}
}
}
You can check if the evaluateError returned from the evaluatePolicy call is a LAError.userCancel.
Something like this:
if success {
//...
}
else if let authError = evaluateError as? LAError {
switch authError.code {
case .userCancel:
// Authentication was canceled by user (e.g. tapped Cancel button).
break
default:
// Other error
break
}
// Or
switch authError {
case LAError.userCancel:
// Authentication was canceled by user (e.g. tapped Cancel button).
break
default:
// Other error
break
}
}
I implemented Twitter share button, but there are no column to change Twitter accounts like this picture. And if I logged out from the account, I can't authorize (login) another account. The view to authorize will never appear.
My app doesn't have the 2 columns ("Account" and "Location"). So I can't change Account except I authorized at first.
How can I solve this problem?
(if possible, it's better to be able to authorize accounts when I logout from all accounts after authorized and tweeted once. But I don't know how to do it....)
let shareImage = UIImage(named:"Twitter_shareImage")
let tweetDescription:String = "text"
TWTRTwitter.sharedInstance().sessionStore.saveSessionWithAuthToken(authToken, authTokenSecret: authTokenSecret, completion: { (session, error) -> Void in
if (TWTRTwitter.sharedInstance().sessionStore.hasLoggedInUsers()) {
if let session = TWTRTwitter.sharedInstance().sessionStore.session() {
let composer = TWTRComposer()
composer.setText("text")
composer.setImage(shareImage)
composer.show(from: self.parentViewController()!, completion: nil)
print(session.userID)
} else {
TWTRTwitter.sharedInstance().logIn {
(session, error) -> Void in
if (session != nil) {
// println("signed in as \(session.userName)");
} else {
// println("error: \(error.localizedDescription)");
}
}
print("no account")
}
} else {
// Log in, and then check again
TWTRTwitter.sharedInstance().logIn { session, error in
if session != nil { // Log in succeeded
let composer = TWTRComposer()
composer.setText("text")
composer.setImage(shareImage)
composer.show(from: self.parentViewController()!, completion: nil)
} else {
let alert = UIAlertController(title: "No Twitter Accounts Available", message: "You must log in before presenting a composer.", preferredStyle: .alert)
self.parentViewController()?.present(alert, animated: false, completion: nil)
}
}
}
When the Touch ID alert is displayed, there is also a "Cancel" button. I would prefer to NOT allow the user to cancel because they are prohibited from continuing any further. 1. Is there a way to remove the "Cancel" button. 2. If the "Cancel" button is required, how can I force the user to re-authenticate with a fingerprint? If authenticate() is called a second time, the Touch ID API just lets them in. There is no alternative passcode and I'd hate to have to code up yet another view controller for it.
func authenticate() {
let myContext:LAContext = LAContext()
let authError:NSErrorPointer = nil
if (myContext.canEvaluatePolicy(.DeviceOwnerAuthenticationWithBiometrics, error: authError)) {
myContext.evaluatePolicy(.DeviceOwnerAuthenticationWithBiometrics, localizedReason: "Press fingerprint", reply: { (success:Bool, error:NSError?) -> Void in
if success == true {
log.debug("SUCCESSFUL AUTHENTICATION")
dispatch_async(dispatch_get_main_queue(), { () -> Void in
self.performSegueWithIdentifier("showUI", sender: self)
})
}
else {
log.debug("FAILED AUTHENTICATION")
self.authenticate()
}
})
}
}
You need to dispatch your failure call to self.authenticate on the main queue;
func authenticate() {
let myContext:LAContext = LAContext()
let authError:NSErrorPointer = nil
if (myContext.canEvaluatePolicy(.deviceOwnerAuthenticationWithBiometrics, error: authError)) {
myContext.evaluatePolicy(.deviceOwnerAuthenticationWithBiometrics, localizedReason: "Press fingerprint", reply: { (success:Bool, error:NSError?) -> Void in
if success {
log.debug("SUCCESSFUL AUTHENTICATION")
DispatchQueue.main.async {
self.performSegueWithIdentifier("showUI", sender: self)
}
}
else {
log.debug("FAILED AUTHENTICATION")
DispatchQueue.main.async {
self.authenticate()
}
}
})
}
}