0

I feel like there is an obvious solution to this that is on the tip of my brain, but I can't seem to figure it out. I am using the FirebaseAuth library, so I can't edit it (or I don't want to go down that path). The function getIDTokenForcingRefresh() uses dispatch_async. This would be a lot easier if it used the async/await functionality from Swift 5.5, but I have to rely on solutions for dispatch_async. I need to grab the output to run a Firebase function request using the token. The following obviously doesn't work because the function will return before getIDTokenForcingRefresh() is finished. I don't care if the main thread is blocked because the user can't proceed in the app until this is done.

    var userToken: String {

        print("FIREBASE: Getting User Token")
        var token = ""
        Auth.auth().currentUser?.getIDTokenForcingRefresh(true) { idToken, error in
            if let error = error {
                print("FIREBASE: There was an error getting the user token")
                return
            }
            print("FIREBASE: Got user token: \(idToken ?? "NONE")")
            token = idToken ?? ""
        }
        print("FIREBASE: Token: \(token)")
        return token
    }
tharris
  • 2,192
  • 2
  • 13
  • 14
  • You are trying to do asynchronous work synchronously. You can't do this in a computed variable, you can use `async await` or completion handlers – lorem ipsum Jun 17 '23 at 16:48
  • 1
    In addition to @loremipsum 's comment it's possible with an `async` computed property. – vadian Jun 17 '23 at 17:01

2 Answers2

1

Even with a completion handler method like getIDTokenForcingRefresh you can take advantage of async/await by wrapping the asynchronous function with completion handler in a Continuation. The error handling is for free

func getUserToken() async throws -> String {
    print("FIREBASE: Getting User Token")
    return withCheckedThrowingContinuation { continuation in
        Auth.auth().currentUser?.getIDTokenForcingRefresh(true) { idToken, error in
            if let error {
                print("FIREBASE: There was an error getting the user token: \(error.localizedDescription)")
                continuation.resume(throwing: error)
            } else {
                print("FIREBASE: Got user token: \(idToken ?? "NONE")")
                continuation.resume(returning: idToken!)
            }
        }
    }
}

And use it

Task {
    do {
        let token = try await getUserToken()
    } catch {
        print(error)
    }
}

Doesn't Firebase support async/await meanwhile?

vadian
  • 274,689
  • 30
  • 353
  • 361
  • This is great. I never knew this. Based on your previous comment I came up with an async computed property solution, but your code might be even better. I will try it. Thank you! – tharris Jun 17 '23 at 17:26
  • 1
    As Firebase obviously does support `async/await` a *native* solution is always preferable. The `Continuation` is just a way to bridge completion handlers to `async/await`. – vadian Jun 17 '23 at 17:32
-2

EDIT: Thanks to vadian's comment above and HangerRash's suggestion, an async computed property is better:

extension Auth {
    var currentUserToken: Task<String, Error> {
        Task { @MainActor in
            do {
                let idToken = try await currentUser?.getIDTokenResult()
                print("FIREBASE: Got user token: \(idToken?.token ?? "NONE")")
                return idToken?.token ?? ""
            } catch {
                print("FIREBASE: There was an error getting the user token")
                throw error
            }
        }
    }
}
        var token = ""
        do {
            token = try await Auth.auth().currentUserToken.value
            //print("FIREBASE: Token: \(token)")
            // Proceed with the code that depends on the token
        } catch {
            print("FIREBASE: Error retrieving user token: \(error)")
            // Handle the error
        }
        
        print("FIREBASE: Token = \(token)")

Old, non-optimal semaphore way

I was able to figure this out using semaphore:

    func getUserToken(completion: @escaping (String?) -> Void) {
        print("FIREBASE: Getting User Token")
        Auth.auth().currentUser?.getIDTokenForcingRefresh(true) { idToken, error in
            if let error = error {
                print("FIREBASE: There was an error getting the user token: \(error.localizedDescription)")
                completion(nil)
            } else {
                print("FIREBASE: Got user token: \(idToken ?? "NONE")")
                completion(idToken)
            }
        }
    }
    
    func getUserTokenBlocking() -> String {
        var userToken: String = ""
        let semaphore = DispatchSemaphore(value: 0)
        
        GPTClient.getUserToken { token in
            userToken = token ?? ""
            semaphore.signal()
        }
        
        semaphore.wait()
        return userToken
    }

Now when I want to grab the token I use:

let token = getUserTokenBlocking()

I obviously need to do some error handling, and this is only useful when you WANT to block the main thread, which usually isn't the right answer but works for me in this situation.

tharris
  • 2,192
  • 2
  • 13
  • 14
  • 1
    *"and this is only useful when you WANT to block the main thread*" - but you never want to block the main thread. iOS could terminate your app if it blocks too long. What happens when a user has a really bad Internet connection and the call takes too long? – HangarRash Jun 17 '23 at 17:09