Firebase Phone Authentication - Long Delay & Multiple OTPs - ios

I'm working on an iOS app project that involves Firebase's phone authentication. I have it working fine on simulator, my iPhone, and my iPad. However, now that I am in the TestFlight stage , my external testers are experiencing long delays in receiving their OTPs as well as receiving duplicates when they reach the ViewController where they enter the OTP code (This is probably due to them hitting the button multiple times).
I also have APNs enabled and working properly.
I don't have much code to share as I followed Firebase's documentation.
What could be some reasons for a long delay in receiving the OTP code from Firebase? I will be including an activity spinner in the project when users tap the sign-in button. However, I also don't want it to be spinning for a minute as users wait for their OTP.
#objc func phoneSignIn() {
guard let phoneNumber = startVerificationView.phoneNumberTextField.text else { return }
let completePhoneNumber = "+1\(phoneNumber)"
Auth.auth().settings?.isAppVerificationDisabledForTesting = isVerificationDisabled
PhoneAuthProvider.provider().verifyPhoneNumber(completePhoneNumber, uiDelegate: nil) { (verificationId, error) in
if error == nil {
guard let verifyId = verificationId else { return }
UserDefaults.standard.set(verifyId, forKey: "verificationId")
let vc = CheckVerificationViewController()
vc.modalPresentationStyle = .fullScreen
vc.completePhoneNumber = completePhoneNumber
self.navigationController?.pushViewController(vc, animated: true)
}
}
}
Also isVerificationDisabled is set to false.

Related

Firestore fails to fetch full document after listener kept firing off continuously

Ok something really weird happened. Have an app that's about to go into production. This is how the application functions
There is a Home Page which fetches the logged in user's document every time the app comes to foreground
This is not a one time fetch but a listener so it will also listen for changes as the app is in foreground
I use this mechanism to keep the user document up to date and pass it to other pages in the app
When app goes to background, I remove the listener
The user document has close to 30 fields
So I was testing another functionality in app in the simulator and everything was working. I parallely ran the app with the same user logged in on a real device and this is what happened after the build:
As the app came to foreground the listener fired without stopping. I have the code in a such a way that every time the listener fetches the document from firestore, a part of the home page animates. So technically the home page refreshed without stopping
I killed the app and re-built it and the continuous firestore fetch happened yet again
So I all together deleted the app and re-built it - Now the continuous firestore fetch stopped
Here is the PROBLEM:
The document has all
30 fields on firestore
But on the real device, it fetches only 2 fields
I tried re-installing and re-building many times but this is the state
But on the simulator with the same user logged in, it works fine.
What could be the issue? Is there some corrupted cache for this particular user? Firestore is a solid product so never encountered anything like this before.
Here is the listener code:
#objc func viewEntersForeground(){
guard let currentUid = Auth.auth().currentUser?.uid else {return}
let ref = Firestore.firestore().collection("users").document(currentUid)
userListener = ref.addSnapshotListener({ (snapshot, error) in
if let error = error{
let alertController = ErrorAlertController(errorText: "\(error.localizedDescription)", errorTitle: "Something's wrong", viewController: self)
alertController.showAlertController()
return
}
//THE BELOW DICTIONARY FETCHES ONLY TWO FIELDS
guard let dictionary = snapshot?.data() else {return}
self.userDictionary = dictionary
self.user = User(dictionary: dictionary)
self.checkIfUserExists()
if self.user?.uid == "" || self.user?.uid == nil {
return
}
self.passDataToInbox()
self.passDataToSettings()
self.checkIfFirTokenExists()
self.checkIfProfilePictureExists()
if self.user?.userState == 3{
self.isUserState3 = true
} else {
self.userState = self.user?.userState
}
})
}

When I use the apple to log in, the selection box will pop up. I choose to use the password to continue and the prompt is not complete

iOS13 (beta) Apple Login error
#available(iOS 13.0, *)
func authorizationController(controller: ASAuthorizationController, didCompleteWithError error: Error) {
// Handle error.
crprint(error.localizedDescription)
}
Failed to complete operation. (com.apple.AuthenticationServices.AuthorizationError error 1000.)
I've encountered the same issue yesterday and I've managed to fix it following these steps:
Go to https://appleid.apple.com/account/manage, under the Devices section you should find devices on which you are signed in with your Apple ID,
Find device/simulator on which Apple SSO is not working, click on it and click remove from the account,
Go back to your device/simulator settings, it will ask you to authenticate again. When you successfully authenticate, Apple SSO should work again!
I'm not sure what caused this issue, probably some issue between the simulator and Apple ID.
In my case, launching ASAuthorizationController including a request for ASAuthorizationPasswordProvider was causing the error.
Failed to complete operation. (com.apple.AuthenticationServices.AuthorizationError error 1000.)
From the ASAuthorizationError.Code documentation; 1000 is for unknown
ASAuthorizationError.Code.unknown
The authorization attempt failed for an unknown reason.
Declaration
case unknown = 1000
Ref: https://developer.apple.com/documentation/authenticationservices/asauthorizationerror/code/unknown
Now that's not particularly helpful but did give me a clue to check my ASAuthorizationController setup which I was trying to launch with 2 requests from ASAuthorizationAppleIDProvider & ASAuthorizationPasswordProvider, like so:
func loginWithAppleButtonPressed() {
let appleSignInRequest = ASAuthorizationAppleIDProvider().createRequest()
appleSignInRequest.requestedScopes = [.fullName, .email]
let anySignInRequest = ASAuthorizationPasswordProvider().createRequest()
let controller = ASAuthorizationController(authorizationRequests: [appleSignInRequest,
anySignInRequest])
controller.delegate = self
controller.presentationContextProvider = self
controller.performRequests()
}
I tried this on a simulator that had an Apple ID with 2FA enabled and also on a device with another Apple ID without 2FA, and both times it would just go to authorizationController(controller:didCompleteWithError error:) and that's it.
Solution:
So to keep it simple, I launched ASAuthorizationController with only ASAuthorizationAppleIDProvider like so:
func loginWithAppleButtonPressed() {
let appleSignInRequest = ASAuthorizationAppleIDProvider().createRequest()
appleSignInRequest.requestedScopes = [.fullName, .email]
let controller = ASAuthorizationController(authorizationRequests: [appleSignInRequest])
controller.delegate = self
controller.presentationContextProvider = self
controller.performRequests()
}
And voilà! This time things worked as expected:
When using an Apple ID with 2FA
popped up with the login request
When using an Apple ID without 2FA
popped up an error telling me to enable 2FA
called authorizationController(controller:didCompleteWithError error:) with error 1000
So seems that in my case ASAuthorizationPasswordProvider was the culprit but since ASAuthorizationError.Code.unknown is a generic error case, this solution may not work for you.
Also, In my case I need only ASAuthorizationAppleIDProvider for Apple ID sign in so dropped the support for ASAuthorizationPasswordProvider.
In my case i needed to first check ASAuthorizationPasswordProvider, then, if there are no stored credential, use ASAuthorizationAppleIDProvider. For this case i had to make some crunches. Code below:
// Initial point
public func fire(appleIDCompletion: #escaping AppleIDServiceCompletion) {
self.completion = appleIDCompletion
let requestPassword = ASAuthorizationPasswordProvider().createRequest()
performRequest(requestPassword)
}
// help function
private func performRequest(_ request: ASAuthorizationRequest) {
let controller = ASAuthorizationController(authorizationRequests: [request])
controller.delegate = self
controller.presentationContextProvider = self
controller.performRequests()
}
// delegate
func authorizationController(controller: ASAuthorizationController, didCompleteWithError error: Error) {
if let e = error as? ASAuthorizationError {
switch e.code {
case .canceled:
trace("User did cancel authorization.")
return
case .failed:
trace("Authorization failed.")
case .invalidResponse:
trace("Authorization returned invalid response.")
case .notHandled:
trace("Authorization not handled.")
case .unknown:
if controller.authorizationRequests.contains(where: { $0 is ASAuthorizationPasswordRequest }) {
trace("Unknown error with password auth, trying to request for appleID auth..")
let requestAppleID = ASAuthorizationAppleIDProvider().createRequest()
requestAppleID.requestedScopes = [.email, .fullName]
requestAppleID.requestedOperation = .operationImplicit
performRequest(requestAppleID)
return
} else {
trace("Unknown error for appleID auth.")
}
default:
trace("Unsupported error code.")
}
}
completion?(.rejected(error))
}
Works like a charm 🔥
Simply Add + "Sign In with Apple" from Capability.
I've resolved it by adding sign in with apple as key in entitlements plist .
From Apple's example,
performExistingAccountSetupFlows, only call this method once on viewDidAppear. If user info exists already then Apple will show it to login. If not then it will throw error.
handleAuthorizationAppleIDButtonPress, whenever user taps on Sign in with Apple button, note that if an account already had existed it would have shown it to the user already. I believe its still in progress and not all use cases are covered, for example if user sees the login info initially from ViewDidAppear call and cancels it then user have to create a new account when tapping on this method since its missing ASAuthorizationPasswordProvider request. If user had some login info then in that case this call (with ASAuthorizationPasswordProvider) will succeed but if no data is available then user will not see any action on tapping this button since it will throw error.
I am still figuring this out, if I have anything more to add then I will update the answer. So, for now we can only have this one use case to use this Sign in with Apple option.
Update:
Once I created a new account, I was offered by this same flow to login with the already existing account. So, I can say that there is no need to include call to ASAuthorizationPasswordProvider request in handleAuthorizationAppleIDButtonPress method. I am doing all the testing on device.
You can always go to Settings -> AppleId -> Password & Security -> Apple ID Logins to check and delete account if you need to test various scenarios.
Update 2:
Everything seems to work fine in other scenarios too if you already have a saved password or App Id account created, so even if I pass ASAuthorizationPasswordProvider in the handleAuthorizationAppleIDButtonPress call, it is working fine. I would suggest to not pass ASAuthorizationPasswordProvider in the next call and keep the flow as described above, this way if no saved password is present or Apple Id created then it will provide option to the user to create a new id, if there is already an id that exists then it will show that id.
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
performExistingAccountSetupFlows()
}
func performExistingAccountSetupFlows() {
// Prepare requests for both Apple ID and password providers.
let requests = [ASAuthorizationAppleIDProvider().createRequest(),
ASAuthorizationPasswordProvider().createRequest()]
// Create an authorization controller with the given requests.
let authorizationController = ASAuthorizationController(authorizationRequests: requests)
authorizationController.delegate = self
authorizationController.presentationContextProvider = self
authorizationController.performRequests()
}
#objc
func handleAuthorizationAppleIDButtonPress() {
let appleIDProvider = ASAuthorizationAppleIDProvider()
let request = appleIDProvider.createRequest()
request.requestedScopes = [.fullName, .email]
let authorizationController = ASAuthorizationController(authorizationRequests: [request])
authorizationController.delegate = self
authorizationController.presentationContextProvider = self
authorizationController.performRequests()
}
I resolved this by holding my finger down on finger print scanner til completion. I'm not an iphone user so I'm not used to the finger print scanner. If you pull your finger off too soon you get this error.

Force user to update the app programmatically in iOS

In my iOS app I have enabled force app update feature. It is like this.
If there is a critical bug fix. In the server we are setting the new release version. And in splash screen I am checking the current app version and if its lower than the service version, shows a message to update the app.
I have put 2 buttons "Update now", "Update later"
I have 2 questions
If I click now. App should open my app in the appstore with the button UPDATE. Currently I use the link "http://appstore.com/mycompanynamepvtltd"
This opens list of my company apps but it has the button OPEN, not the UPDATE even there is a new update for my app. whats the url to go for update page?
If he click the button "Update Later" is it ok to close the app programmatically? Does this cause to reject my app in the appstore?
Please help me for these 2 questions
Point 2 : You should only allow force update as an option if you don't want user to update later. Closing the app programmatically is not the right option.
Point 1 : You can use a good library available for this purpose.
Usage in Swift:
Library
func applicationDidBecomeActive(application: UIApplication) {
/* Perform daily (.daily) or weekly (.weekly) checks for new version of your app.
Useful if user returns to your app from the background after extended period of time.
Place in applicationDidBecomeActive(_:)*/
Siren.shared.checkVersion(checkType: .daily)
}
Usage in Objective-C: Library
-(void)applicationDidBecomeActive:(UIApplication *)application {
// Perform daily check for new version of your app
[[Harpy sharedInstance] checkVersionDaily];
}
How it works : It used lookup api which returns app details like link including version and compares it.
For an example, look up Yelp Software application by iTunes ID by calling https://itunes.apple.com/lookup?id=284910350
For more info, please visit link
Don't close the app programmatically. Apple can reject the app. Better approach will be do not allow user to use the app. Keep the update button. Either user will go to app store or close the app by himself.
According to Apple, your app should not terminate on its own. Since the user did not hit the Home button, any return to the Home screen gives the user the impression that your app crashed. This is confusing, non-standard behavior and should be avoided.
Please check this forum:
https://forums.developer.apple.com/thread/52767.
It is happening with lot of people. In my project I redirected the user to our website page of downloading app from app store. In that way if the user is not getting update button in app store, at least the user can use the website in safari for the time being.
To specifically answer your question:
Use this URL to directly open to your app in the app store:
https://apps.apple.com/app/id########## where ########## is your app's 10 digit numeric ID. You can find that ID in App Store Connect under the App Information section. It's called "Apple ID".
I actually have terminate functionality built into my app if it becomes so out of date that it can no longer act on the data it receives from the server (my app is an information app that requires connectivity to my web service). My app has not been rejected for having this functionality after a dozen updates over a couple years, although that function has never been invoked. I will be switching to a static message instead of terminating the app, just to be safe to avoid future updates from being rejected.
I have found that the review process is at least somewhat subjective, and different reviewers may focus on different things and reject over something that has previously been overlooked many times.
func appUpdateAvailable() -> (Bool,String?) {
guard let info = Bundle.main.infoDictionary,
let identifier = info["CFBundleIdentifier"] as? String else {
return (false,nil)
}
// let storeInfoURL: String = "http://itunes.apple.com/lookupbundleId=\(identifier)&country=IN"
let storeInfoURL:String = "https://itunes.apple.com/IN/lookup?
bundleId=\(identifier)"
var upgradeAvailable = false
var versionAvailable = ""
// Get the main bundle of the app so that we can determine the app's
version number
let bundle = Bundle.main
if let infoDictionary = bundle.infoDictionary {
// The URL for this app on the iTunes store uses the Apple ID
for the This never changes, so it is a constant
let urlOnAppStore = NSURL(string: storeInfoURL)
if let dataInJSON = NSData(contentsOf: urlOnAppStore! as URL) {
// Try to deserialize the JSON that we got
if let dict: NSDictionary = try?
JSONSerialization.jsonObject(with: dataInJSON as Data, options:
JSONSerialization.ReadingOptions.allowFragments) as! [String:
AnyObject] as NSDictionary? {
if let results:NSArray = dict["results"] as? NSArray {
if let version = (results[0] as! [String:Any]).
["version"] as? String {
// Get the version number of the current version
installed on device
if let currentVersion =
infoDictionary["CFBundleShortVersionString"] as? String {
// Check if they are the same. If not, an
upgrade is available.
print("\(version)")
if version != currentVersion {
upgradeAvailable = true
versionAvailable = version
}
}
}
}
}
}
}
return (upgradeAvailable,versionAvailable)
}
func checkAppVersion(controller: UIViewController){
let appVersion = ForceUpdateAppVersion.shared.appUpdateAvailable()
if appVersion.0 {
alertController(controller: controller, title: "New Update", message: "New version \(appVersion.1 ?? "") is available")
}
}
func alertController(controller:UIViewController,title: String,message: String){
let alertController = UIAlertController(title: title, message: message, preferredStyle: .alert)
alertController.addAction(UIAlertAction(title: "Update", style: .default, handler: { alert in
guard let url = URL(string: "itms-apps://itunes.apple.com/app/ewap/id1536714073") else { return }
if #available(iOS 10.0, *) {
UIApplication.shared.open(url, options: [:], completionHandler: nil)
} else {
UIApplication.shared.openURL(url)
}
}))
DispatchQueue.main.async {
controller.present(alertController, animated: true)
}
}
Use appgrades.io. Keep your app focus on delivering the business value and let 3rd party solution do their tricks. With appgrades, you can, once SDK integrated, create a custom view/alert to display for your old versions users asking them to update their apps. You can customize everything in the restriction view/alert to make it appear as part of your app.

replaykit startrecording sometimes never enters completion handler

I am using replay kit to save a video of my screen during gameplay but randomly on occasion startRecordingWithMicrophoneEnabled and recorder.stopRecordingWithHandler never enters the completion handler
it doesn't throw an error, it just runs and hangs indefinitely.
if recorder.available && recorder.microphoneEnabled {
recorder.startRecordingWithMicrophoneEnabled(true) { [unowned self] (error) in
if let unwrappedError = error {
print(unwrappedError.localizedDescription)
} else {
print("called")
self.manager.instructions.text = "Click to Start Game"
}
}
}
if recorder.available && recorder.microphoneEnabled {
print("initiating stop recording")
recorder.stopRecordingWithHandler { [unowned self] (RPPreviewViewController, error) in
print("in completion handler")
if let previewView = RPPreviewViewController {
print("will transition to gameplay video")
previewView.previewControllerDelegate = self
self.presentViewController(previewView, animated: true, completion: nil)
self.sessionHandler.session.stopRunning()
}
}
}
I was getting this same thing. Was working on one device, and not on another. Only difference was the working device was on iOS version 10.1.0 and the other was iOS version 10.0.2 - upgraded to 10.2.0 and it started working immediately.
I had the same problem and just found its cause (or maybe just a cause). If your device is connected to a WiFi that has no internet access this problem occurs. If you connect it to a WiFi that has internet access or disable WiFi it works just fine. I guess when starting a recording, ReplayKit tries to connect to some Apple servers but never reaches them and also never times out. You can observe the same behavior with the App Store. When you are connected to a WiFi without internet it tries to load the store forever and never times out.

Set a reminder in iOS Swift

I am trying to set a simple EKReminder in my swift application to remind users to catch the bus. However, when I try to save my reminder, I always get a error (no error is reported, the app just crashes). I have the code below.
public class func createReminder(reminderTitle: String, timeInterval: NSDate) {
var calendarDatabase = EKEventStore()
calendarDatabase.requestAccessToEntityType(EKEntityTypeReminder,
completion: nil)
let reminder = EKReminder(eventStore: calendarDatabase)
reminder.title = reminderTitle
let alarm = EKAlarm(absoluteDate: timeInterval)
reminder.addAlarm(alarm)
reminder.calendar = calendarDatabase.defaultCalendarForNewReminders()
var error: NSError?
calendarDatabase.saveReminder(reminder, commit: true, error: &error)
}
The following should work in Swift 4.2
func AddReminder() {
eventStore.requestAccess(to: EKEntityType.reminder, completion: {
granted, error in
if (granted) && (error == nil) {
print("granted \(granted)")
let reminder:EKReminder = EKReminder(eventStore: self.eventStore)
reminder.title = "Must do this!"
reminder.priority = 2
// How to show completed
//reminder.completionDate = Date()
reminder.notes = "...this is a note"
let alarmTime = Date().addingTimeInterval(1*60*24*3)
let alarm = EKAlarm(absoluteDate: alarmTime)
reminder.addAlarm(alarm)
reminder.calendar = self.eventStore.defaultCalendarForNewReminders()
do {
try self.eventStore.save(reminder, commit: true)
} catch {
print("Cannot save")
return
}
print("Reminder saved")
}
})
}
info.plist requires appropriate privacy settings as well.
I haven't used anything like this before, but looking at your code I can see that you call the requestAccessToEntity-method, without handling the response. That method will most likely show the user a prompt, asking them to accept that your app has access to "Reminders". With your code, you ask for the permission, but the rest of your code will execute immediately after asking, without 'waiting' for the response. The very first time this code runs, the user will be asked, and your reminder will be denied, because it tries to save right away.
Even if your user clicks "allow", your code has already run without permission.
Now, if the user clicked allow one time, and then tries to do the same again, then maybe it will work, I don't know. But if your user clicked "Cancel" on the prompt, your code will never work until they go into Settings and allow your app to show reminders.
You should not create your reminder before you know if the user allows it, so you should really split this function into two separate functions. And do not pass nil for completion in that function; handle the response.
try the following:
EKEntityTypeReminder -> EKEntityType.Reminder

Resources