1

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")
        ....
    }

1 Answer 1

3

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()
      }
    }
  }
Sign up to request clarification or add additional context in comments.

3 Comments

This is great, thanks Peter. I'll accept after I can test it later today. is it bad if I run refreshUser() from my getTimeline() function, or should I have it in a viewmodel?
I've got issues using this code. With the recent Firebase versions, currentUser == sharedUser evaluates to False, while currentUser?.uid == sharedUser?.uid evaluates to True !!
In my case I was doing some destructive actions when current and shared users were believed to not match. With the scope of your code here, you may not have a chance to realize your code logic is wrong.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.