I'm working on integrating touchID into my application. The process was fairly simple, but even just using my dummy data, it takes about 5 seconds after it authenticated my fingerprint, before it performs it's task.
Here's my code:
func requestFingerprintAuthentication() {
let context = LAContext()
var authError: NSError?
let authenticationReason: String = "Login"
if context.canEvaluatePolicy(LAPolicy.DeviceOwnerAuthenticationWithBiometrics, error: &authError) {
context.evaluatePolicy(LAPolicy.DeviceOwnerAuthenticationWithBiometrics, localizedReason: authenticationReason, reply: {
(success: Bool, error: NSError?) -> Void in
if success {
println("successfull signin with touchID")
self.emailInputField.text = "john.doe#gmail.com"
self.passwordInputField.text = "password"
self.signIn(self.signInButton)
} else {
println("Unable to Authenticate touchID")
}
})
}
}
even with the dummy data, it takes waaay too long.
When I login normally, by typing the email and the password into my inputfields, the signIn() function runs instantly.
To see if it was a problem with that. I tried replacing that, with 2 lines that simply takes me to the correct viewController. But it still takes several seconds after it's authenticated my fingerprint.
I know it's not the phone, nor touchID. Cause it runs my println("successfull signin with touchID") immediately. It's what comes after that, that for some reason takes several seconds for it to run?
Any help explaining this would be greatly appreciated!
The documentation states:
This method asynchronously evaluates an authentication policy.
You are running UI code on a thread that is not the main. Wrap your code to get it to perform on the main thread:
func requestFingerprintAuthentication() {
let context = LAContext()
var authError: NSError?
let authenticationReason: String = "Login"
if context.canEvaluatePolicy(LAPolicy.DeviceOwnerAuthenticationWithBiometrics, error: &authError) {
context.evaluatePolicy(LAPolicy.DeviceOwnerAuthenticationWithBiometrics, localizedReason: authenticationReason, reply: {
(success: Bool, error: NSError?) -> Void in
if success {
NSOperationQueue.mainQueue().addOperationWithBlock({ () -> Void in
println("successfull signin with touchID")
self.emailInputField.text = "john.doe#gmail.com"
self.passwordInputField.text = "password"
self.signIn(self.signInButton)
})
} else {
println("Unable to Authenticate touchID")
}
})
}
}
Related
func authenticateBiometry(completion: #escaping ErrorHandler) {
context.evaluatePolicy(.deviceOwnerAuthentication, localizedReason: " ") { success, error in
guard let error = error else {
if success {
completion(nil)
}
return
}
completion(error)
}
}
But it prompts for touchId/faceId only the first time. What can I do to ask for it for example every time when I tap button? Let's say every 15 seconds.
Just tested locally and it works for me, this is the only way I found. I saw your comment above but I will put an answer here because probably someone will not find it ugly haha :).
I took some time to google some kind of reset method in LAContext class, but didn't find anything.
The solution was to reset the LAContext at the beginning of the method called on button tap:
func authenticateBiometry(completion: #escaping ErrorHandler) {
context = LAContext() //THIS, implying that context is declared as `var`
context.evaluatePolicy(.deviceOwnerAuthentication, localizedReason: " ") { success, error in
guard let error = error else {
if success {
completion(nil)
}
return
}
completion(error)
}
}
You will be able to prompt face/touch ID on each button click, as soon as one ends.
I am using Apple's HealthKit to build an app, and am requesting permission from the user to read and write Sleep Data as soon as a View loads using SwiftUI's .onAppear modifier.
It all works fine and I get the data I need using this method.
However, if a user revokes the read and write permissions for my app through Settings, instead of requesting permission again, the app crashes. This is how I have set things up.
#State var authorized: Bool = false
var body: some View {
Text("\(initTextContent)")
.onAppear {
healthstore.getHealthAuthorization(completionAuth: {success in
authorized = success
if self.authorized == true {
healthstore.getSleepMetrics(startDate: sevendaysAgo, endDate: sevendaysAfter, completion: sleepDictAssign)
}
else {
initTextContent = "Required Authorization Not Provided"
}
})
}
}
I've created a class called healthstore and am simply using HealthKit's requestAuthorization method as follows:
var healthStore: HKHealthStore()
func getHealthAuthorization(completionAuth: #escaping (Bool) -> Void) {
//we will call this inside our view
healthStore.requestAuthorization(toShare: [sleepSampleType], read: [sleepSampleType]) { (success, error) in
if let error = error {
// Handle the error here.
fatalError("*** An error occurred \(error.localizedDescription) ***")
}
else {
completion(success)
}
}
}
If the user revoked the permisions they are revoked. You cannot ask again. If you want to handle this scenario you would need to throw the error and handle it outside of the function by showing a message to the user asking them to enable it again.
Or simply return the success boolean ignoring the error.
func getHealthAuthorization(completionAuth: #escaping (Bool) -> Void) {
//we will call this inside our view
healthStore.requestAuthorization(toShare: [sleepSampleType], read: [sleepSampleType]) { (success, error) in
//ignore error since your func can just return a boolean
completion(success)
}
}
I have inherited a code base with the following class providing support for Face/Touch ID.
The expected behaviour is that on Face/Touch ID success the user is signed in. This works.
However, should the user fail Face ID and opt to enter their passcode, they are signed out once the completion handler is called. I believe opting to use passcode is triggering
else {
self.authState = .unauthenticated
completion(.unauthenticated)
}
How can I trigger the passcode prompt instead? Should I create a second policy using LAPolicy.deviceOwnerAuthentication and evaluate that instead?
import LocalAuthentication
public enum AuthenticationState {
case unknown
case authenticated
case unauthenticated
public func isAuthenticated() -> Bool {
return self == .authenticated
}
}
public protocol TouchIDAuthenticatorType {
var authState: AuthenticationState { get }
func authenticate(reason: String, completion: #escaping (AuthenticationState) -> Void) -> Void
func removeAuthentication() -> Void
}
public protocol LAContextType: class {
func canEvaluatePolicy(_ policy: LAPolicy, error: NSErrorPointer) -> Bool
func evaluatePolicy(_ policy: LAPolicy, localizedReason: String, reply: #escaping (Bool, Error?) -> Void)
}
public class TouchIDAuthenticator: TouchIDAuthenticatorType {
public var authState: AuthenticationState = .unknown
private var context: LAContextType
private var policy = LAPolicy.deviceOwnerAuthenticationWithBiometrics
public init(context: LAContextType = LAContext()) {
self.context = context
}
public func authenticate(reason: String, completion: #escaping (AuthenticationState) -> Void) -> Void {
var error: NSError?
if context.canEvaluatePolicy(policy, error: &error) {
context.evaluatePolicy(policy, localizedReason: reason) { (success, error) in
DispatchQueue.main.async {
if success {
self.authState = .authenticated
completion(.authenticated)
} else {
self.authState = .unauthenticated
completion(.unauthenticated)
}
}
}
} else {
authState = .authenticated
completion(.authenticated)
}
}
public func removeAuthentication() -> Void {
authState = .unknown
context = LAContext() // reset the context
}
}
extension LAContext: LAContextType { }
I should point out, on the simulator this appears to work as expected, but on a device it does not and I signed out.
You have to use .deviceOwnerAuthentication instead of asking for biometrics. If FaceID is available, it will force the attempt to use this either way.
If you try enough times then you will get another dialogue to "Cancel" or fallback to "Use passcode".
Choosing the fallback will show you the passcode screen.
However, if you specified .deviceOwnerAuthenticationWithBiometrics, you will get the same fallback option. Rather than getting this dialogue I would have expected to receive an error of LAError.Code.biometryLockout. But instead I get this fallback option dialogue. But that is ok...
However, if I then tap the fallback option to "Use passcode", it will NOT present the passcode alert. Instead it fails with a LAError.Code.userFallback error.
If you use the policy without biometrics, you will not get and be able to catch the .userFallback error.
So to sum things up:
If you ask for the deviceOwnerAuthenticationWithBiometrics policy then you will have to handle the fallback yourself.
If you ask for deviceOwnerAuthentication only, then biometrics will be used if available and authorized, otherwise it will automatically fall back to passcode if biometrics is unavailable, or give you the fallback option to enter passcode automatically if biometrics attemps fail.
I'm trying to link two firebase accounts
a phone account ( signed in ) to Anonymous account ( prevUser )
this is my code
func verifyCode() {
let credential = PhoneAuthProvider.provider().credential(
withVerificationID: self.verificationID,
verificationCode: phoneCode.text!)
let prevUser = Auth.auth().currentUser!
Auth.auth().signIn(with: credential) { (user, error) in
if let error = error {
print("1 something went wrong : ", error.localizedDescription)
return
}
print("the user ID is : " , user!.uid)
prevUser.link(with: credential, completion: { (user, error) in
if let error = error {
print("something went wrong : ", error.localizedDescription)
return
}
})
}
}
I keep getting the same error always
something went wrong : The SMS code has expired. Please re-send the verification
code to try again.
thanks in advance
There are 2 issues here:
You are signing in with the phone auth credential first and then linking to the same credential which contains the same underlying SMS code which is a one time code. This will always fail with the error you are getting since the Firebase Auth backend can only use the code once (it immediately expires the first time it is used).
Even if the code is usable more than once, you can't have more than one user with the same phone credential. You first sign in one user with that credential and then try to link it to another user. This will always fail with an error that the credential already exists on another account.
Step 1: You can verify the phone number by providing phone number string and in completion you will be returned a verificationID and error if any.
func verifyPhoneNumber(withPhoneNo phoneNo:String, completion: #escaping (_ error: Error?, _ verificationID: String?)->Void) {
PhoneAuthProvider.provider().verifyPhoneNumber(phoneNo) { (verificationID, error) in
if let error = error {
print(error.localizedDescription)
completion(error, verificationID)
} else {
print(verificationID ?? "no id provided")
completion(nil, verificationID)
}
}
}
Step 2: Linking Phone Number to the current User by providing the current user, 6digit verification code, verificationID obtained from the previous function.
In completion you will be returned with an error if any.
func linkPhoneNumber(withUser user: User, verificationCode: String, verificationID id: String, completion: #escaping (_ error: Error?)-> Void) {
let credential = PhoneAuthProvider.provider().credential(withVerificationID: id, verificationCode: verificationCode)
user.link(with: credential) { (user, error) in
if let error = error {
print(error.localizedDescription)
completion(error)
} else {
print(user!.uid)
completion(nil)
}
}
}
So I've been following the instructions in this answer...
Healthkit background delivery when app is not running
The code runs fine and works whilst the application is open and says that background delivery is successful, however when I test the application by walking around and changing the clock on the device to an hour forward I do not receive any logs to let me know it has run. However, if I open the application again the observer query runs.
private func checkAuthorization(){
let healthDataToRead = Set(arrayLiteral: self.distanceQuantityType!)
healthKitStore.requestAuthorization(toShare: nil, read: healthDataToRead) { (success, error) in
if error != nil {
print(error?.localizedDescription)
print("There was an error requesting Authorization to use Health App")
}
if success {
print("success")
}
}
}
public func enableBackgroundDelivery() {
self.checkAuthorization()
self.healthKitStore.enableBackgroundDelivery(for: self.distanceQuantityType!, frequency: .hourly) { (success, error) in
if success{
print("Background delivery of steps. Success = \(success)")
}
if let error = error {
print("Background delivery of steps failed = \(error.localizedDescription)")
}
}
}
func observeDistance(_ handler:#escaping (_ distance: Double) -> Void) {
let updateHandler: (HKObserverQuery?, HKObserverQueryCompletionHandler?, Error?) -> Void = { query, completion, error in
if !(error != nil) {
print("got an update")
completion!()
} else {
print("observer query returned error: \(error)")
}
}
let query = HKObserverQuery(sampleType: self.distanceQuantityType!, predicate: nil, updateHandler: updateHandler)
self.healthKitStore.execute(query)
}
The query is initialised in the appDelegate method didFinishLaunching
This particular HealthKitQuery is asynchronous. You should wait until it finishes processing.
However, this case is not possible in didFinishLaunching. The application just ended execution and there is not enough time to process the query.
I would seriously suggest to rethink the logic behind the operation of your code. A good way to solve this would be to put the request elsewhere, preferrably after the needed operations were completed.