You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

iOS端Firebase无密码登录:动态链接异常问题求助

Firebase无密码登录链接问题排查与解决

问题背景

已完成App与Firebase项目的配置,实现用户输入邮箱后接收无密码登录链接的流程。用户点击邮件链接可跳转至App,但AppDelegate的application(_:continue:restorationHandler:)及相关回调均未触发,导致后续认证流程无法执行。此外,桌面端点击该链接时,页面显示“站点未找到”提示。

核心问题

  1. 移动端跳转至App后,登录链接为何未被处理?
  2. 桌面端点击链接为何出现“站点未找到”提示?

相关代码

AppDelegate 相关方法

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: "")
}

认证ViewController代码

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")
        }
    }
}

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
  }
}

问题解答

问题1:移动端跳转至App后链接未被处理的原因及修复

1. Universal Links配置缺失或错误

  • 检查apple-app-site-association文件是否部署在https://my-app.firebaseapp.com/.well-known/路径下,文件需包含你的App的Team ID和Bundle ID,格式如下:
    {
      "applinks": {
        "apps": [],
        "details": [
          {
            "appID": "你的TeamID.你的BundleID",
            "paths": ["*"]
          }
        ]
      }
    }
    
  • 确认Xcode的Signing & Capabilities中,Associated Domains已添加applinks:my-app.page.link和applinks:my-app.firebaseapp.com,且Team ID与开发者账号一致。

2. iOS版本适配问题(iOS 13+)

iOS 13及以上版本使用SceneDelegate管理App生命周期,需在SceneDelegate中补充实现链接处理逻辑:

func scene(_ scene: UIScene, continue userActivity: NSUserActivity) {
    guard userActivity.activityType == NSUserActivityTypeBrowsingWeb, 
          let url = userActivity.webpageURL else {
        return
    }
    _ = handlePasswordlessSignIn(withURL: url)
}

同时需完善AppDelegate中旧版回调方法的实现:

func application(_ app: UIApplication, open url: URL, sourceApplication: String?, annotation: Any) -> Bool {
    print(#function)
    return handlePasswordlessSignIn(withURL: url)
}

3. Dynamic Link配置验证

  • 确认ActionCodeSettings中的dynamicLinkDomain与Firebase控制台“动态链接”页面配置的域名完全一致。
  • 检查url参数对应的域名(my-app.firebaseapp.com)已添加到Firebase控制台“Authentication”→“登录方法”→“授权域名”列表中。

问题2:桌面端点击链接显示“站点未找到”的原因及修复

1. Firebase Hosting未部署内容

actionCodeSettings.url设置的https://my-app.firebaseapp.com是Firebase Hosting域名,若未部署任何静态页面,访问时会显示“站点未找到”。

  • 解决:在Firebase Hosting中部署一个简单的静态页面(如提示用户打开App登录的页面);若不需要桌面端登录,可修改url为已部署内容的域名,或保留原域名但完成Hosting部署。

2. 动态链接域名配置异常

确认my-app.page.link域名已在Firebase控制台“动态链接”中正确创建,且域名状态正常(未过期、未被禁用)。


内容的提问来源于stack exchange,提问作者Moses Harding

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.07 09:27:04