Skip to content

Conversation

peterfriese
Copy link
Contributor

@peterfriese peterfriese commented Sep 28, 2025

To better align the Firebase Auth SDK with modern Swift concurrency, this PR introduces AsyncStream-based APIs for observing authentication state and ID token changes.

Key Features & Benefits

  • Modern Concurrency: New authStateChanges and idTokenChanges properties on Auth return an AsyncStream<User?>, allowing developers to use for await loops to monitor authentication events, which is more idiomatic in modern Swift.
  • Improved Ergonomics: These new APIs provide a more intuitive and readable alternative to the traditional closure-based listeners (addStateDidChangeListener and addIDTokenDidChangeListener).
  • Resource Safety: The streams are designed to be safely cancelled, preventing potential resource leaks. This is validated by comprehensive unit tests.

Implementation Details

  • The new async properties are housed in a new FirebaseAuth/Sources/Auth+Async.swift file.
  • Unit tests have been added in FirebaseAuth/Tests/Unit/AuthAsyncTests.swift to cover sign-in, sign-out, token refresh, and stream cancellation scenarios.
  • The FirebaseAuth/CHANGELOG.md has been updated to reflect these new features.
  • The new APIs are documented with clear usage examples, following Apple's documentation best practices.

Example Usage

Monitoring Authentication State

func monitorAuthState() async {
  for await user in Auth.auth().authStateChanges {
    if let user = user {
      print("User signed in: \(user.uid)")
      // Update UI or perform actions for a signed-in user.
    } else {
      print("User signed out.")
      // Update UI or perform actions for a signed-out state.
    }
  }
}

Monitoring ID Token Changes

func monitorIDTokenState() async {
  for await user in Auth.auth().idTokenChanges {
    if let user = user {
      print("ID token changed for user: \(user.uid)")
      // The user's ID token has been refreshed.
      // You can get the new token by calling user.getIDToken()
      do {
        let token = try await user.getIDToken()
        print("New ID token: \(token)")
        // Send the new token to your backend server, etc.
      } catch {
        print("Error getting ID token: \(error)")
      }
    }
  }
}

This commit introduces an `AsyncStream`-based API for observing authentication state changes, aligning the Firebase Auth SDK with modern Swift concurrency patterns.

The new `authStateChanges` computed property on `Auth` returns an `AsyncStream<User?>` that emits the current user whenever the authentication state changes. This provides a more ergonomic alternative to the traditional closure-based `addStateDidChangeListener`.

Key changes include:
- The implementation of `authStateChanges` in a new `Auth+Async.swift` file.
-  Comprehensive unit tests in `AuthAsyncTests.swift` covering the stream's behavior for sign-in, sign-out, and cancellation scenarios to prevent resource leaks.
- An entry in the `FirebaseAuth/CHANGELOG.md` for the new feature.
- Detailed API documentation for `authStateChanges`, including a clear usage example, following Apple's documentation best practices.
@peterfriese peterfriese self-assigned this Sep 28, 2025
@peterfriese peterfriese linked an issue Sep 28, 2025 that may be closed by this pull request
This commit introduces an `AsyncStream`-based API for observing ID token changes, aligning the Firebase Auth SDK with modern Swift concurrency patterns.

The new `idTokenChanges` computed property on `Auth` returns an `AsyncStream<User?>` that emits the current user whenever the ID token changes. This provides a more ergonomic alternative to the traditional closure-based `addIDTokenDidChangeListener`.

Key changes include:
- The implementation of `idTokenChanges` in `Auth+Async.swift`.
- Comprehensive unit tests in `IdTokenChangesAsyncTests.swift` covering the stream's behavior.
- Renamed `AuthAsyncTests.swift` to `AuthStateChangesAsyncTests.swift` to better reflect its content.
@peterfriese peterfriese marked this pull request as ready for review September 30, 2025 09:06
@ncooke3
Copy link
Member

ncooke3 commented Oct 2, 2025

/gemini review

Copy link
Contributor

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces AsyncStream-based APIs for observing authentication state and ID token changes, which is a great step towards modernizing the Firebase Auth SDK for Swift concurrency. The implementation is clean, well-documented, and includes a good set of unit tests.

My review includes a few suggestions to enhance the new APIs and tests:

  • Broadening the API availability to support older OS versions where Swift concurrency is available.
  • Improving the robustness of the unit tests by replacing Task.sleep with expectations.
  • Refactoring duplicated setup code in the tests to improve maintainability.

Comment on lines 48 to 49
@available(iOS 18.0, *)
var authStateChanges: some AsyncSequence<User?, Never> {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The @available(iOS 18.0, *) attribute seems overly restrictive. Swift's AsyncStream and opaque some AsyncSequence return types are available on earlier OS versions (iOS 13+, macOS 10.15+, etc.). To maximize the utility of this new API for developers, consider lowering the availability to align with the introduction of Swift's concurrency features, for example:

@available(iOS 13.0, macOS 10.15, macCatalyst 13.0, tvOS 13.0, watchOS 7.0, *)

This would also make it consistent with the availability of the Auth class itself. This change should be applied to idTokenChanges on line 90 as well, and the corresponding test functions in AuthStateChangesAsyncTests.swift and IdTokenChangesAsyncTests.swift.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a valid comment. We only need to gate on iOS 18 if Never was an Error here.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does the current callback-based listener forward errors to the client's listener block? If so, is the listener cancelled?

Copy link
Contributor Author

@peterfriese peterfriese Oct 2, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The protocol is defined as follows:

@available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *)
public protocol AsyncSequence<Element, Failure> {

    /// The type of asynchronous iterator that produces elements of this
    /// asynchronous sequence.
    associatedtype AsyncIterator : AsyncIteratorProtocol

    /// The type of element produced by this asynchronous sequence.
    associatedtype Element where Self.Element == Self.AsyncIterator.Element

    /// The type of errors produced when iteration over the sequence fails.
    @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *)
    associatedtype Failure = any Error where Self.Failure == Self.AsyncIterator.Failure

    /// Creates the asynchronous iterator that produces elements of this
    /// asynchronous sequence.
    ///
    /// - Returns: An instance of the `AsyncIterator` type used to produce
    /// elements of the asynchronous sequence.
    func makeAsyncIterator() -> Self.AsyncIterator
}

When using @available(iOS 13.0, macOS 10.15, macCatalyst 13.0, tvOS 13.0, watchOS 7.0, *), the compiler says that Failure is only available on iOS 18+.

CleanShot 2025-10-02 at 19 02 44

I am happy to use the more complete availability spec on the function, but unless I am missing something, the only other way around it is to return the concrete type AsyncStream<User?> (like we did in a previous version of this branch).

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does the current callback-based listener forward errors to the client's listener block? If so, is the listener cancelled?

No - the callback based listener doesn't throw errors.

Comment on lines 48 to 49
@available(iOS 18.0, *)
var authStateChanges: some AsyncSequence<User?, Never> {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a valid comment. We only need to gate on iOS 18 if Never was an Error here.

Comment on lines 48 to 49
@available(iOS 18.0, *)
var authStateChanges: some AsyncSequence<User?, Never> {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does the current callback-based listener forward errors to the client's listener block? If so, is the listener cancelled?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Implement AsyncStream support for Firebase Auth
3 participants