简体   繁体   中英

Using SwiftUI, Combine, and Firebase, how do I verify that a user is signed in anonymously before linking their account to an email/password?

I have an AuthService with the following functions:

func signInAnon() -> AnyPublisher<User, AuthError> {
    return Future<User, AuthError> { promise in
        Auth.auth().signInAnonymously { result, error in
            if let error = error {
                return promise(.failure(.signInAnon(description: error.localizedDescription)))
            } else if let user = result?.user {
                return promise(.success(user))
            }
        }
    }.eraseToAnyPublisher()
}

func linkAccount(email: String, password: String) -> AnyPublisher<User, AuthError> {
    
    let emailCredential = EmailAuthProvider.credential(withEmail: email, password: password)
    
    return Future<User, AuthError> { promise in
        Auth.auth().currentUser?.link(with: emailCredential) { result, error in
            if let error = error {
                return promise(.failure(.linkAccount(description: error.localizedDescription)))
            } else if let user = result?.user {
                Auth.auth().updateCurrentUser(user) { error in
                    if let error = error {
                        return promise(.failure(.updateCurrentUser(description: error.localizedDescription)))
                    } else {
                        return promise(.success((user)))
                    }
                }
            }
        }
    } .eraseToAnyPublisher()
}

In my ViewModel, I want to link the user's anonymous account with an email/password credential. However, at the point of sign in, anonymous sign in has not happened.

Here is the code in my ViewModel:

case .signUp:

isLoading = true

authService.signInAnon()
    .timeout(.seconds(5), scheduler: DispatchQueue.main, options: nil, customError: { () -> AuthError in
        return .signInAnon(description: self.error?.localizedDescription)})
    .sink { completion in
        switch completion {
        case let .failure(error):
            self.error = error
        case .finished:
            print("User signed in anonymously")
            self.error = nil
            }
    } receiveValue: { user in
        self.user = user
    }
    .store(in: &cancellables)

authService.linkAccount(email: email, password: password)
    .timeout(.seconds(5), scheduler: DispatchQueue.main, options: nil, customError: { () -> AuthError in
        return .linkAccount(description: self.error?.localizedDescription)})
    .sink { completion in
    self.isLoading = false
    switch completion {
    case let .failure(error):
        self.error = error
        self.alertIsPresented = true
        print(error.localizedDescription)
    case .finished:
        print("Sign up successful")
    }
} receiveValue: { user in
    self.user = user
}
.store(in: &cancellables)

I am running into problems because sometimes the function linking the accounts finishes before the anonymous sign in function, presenting an error.

How can I write this conditionally so that there is a check to see if the user is signed in anonymously? If they are, link their account. If they are not, sign them in anonymously, and then link their account.

Here's my recommendation for how to use Firebase Anonymous Auth:

  1. As soon as your app has launched, initialise Firebase and check if a user is signed in. If a user has previously signed in, Firebase Auth will have fetched that user on time before you call this method.
  2. If no user is present, sign in anonymously.
  3. You can then start using all Firebase services that benefit from a signed-in user (such as Firestore)
  4. Once the user decides to actually sign in, use any Firebase Authentication provider to get a token, and link that to the existing anonymous user. BTW, I recommend using something more secure than Email/Password.

Sample code for performing anonymous sign-in at app start-up (taken from this sample app ):

func signIn() {
  if Auth.auth().currentUser == nil {
    Auth.auth().signInAnonymously()
  }
}

Sample code for upgrading to a Sign in with Apple account (taken from the same sample app ):

      case .link:
        if let currentUser = Auth.auth().currentUser {
          currentUser.link(with: credential) { (result, error) in
            if let error = error, (error as NSError).code == AuthErrorCode.credentialAlreadyInUse.rawValue {
              print("The user you're signing in with has already been linked, signing in to the new user and migrating the anonymous users [\(currentUser.uid)] tasks.")
              
              if let updatedCredential = (error as NSError).userInfo[AuthErrorUserInfoUpdatedCredentialKey] as? OAuthCredential {
                print("Signing in using the updated credentials")
                Auth.auth().signIn(with: updatedCredential) { (result, error) in
                  if let user = result?.user {
                    currentUser.getIDToken { (token, error) in
                      if let idToken = token {
                        (self.taskRepository as? FirestoreTaskRepository)?.migrateTasks(from: idToken)
                        self.doSignIn(appleIDCredential: appleIDCredential, user: user)
                      }
                    }
                  }
                }
              }
            }
            else if let error = error {
              print("Error trying to link user: \(error.localizedDescription)")
            }
            else {
              if let user = result?.user {
                self.doSignIn(appleIDCredential: appleIDCredential, user: user)
              }
            }
          }
        }

If you're interested in how this would like for Combine, we've started implementing Combine support for Firebase - check out our project tracker

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM