iOS端Firebase无密码登录:动态链接异常问题求助
Firebase无密码登录链接问题排查与解决
问题背景
已完成App与Firebase项目的配置,实现用户输入邮箱后接收无密码登录链接的流程。用户点击邮件链接可跳转至App,但AppDelegate的application(_:continue:restorationHandler:)及相关回调均未触发,导致后续认证流程无法执行。此外,桌面端点击该链接时,页面显示“站点未找到”提示。
核心问题
- 移动端跳转至App后,登录链接为何未被处理?
- 桌面端点击链接为何出现“站点未找到”提示?
相关代码
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
相关产品推荐
相关产品推荐

