Search code examples
swiftfirebasefirebase-authenticationios-app-extensionios-app-group

Firebase Auth user is outdated when using shared user access group


Sharing auth state between an app and its widget extension, but sometimes the auth state is not in sync. When the user signs in/out from my app, it refreshes the widget timeline, which then checks for the auth state to display the correct item. However, if I sign the user out, sometimes the call to Auth.auth().currentUser still returns a valid user.

I've confirmed that both apps are on the same user access group, and I have that app group enabled in each target's capabilities. Is there a lag between when one group updates auth state, and when another can access that state?

Widget code

    struct Provider: TimelineProvider {
        ...
        func getTimeline(in context: Context, completion: @escaping (Timeline<BirthdaysEntry>) -> Void) {
            if let uid = Auth.auth().currentUser?.uid {
                // fetch data from firestore
            } else {
                // logged out 
            }
        }
        ...
    }

    @main
    struct MyWidget: Widget {
        private let kind = "my_widget"
        
        init() {
            FirebaseApp.configure()
            if Auth.auth().userAccessGroup == nil {
                do {
                    try Auth.auth().useUserAccessGroup("group.com.****.******")
                } catch let error as NSError {
                    ...
                }
            }
        }
        
        var body: some WidgetConfiguration {
            StaticConfiguration(kind: kind, provider: Provider()) { entry in
                WidgetEntryView(entry: entry)
            }
        }
    }

App code

    // part of main file
    class AppDelegate: NSObject, UIApplicationDelegate, UNUserNotificationCenterDelegate {
        func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey : Any]? = nil) -> Bool {
            FirebaseApp.configure()
            ...
            // migrate to shared keychain
            if Auth.auth().userAccessGroup == nil {
                let user = Auth.auth().currentUser
                do {
                    try Auth.auth().useUserAccessGroup("group.com.****.****") // same group
                } catch let error as NSError {
                    ...
                }
                // when we switch to app group, user will be set to nil, so
                // if user is logged in, update them in the app group
                if user != nil {
                    Auth.auth().updateCurrentUser(user!) { (err) in
                        if let err = err {
                            print(err.localizedDescription)
                        }
                    }
                }
            }
            return true
        }
    }
    
    // in a viewmodel somewhere else
    Auth.auth().addStateDidChangeListener { [weak self] (auth, user) in
        WidgetCenter.shared.reloadTimelines(ofKind: "my_widget")
        ....
    }

Solution

  • The documentation states:

    Note: Shared keychain does not automatically update users across apps in real time. If you make a change to a user in one app, the user must be reloaded in any other shared keychain apps before the changes will be visible.

    Here's some code to get you started:

    func refreshUser() {
        do {
          let currentUser = Auth.auth().currentUser
          let sharedUser = try Auth.auth().getStoredUser(forAccessGroup: accessGroup)
          print("Current user: \(String(describing: currentUser)), shared User: \(sharedUser.uid)")
          if currentUser != sharedUser {
            updateUser(user: sharedUser)
          }
        }
        catch {
          do {
            try Auth.auth().signOut()
          }
          catch {
            print("Error when trying to sign out: \(error.localizedDescription)")
          }
        }
      }
      
      func updateUser(user: User) {
        Auth.auth().updateCurrentUser(user) { error in
          if let error = error {
            print("Error when trying to update the user: \(error.localizedDescription)")
          }
        }
      }
    

    The following code shows how to use this in your main SwiftUI app, but can be easily adapted to a Widget:

    var body: some Scene {
        WindowGroup {
          ContentView()
            .environmentObject(authenticationService)
        }
        .onChange(of: scenePhase) { phase in
          print("Current phase \(phase)")
          if let user = Auth.auth().currentUser {
            print("User: \(user.uid)")
          }
          else {
            print("No user present")
          }
          
          if phase == .active {
            // Uncomment this to refresh the user once the app becomes active
            authenticationService.refreshUser()
          }
        }
      }