6

I'm trying and failing badly to implement the cool Firebase email link login feature. I successfully setup sending an email link. However, I can't get the email link to open up the app. It just opens up the preview page like it can't open the app.

I've tested the dynamic link I setup and I can get it to open up the app in a device. I just can't get the email link to do the same.

Code in my app:

func sendFirebaseEmailLink() {

    let actionCodeSettings = ActionCodeSettings.init()

    // userEmail comes from a textField
    let email = userEmail

    actionCodeSettings.url = URL.init(string: String(format: "https://<myappname>.firebaseapp.com/?email=%@", email))
    // The sign-in operation has to always be completed in the app.
    actionCodeSettings.handleCodeInApp = true
    actionCodeSettings.setIOSBundleID(Bundle.main.bundleIdentifier!)

    Auth.auth().sendSignInLink(toEmail: email,
        actionCodeSettings: actionCodeSettings) { error in

        if let error = error {
            print(error.localizedDescription)
            return
        }
        else {
            UserDefaults.standard.set(email, forKey: "Email")
            print("email sent to user")
        }
    }
}

When I say I've successfully gotten my dynamic link to open the app what I mean is when I follow the link I created (mylinkname.page.link/emaillogin) on a device that has the app installed, it opens the app. Because of that and [this helpful Firebase video][1] on setting up a dynamic link it seems like I've got those details correct and the issue is with the code, but I'm new to this so I'm not sure.

I've spend few days going around in circles to figure this out, and trying to parse the dense Firebase documentation, so any ideas are greatly appreciated.

Ben
  • 3,346
  • 6
  • 32
  • 51

2 Answers2

6

I finally figured it out. The code was fine. It was an issue related to the dynamic link. I had a couple links setup in Firebase because I had to create a new Bundle ID at one point. When I deleted out the old one in Firebase the email link started working.

It shows up in my app association site like this, and oddly still does even though I deleted out the old link, but at least it works now!

{"applinks":{"apps":[],"details":[{"appID":"TEAMID.com.OLDBUNDLEIDENTIFIER.APPNAME","paths":["NOT //*","/*"]},{"appID":"TEAMID.com.NEWBUNDLEIDENTIFIER.APPNAME","paths":["NOT //","/"]}]}}

UPDATE: My full code to implement passwordless email login is below. It was painful for me to piece together using the documentation so hopefully this saves you the trouble.

Key steps assuming you understand the basics of Firebase Setup.

1) Setup a Dynamic Link Using the Firebase Video tutorial.

2) Code in View Controller:

var userEmail: String?
var link: String?

func sendFirebaseEmailLink() {

    let actionCodeSettings = ActionCodeSettings.init()
    let email = userEmail
    actionCodeSettings.url = URL.init(string: String(format: "https://<myappname>.page.link/emaillogin/?email=%@", email!))
    // The sign-in operation has to always be completed in the app.
    actionCodeSettings.handleCodeInApp = true
    actionCodeSettings.setIOSBundleID(Bundle.main.bundleIdentifier!)

    Auth.auth().sendSignInLink(toEmail: email!,
        actionCodeSettings: actionCodeSettings) { error in

        if let error = error {
            print(error.localizedDescription)
            return
        }
        else {
            UserDefaults.standard.set(email, forKey: "Email")
            print("email sent to user")
        }

        // TODO: Notify user to check email and click the link.
    }
}

// Sign in user after they clicked email link called from AppDelegate
@objc func signInUserAfterEmailLinkClick() {

    // Get link url string from the dynamic link captured in AppDelegate.
    if let link = UserDefaults.standard.value(forKey: "Link") as? String {
        self.link = link
    }

    // Sign user in with the link and email.
    Auth.auth().signIn(withEmail: userEmail!, link: link!) { (result, error) in

        if error == nil && result != nil {

            if (Auth.auth().currentUser?.isEmailVerified)! {
                print("User verified with passwordless email")

                // TODO: Do something after user verified like present a new View Controller

            }
            else {
                print("User NOT verified by passwordless email")

            }
        }
        else {
            print("Error with passwordless email verfification: \(error?.localizedDescription ?? "Strangely, no error avaialble.")")
        }   
    }
}

3) Code in AppDelegate

// For Passwordless Email Login to Handle Dynamic Link after User Clicks Email Link
func application(_ application: UIApplication, continue userActivity: NSUserActivity,
                 restorationHandler: @escaping ([UIUserActivityRestoring]?) -> Void) -> Bool {

    if let incomingURL = userActivity.webpageURL {
        print("Incoming URL is \(incomingURL)")

        // Parse incoming
        let linkHandled = DynamicLinks.dynamicLinks().handleUniversalLink(incomingURL) { (dynamicLink, error) in

            guard error == nil else {
                print("Found an error: \(error!.localizedDescription)")
                return
            }
            if let dynamicLink = dynamicLink {
                self.handleIncomingDynamicLink(dynamicLink)
            }
        }
        if linkHandled {
            return true
        }
        else {
            // Maybe do other things with dynamic links in future?
            return false
        }
    }
    return false
}

// Handles the link and saves it to userDefaults to assist with login.
func handleIncomingDynamicLink(_ dynamicLink: DynamicLink) {
    guard let url = dynamicLink.url else {
        print("My dynamic link object has no url")
        return
    }
    print("Incoming link parameter is \(url.absoluteString)")

    let link = url.absoluteString
    if Auth.auth().isSignIn(withEmailLink: link) {

        // Save link to userDefaults to help finalize login.
        UserDefaults.standard.set(link, forKey: "Link")

        // Send notification to ViewController to push the First Time Login VC
        NotificationCenter.default.post(
            name: Notification.Name("SuccessfulPasswordlessEmailNotification"), object: nil, userInfo: nil)
    }
}
Ben
  • 3,346
  • 6
  • 32
  • 51
  • I've a similar problem. Most people can authenticate with the link, but some can't. It opens the App Store, shows the app as installed, user opens the app but then the login process is broken. Any idea on where the problem can be? – Dpedrinha Feb 20 '20 at 15:30
  • @Dpedriha I suggest posting a separate question about this with your code. – Ben Feb 22 '20 at 15:38
  • the code is working for 95% of the users, so the problem is not in the code. – Dpedrinha Nov 04 '21 at 12:55
  • 2
    I have had a similar issue. The imperfect solution has been to de-prioritize the link login approach and offer a password login option as well. – Ben Nov 08 '21 at 18:50
0

For anyone using SwiftUI with AppDelegate and SceneDelegate files instead of UIKit, here's what I've done:

  1. Create a function to send a link to the user's email
func sendSignLink(email: String) async throws {
    
do {
        let actionCodeSettings = ActionCodeSettings()
        actionCodeSettings.url = URL(string: "*enter your Firebase Dynamic link here*")
        actionCodeSettings.handleCodeInApp = true
        actionCodeSettings.setIOSBundleID(Bundle.main.bundleIdentifier!)
        
        try await Auth.auth().sendSignInLink(toEmail: email, actionCodeSettings: actionCodeSettings)
        UserDefaults.standard.set(email, forKey: "email")
    }
    catch {
        throw error
    }
    
}
  1. In the SceneDelegate file, import FirebaseDynamicLinks and add the below code
    func scene(_ scene: UIScene, continue userActivity: NSUserActivity) {
                
        if let incomingURL = userActivity.webpageURL {
            
            print("\n \nIncoming URL is \(incomingURL)")
            
            _ = DynamicLinks.dynamicLinks().handleUniversalLink(incomingURL) { (dynamicLink, error) in
                
                guard error == nil else {
                    print("\n \nError with handling incoming URL: \(error!.localizedDescription)")
                    return
                }
                
                if let dynamicLink = dynamicLink {
                    
                    guard let url = dynamicLink.url else {
                        print("\n \nDynamic link object has no url")
                        return
                    }
                    
                    print("\n \nIncoming link parameter is \(url.absoluteString)")
                    
                    let link = url.absoluteString
                    
                    if Auth.auth().isSignIn(withEmailLink: link) {
                        
                        // Send notification to trigger the rest of the sign in sequence
                        NotificationCenter.default.post(name: Notification.Name("Success"), object: nil, userInfo: ["link": link])
                        
                    } else {
                        
                        // Send error notification
                        NotificationCenter.default.post(name: Notification.Name("Error"), object: nil, userInfo: nil)
                        
                    }
                    
                }
                
            }
        }
    }
  1. Create a function to handle the sign in after the user has clicked on the link in their email
func signInWithEmail(link: String) async throws {
    
    do {
        let email = UserDefaults.standard.value(forKey: "email")
        try await Auth.auth().signIn(withEmail: email, link: link)
    }
    catch {
        throw error
    }
    
}
  1. In a relevant view, handle the notifications which get posted
struct MyView: View {
    
    var body: some View {
        
        VStack {
            Text("View")
        }
        
        .onReceive(NotificationCenter.default.publisher(for: Notification.Name("Success"))) { notificationInfo in
            
            if let userInfo = notificationInfo.userInfo {
                if let link = userInfo["link"] as? String {
                    Task.init {
                        do {
                            try await signInWithEmail(link: link)
                        } catch {
                            print(error)
                            
                        }
                    }
                }
            }
        }
        
        
        .onReceive(NotificationCenter.default.publisher(for: Notification.Name("Error"))) { _ in
            //do something with error
        }
        
    }
}
Vaz
  • 309
  • 2
  • 15