iosswiftfirebase-authenticationpassword-lessdynamic-links

Authenticating to Firebase with passwordless sign in - ios - dynamic link issues


I've been able to set up my app and firebase project so that a user will be prompted to sign in with a passwordless link. They input their email, receive an email to their inbox, and tap it. It then redirects them to my app.

What I'm expecting at that point is for them to be authenticated, which is a process which woudl take place via the application(_ application: UIApplication, continue userActivity: NSUserActivity,restorationHandler: @escaping ([UIUserActivityRestoring]?) method in appDelegate. However, when the user clicks on the link on mobile, they are just redirected to the app and nothing is called. When the user clicks on the link on a desktop, they are brought to a site that says the following:

"Site Not Found Why am I seeing this? There are a few potential reasons:

You haven't deployed an app yet. You may have deployed an empty directory. This is a custom domain, but we haven't finished setting it up yet."

I have two questions here -

  1. Why is the link not being handled when the user is redirected to the app?
  2. Why is the user getting that message when clicking the link on desktop?

My appDelegate methods are here (note that none of these are called when the user is brought back to the app):

    func application(_ application: UIApplication, continue userActivity: NSUserActivity,
                     restorationHandler: @escaping ([UIUserActivityRestoring]?) -> Void) -> Bool {
        print(#function)
      return userActivity.webpageURL.flatMap(handlePasswordlessSignIn)!
    }

 func handlePasswordlessSignIn(withURL url: URL) -> Bool {
        print(#function)
      let link = url.absoluteString
      // [START is_signin_link]
      if Auth.auth().isSignIn(withEmailLink: link) {
        // [END is_signin_link]
        UserDefaults.standard.set(link, forKey: "Link")
        (window?.rootViewController as? UINavigationController)?
          .popToRootViewController(animated: false)
        window?.rootViewController?.children[0]
          .performSegue(withIdentifier: "passwordless", sender: nil)
        return true
      }
      return false
    }

    func application(_ app: UIApplication, open url: URL,
                     options: [UIApplication.OpenURLOptionsKey: Any]) -> Bool {
        print(#function)
      return application(app, open: url,
                         sourceApplication: options[UIApplication.OpenURLOptionsKey
                           .sourceApplication] as? String,
                         annotation: "")
    }

My viewcontroller to handle the authentication is here. Note that I have added both "my-app.firebaseapp.com" and "my-app.page.link" to my authorized domains, both in my app's settings in xcode and in firebase.

class PasswordlessViewController: UIViewController {
    var emailField = UITextField()
    var signInButton = UIButton()
    var link = String()
    
    override func viewDidLoad() {
        print(#function)
        super.viewDidLoad()
        
        setUpViews()

        if let link = UserDefaults.standard.value(forKey: "Link") as? String {
            self.link = link
            signInButton.isEnabled = true
        }

    }
    
    func setUpViews() {
        
        view.backgroundColor = .white
        
        let stackView = UIStackView()
        stackView.axis = .vertical
        stackView.distribution = .fill
        stackView.spacing = 10
        
        view.addSubview(stackView)
        stackView.translatesAutoresizingMaskIntoConstraints = false
        
        stackView.centerXAnchor.constraint(equalTo: view.centerXAnchor).isActive = true
        stackView.centerYAnchor.constraint(equalTo: view.centerYAnchor, constant: -50).isActive = true
        stackView.heightAnchor.constraint(equalTo: view.heightAnchor, multiplier: 0.5).isActive = true
        stackView.widthAnchor.constraint(equalTo: view.widthAnchor, multiplier: 0.5).isActive = true

        
        signInButton.configuration = .plain()
        signInButton.setTitle("Sign In", for: .normal)
        signInButton.addTarget(self, action: #selector(didTapSendSignInLink), for: .touchUpInside)
        
        emailField.layer.borderWidth = 1
        emailField.textColor = .black
        emailField.text = UserDefaults.standard.value(forKey: "Email") as? String
        
        stackView.addArrangedSubview(emailField)
        stackView.addArrangedSubview(signInButton)
        stackView.addArrangedSubview(UIView())
    }
    
    override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
        view.endEditing(true)
    }
    
    
    
    @objc func didTapSignInWithEmailLink(_ sender: AnyObject) {
        if let email = emailField.text {
            showSpinner {
                // [START signin_emaillink]
                Auth.auth().signIn(withEmail: email, link: self.link) { user, error in
                    // [START_EXCLUDE]
                    self.hideSpinner {
                        if let error = error {
                            self.showMessagePrompt(error.localizedDescription)
                            return
                        }
                        self.navigationController!.popViewController(animated: true)
                    }
                    // [END_EXCLUDE]
                }
                // [END signin_emaillink]
            }
        } else {
            showMessagePrompt("Email can't be empty")
        }
    }
    
    @objc func didTapSendSignInLink(_ sender: AnyObject) {
        if let email = emailField.text {
            showSpinner {
                // [START action_code_settings]
                let actionCodeSettings = ActionCodeSettings()
                actionCodeSettings.url = URL(string: "https://my-app.firebaseapp.com/?email=\(email)")
                // The sign-in operation has to always be completed in the app.
                actionCodeSettings.handleCodeInApp = true
                actionCodeSettings.setIOSBundleID(Bundle.main.bundleIdentifier!)
                actionCodeSettings.setAndroidPackageName("com.example.android",
                                                         installIfNotAvailable: false, minimumVersion: "12")
                actionCodeSettings.dynamicLinkDomain = "my-app.page.link"
                // [END action_code_settings]
                // [START send_signin_link]
                Auth.auth().sendSignInLink(toEmail: email,
                                           actionCodeSettings: actionCodeSettings) { error in
                    // [START_EXCLUDE]
                    self.hideSpinner {
                        // [END_EXCLUDE]
                        if let error = error {
                            self.showMessagePrompt(error.localizedDescription)
                            return
                        }
                        // The link was successfully sent. Inform the user.
                        // Save the email locally so you don't need to ask the user for it again
                        // if they open the link on the same device.
                        UserDefaults.standard.set(email, forKey: "Email")
                        self.showMessagePrompt("Check your email for link")
                        // [START_EXCLUDE]
                    }
                    // [END_EXCLUDE]
                }
                // [END send_signin_link]
            }
        } else {
            showMessagePrompt("Email can't be empty")
        }
    }
}

Helper functions for the above viewcontroller:

extension PasswordlessViewController {
    func showMessagePrompt(_ message: String) {
      let alert = UIAlertController(title: nil, message: message, preferredStyle: .alert)
      let okAction = UIAlertAction(title: "OK", style: .default, handler: nil)
      alert.addAction(okAction)
      present(alert, animated: false, completion: nil)
    }

    /*! @fn showTextInputPromptWithMessage
     @brief Shows a prompt with a text field and 'OK'/'Cancel' buttons.
     @param message The message to display.
     @param completion A block to call when the user taps 'OK' or 'Cancel'.
     */
    func showTextInputPrompt(withMessage message: String,
                             completionBlock: @escaping ((Bool, String?) -> Void)) {
      let prompt = UIAlertController(title: nil, message: message, preferredStyle: .alert)
      let cancelAction = UIAlertAction(title: "Cancel", style: .cancel) { _ in
        completionBlock(false, nil)
      }
      weak var weakPrompt = prompt
      let okAction = UIAlertAction(title: "OK", style: .default) { _ in
        guard let text = weakPrompt?.textFields?.first?.text else { return }
        completionBlock(true, text)
      }
      prompt.addTextField(configurationHandler: nil)
      prompt.addAction(cancelAction)
      prompt.addAction(okAction)
      present(prompt, animated: true, completion: nil)
    }
    
    func showSpinner(_ completion: (() -> Void)?) {
      let alertController = UIAlertController(title: nil, message: "Please Wait...\n\n\n\n",
                                              preferredStyle: .alert)
      SaveAlertHandle.set(alertController)
      let spinner = UIActivityIndicatorView(style: .whiteLarge)
      spinner.color = UIColor(ciColor: .black)
      spinner.center = CGPoint(x: alertController.view.frame.midX,
                               y: alertController.view.frame.midY)
      spinner.autoresizingMask = [.flexibleBottomMargin, .flexibleTopMargin,
                                  .flexibleLeftMargin, .flexibleRightMargin]
      spinner.startAnimating()
      alertController.view.addSubview(spinner)
      present(alertController, animated: true, completion: completion)
    }

    /*! @fn hideSpinner
     @brief Hides the please wait spinner.
     @param completion Called after the spinner has been hidden.
     */
    func hideSpinner(_ completion: (() -> Void)?) {
      if let controller = SaveAlertHandle.get() {
        SaveAlertHandle.clear()
        controller.dismiss(animated: true, completion: completion)
      }
    }
}

private class SaveAlertHandle {
  static var alertHandle: UIAlertController?

  class func set(_ handle: UIAlertController) {
    alertHandle = handle
  }

  class func clear() {
    alertHandle = nil
  }

  class func get() -> UIAlertController? {
    return alertHandle
  }
}

Solution

  • I solved this by adding the function to the scenedelegate, as opposed to the app delegate.

    func scene(_ scene: UIScene, continue userActivity: NSUserActivity) {
        //print(#function)
    
        
        if let url = userActivity.webpageURL {
            self.handlePasswordlessSignIn(withURL: url)
        }
    }