firebase / quickstart-unity

Firebase Quickstart Samples for Unity
https://firebase.google.com/games
Apache License 2.0
819 stars 424 forks source link

Using Firebase Firestore Listener with Firebase Authentication Freezes in Editor every time #1296

Closed og1337og closed 1 year ago

og1337og commented 1 year ago

[REQUIRED] Please describe the issue here:

Followed the tutorial on the firebase youtube channel for Firestore listener that updates on firestore and on the game as a text field using a getCharacterData script that uses a characterData struct to listen for updates and a setCharacterData script for writing to firestore from input fields on button press. The game also has a Login and Registration function with Firebase Authentication in a seperate Auth script. Both the set of 3 Firestore Scripts and the 1 Authentication scripts work fine alone. The issue that keeps occurring is when both are included the unity editor freezes when running the game to test. Removing either of them solves this for the other. Is there a way to use Authentication and FireStore in the same App that will resolve this issue? I get no error codes. The editor just freezes on run. :/ Based on it crashing I assume the issue is the start function setting firestore as a default instance of firebase and running the listener on the GetPlayerData Script or the Initialize Firebase on the Awake function for Authentication on the Auth script. I am guessing they are clashing since removing the game object with the scripts solves the issues for the other and does not crash the game when running the unity editor. Sorry if this was too detailed or not detailed enough, this is my first question on here, I have honestly tried so hard to make this work and have hit a full dead end with no error codes to guide me and no documentation that solves this issue. Any help would be greatly appreciated.

Steps to reproduce:

Running a firestore listener with an authentication script and running the game in unity crashes unity

Relevant Code:

//Firestore Get Player Data Listener code snippet private ListenerRegistration listenerRegistration;

void Start()
{
    var firestore = FirebaseFirestore.DefaultInstance;

    listenerRegistration = firestore.Document(playerPath).Listen(snapshot =>
    {
        var playerData = snapshot.ConvertTo<PlayerData>();

        userNameText.text = $"UserName: {playerData.UserName}";
        factionText.text = $"Faction: {playerData.Faction}";
    });
}

//Awake and Initialize Firebase Functions for Auth

void Awake() { //Check that all of the necessary dependencies for Firebase are present on the system FirebaseApp.CheckAndFixDependenciesAsync().ContinueWith(task => { dependencyStatus = task.Result; if (dependencyStatus == DependencyStatus.Available) { //If they are avalible Initialize Firebase InitializeFirebase(); } else { Debug.LogError("Could not resolve all Firebase dependencies: " + dependencyStatus); } }); }

private void InitializeFirebase()
{
    Debug.Log("Setting up Firebase Auth");
    //Set the authentication instance object
    auth = FirebaseAuth.DefaultInstance;
}
// TODO(you): code here to reproduce the problem

//Code to reproduce

//The PlayerData Struct used for listener

[FirestoreData]

public struct PlayerData { [FirestoreProperty]

public string UserName { get; set; }

[FirestoreProperty]

public string Faction { get; set; }

}

//The Set Player Data Script

public class SetPlayerData : MonoBehaviour {

[SerializeField] private string playerPath = "Users/Player1";

[SerializeField] private TMP_InputField userName;
[SerializeField] private TMP_InputField faction;

[SerializeField] private Button submitButton;

void Start()
{
    submitButton.onClick.AddListener(() =>
    {

        var playerData = new PlayerData
        {
            UserName = userName.text,
            Faction = faction.text,
        };

        var firestore = FirebaseFirestore.DefaultInstance;
        firestore.Document(playerPath).SetAsync(playerData);
    });
}

}

//Get Player Data Script

public class GetPlayerData : MonoBehaviour { [SerializeField] private string playerPath = "Users/Player1";

[SerializeField] private TMP_Text userNameText;

[SerializeField] private TMP_Text factionText;

private ListenerRegistration listenerRegistration;

void Start()
{
    var firestore = FirebaseFirestore.DefaultInstance;

    listenerRegistration = firestore.Document(playerPath).Listen(snapshot =>
    {
        var playerData = snapshot.ConvertTo<PlayerData>();

        userNameText.text = $"UserName: {playerData.UserName}";
        factionText.text = $"Faction: {playerData.Faction}";
    });
}

void OnDestroy()
{
    listenerRegistration.Stop();
}

} //End of Firestore Scripts

//The Auth Script

public class Auth : MonoBehaviour { //Firebase variables [Header("Firebase")] public DependencyStatus dependencyStatus; public FirebaseAuth auth; public FirebaseUser User;

//Login variables
[Header("Login")]
public TMP_InputField emailLoginField;
public TMP_InputField passwordLoginField;
public TMP_Text warningLoginText;
public TMP_Text confirmLoginText;

//Register variables
[Header("Register")]
public TMP_InputField usernameRegisterField;
public TMP_InputField emailRegisterField;
public TMP_InputField passwordRegisterField;
public TMP_InputField passwordRegisterVerifyField;
public TMP_Text warningRegisterText;

void Awake()
{
    //Check that all of the necessary dependencies for Firebase are present on the system
    FirebaseApp.CheckAndFixDependenciesAsync().ContinueWith(task =>
    {
        dependencyStatus = task.Result;
        if (dependencyStatus == DependencyStatus.Available)
        {
            //If they are avalible Initialize Firebase
            InitializeFirebase();
        }
        else
        {
            Debug.LogError("Could not resolve all Firebase dependencies: " + dependencyStatus);
        }
    });
}

private void InitializeFirebase()
{
    Debug.Log("Setting up Firebase Auth");
    //Set the authentication instance object
    auth = FirebaseAuth.DefaultInstance;
}

//Function for the login button
public void LoginButton()
{
    //Call the login coroutine passing the email and password
    StartCoroutine(Login(emailLoginField.text, passwordLoginField.text));
}
//Function for the register button
public void RegisterButton()
{
    //Call the register coroutine passing the email, password, and username
    StartCoroutine(Register(emailRegisterField.text, passwordRegisterField.text, usernameRegisterField.text));
}

private IEnumerator Login(string _email, string _password)
{
    //Call the Firebase auth signin function passing the email and password
    var LoginTask = auth.SignInWithEmailAndPasswordAsync(_email, _password);
    //Wait until the task completes
    yield return new WaitUntil(predicate: () => LoginTask.IsCompleted);

    if (LoginTask.Exception != null)
    {
        //If there are errors handle them
        Debug.LogWarning(message: $"Failed to register task with {LoginTask.Exception}");
        FirebaseException firebaseEx = LoginTask.Exception.GetBaseException() as FirebaseException;
        AuthError errorCode = (AuthError)firebaseEx.ErrorCode;

        string message = "Login Failed!";
        switch (errorCode)
        {
            case AuthError.MissingEmail:
                message = "Missing Email";
                break;
            case AuthError.MissingPassword:
                message = "Missing Password";
                break;
            case AuthError.WrongPassword:
                message = "Wrong Password";
                break;
            case AuthError.InvalidEmail:
                message = "Invalid Email";
                break;
            case AuthError.UserNotFound:
                message = "Account does not exist";
                break;
        }
        warningLoginText.text = message;
    }
    else
    {
        //User is now logged in
        //Now get the result
        User = LoginTask.Result;
        Debug.LogFormat("User signed in successfully: {0} ({1})", User.DisplayName, User.Email);
        warningLoginText.text = "";
        confirmLoginText.text = "Logged In";
    }
}

private IEnumerator Register(string _email, string _password, string _username)
{
    if (_username == "")
    {
        //If the username field is blank show a warning
        warningRegisterText.text = "Missing Username";
    }
    else if (passwordRegisterField.text != passwordRegisterVerifyField.text)
    {
        //If the password does not match show a warning
        warningRegisterText.text = "Password Does Not Match!";
    }
    else
    {
        //Call the Firebase auth signin function passing the email and password
        var RegisterTask = auth.CreateUserWithEmailAndPasswordAsync(_email, _password);
        //Wait until the task completes
        yield return new WaitUntil(predicate: () => RegisterTask.IsCompleted);

        if (RegisterTask.Exception != null)
        {
            //If there are errors handle them
            Debug.LogWarning(message: $"Failed to register task with {RegisterTask.Exception}");
            FirebaseException firebaseEx = RegisterTask.Exception.GetBaseException() as FirebaseException;
            AuthError errorCode = (AuthError)firebaseEx.ErrorCode;

            string message = "Register Failed!";
            switch (errorCode)
            {
                case AuthError.MissingEmail:
                    message = "Missing Email";
                    break;
                case AuthError.MissingPassword:
                    message = "Missing Password";
                    break;
                case AuthError.WeakPassword:
                    message = "Weak Password";
                    break;
                case AuthError.EmailAlreadyInUse:
                    message = "Email Already In Use";
                    break;
            }
            warningRegisterText.text = message;
        }
        else
        {
            //User has now been created
            //Now get the result
            User = RegisterTask.Result;

            if (User != null)
            {
                //Create a user profile and set the username
                UserProfile profile = new UserProfile { DisplayName = _username };

                //Call the Firebase auth update user profile function passing the profile with the username
                var ProfileTask = User.UpdateUserProfileAsync(profile);
                //Wait until the task completes
                yield return new WaitUntil(predicate: () => ProfileTask.IsCompleted);

                if (ProfileTask.Exception != null)
                {
                    //If there are errors handle them
                    Debug.LogWarning(message: $"Failed to register task with {ProfileTask.Exception}");
                    FirebaseException firebaseEx = ProfileTask.Exception.GetBaseException() as FirebaseException;
                    AuthError errorCode = (AuthError)firebaseEx.ErrorCode;
                    warningRegisterText.text = "Username Set Failed!";
                }
                else
                {
                    //Username is now set
                    //Now return to login screen
                    UIManager.instance.LoginScreen();
                    warningRegisterText.text = "";
                }
            }
        }
    }
}

}

google-oss-bot commented 1 year ago

This issue does not seem to follow the issue template. Make sure you provide all the required information.

og1337og commented 1 year ago

Fixed post to follow Template for the bot.

paulinon commented 1 year ago

Hi @og1337og,

Thanks for reporting this issue. Based on the code you provided, it seems that you're declaring two Firestore instances in your app. This may be related to an issue (#928) with how persistence is implemented when running on desktop.

Could you confirm if disabling persistence makes a difference? You may do so by adding something like:

#if UNITY_EDITOR
  FirebaseFirestore.DefaultInstance.Settings.PersistenceEnabled = false;
#endif

If the crash still occurs, please provide your Player.log file.

Additionally, does the crash occur using an Android build?

og1337og commented 1 year ago

Hello, Yes the issues was that I was calling two default instances. One for Auth to handle User Login, Register and UID check for Documnet Name but also another for the Firestore Database to have a listener on the user's database info. The 2 of them running at the same time using

FirebaseApp.CheckAndFixDependenciesAsync().ContinueWith(async task => { }

in the Awake function was not compatible with 2 of them on the desktop and was freezing the app when testing in the editor.

Also, you were right, changing the build to Android allowed it to run without freezing. The only issue I had was it only allowed auth login and register and the write to firestore but the listener to read from firestore as updates came in was not working. I guess because it wouldn't allow two instances?

So I altered the code by adding the Get Player Data script to the Auth script both the declarations and the start function. Then modified the Awake function to

async void Awake() { //Check that all of the necessary dependencies for Firebase are present on the system // FirebaseApp.CheckAndFixDependenciesAsync().ContinueWith(async task => // { var dependencyStatus = await FirebaseApp.CheckAndFixDependenciesAsync(); if (dependencyStatus == DependencyStatus.Available) { //If they are avalible Initialize Firebase InitializeFirebase(); } else { Debug.LogError("Could not resolve all Firebase dependencies: " + dependencyStatus); } // }); }

You can see that I changed it to an async void Awake() and removed the

FirebaseApp.CheckAndFixDependenciesAsync().ContinueWith(async task => { }

The last thing I did was I added "await" in front of the FirebaseApp.CheckAndFixDependenciesAsync();

Using this method I can now run the game in Unity editor without the game freezing and it runs on Desktop using both an Auth DefaultInstance and a Firestore DefaultInstance so I can register a new user, update the Firestore database with their info and and use the listener on button click to update that information on their screen and still login in and out again with out any interference from either Firebase product. Thank you so much for taking time to help. Without error codes I felt lost, I hope this code helps others trying to run authentication and Firestore at the same time and run into this problem that want to keep being able to test in the editor. Thank you again.

paulinon commented 1 year ago

Glad to hear that you've found a solution, @og1337og. I'll be closing this for now. Let me know if a new issue arises with this,

og1337og commented 1 year ago

The work around unfortunatley only works on desktop. On android neither one works. When I added the FirebaseFirestore.DefaultInstance.Settings.PersistenceEnabled = false; in the awake method like you recomended the application stops crashing in the editor but on android only Firebase Authentication works. Its like this bit of code just disabled Firestore when using that line of code. Is there some workaround for this because at this point I don't think it is possible to use both Firestore and Authentication in the same app due to the default instance being required for both Firestore as db and Authentication as auth.

paulinon commented 1 year ago

Hi @og1337og,

Were you able to include the conditional compilation for the Unity editor (#if UNITY_EDITOR) in your code? That should ensure that the persistence is only disabled when using the editor.

Also, when you mentioned "neither one works", do you mean that Firestore doesn't function regardless whether the workaround is there or not?

If the issue is still there, it would be helpful if you share a minimal, reproducible example of your implementation so that we could analyze this behavior.

og1337og commented 1 year ago

Hello

The "neither one works" was in reference to the workaround I tried before that I thought worked but only worked on desktop, not in regards to the work around you suggested.

I did add all of what you said. At the start of the Awake function I added

if UNITY_EDITOR

FirebaseFirestore.DefaultInstance.Settings.PersistenceEnabled = false;

endif

Like you recommended. This makes both work on desktop, just a delay on the write to firestore but can still register users and login. Unfortunately when using the android build Firestore no longer works and does not let me write to the database but Register and Login still work.

Here is just the parts of the code that would be relevant for reference 0 Declerations 1 Awake 2 Start 3 Initialize Firebase 4 AuthStateChanged 5 Register 6 Write to Firestore

//Relevant Declarations public FirebaseAuth auth; public FirebaseUser user; FirebaseFirestore db; Dictionary<string, object> users;

//Relevant code //Awake, Start, Initialize Firebase, AuthStateChanged, Register, Write to Firestore

private void Awake() {

if UNITY_EDITOR

FirebaseFirestore.DefaultInstance.Settings.PersistenceEnabled = false;

endif

    DontDestroyOnLoad(gameObject);
    if (instance == null)
    {
        instance = this;
    }
    else if (instance != this)
    {
        Destroy(instance.gameObject);
        instance = this;
    }

    FirebaseApp.CheckAndFixDependenciesAsync().ContinueWith(checkDependancyTask =>
    {

    var dependancyStatus = checkDependancyTask.Result;

        if (dependancyStatus == DependencyStatus.Available)
        {
            InitializeFirebase();
        }
        else
        {
            Debug.LogError($"Could not resolve all Firebase dependencies: {dependancyStatus}");
        }
    });
}

private void Start()
{
      db = FirebaseFirestore.DefaultInstance;
}

private void InitializeFirebase() { auth = FirebaseAuth.DefaultInstance;

    auth.StateChanged += AuthStateChanged;
    AuthStateChanged(this, null);
}

private void AuthStateChanged(object sender, System.EventArgs eventArgs)
{
    if(auth.CurrentUser != null)
    {
        bool signedIn = user != auth.CurrentUser && auth.CurrentUser != null;

        if(!signedIn && user != null)
        {
            Debug.Log("Signed Out");
        }
        user = auth.CurrentUser;

        if (signedIn)
        {
            Debug.Log($"Signed In: {user.DisplayName}");
        }
    }
}

//Register Method private IEnumerator RegisterLogic(string _username, string _email, string _password, string _confirmPassword) { if(_username == "") { registerOutputText.text = "Please Enter A Username"; } else if (_password != _confirmPassword) { registerOutputText.text = "Passwords Do Not Match"; } else { var registerTask = auth.CreateUserWithEmailAndPasswordAsync(_email, _password);

        yield return new WaitUntil(predicate: () => registerTask.IsCompleted);

        if(registerTask.Exception != null)
        {
            FirebaseException firebaseException = (FirebaseException)registerTask.Exception.GetBaseException();
            AuthError error = (AuthError)firebaseException.ErrorCode;
            string output = "Unkown Error, Please Try Again";

            switch (error)
            {
                case AuthError.InvalidEmail:
                    output = "Invalid Email";
                    break;
                case AuthError.EmailAlreadyInUse:
                    output = "Email Already In Use";
                    break;
                case AuthError.WeakPassword:
                    output = "Weak Password";
                    break;
                case AuthError.MissingEmail:
                    output = "Please Enter Your Email";
                    break;
                case AuthError.MissingPassword:
                    output = "Please Enter Your Password";
                    break;

            }
            registerOutputText.text = output;
        }
        else
        {
            UserProfile profile = new UserProfile
            {
                DisplayName = _username,

                //TODO Give Profile Default Photo
            };

          var defaultUserTask = user.UpdateUserProfileAsync(profile);

            yield return new WaitUntil(predicate: () => defaultUserTask.IsCompleted);

            if (defaultUserTask.Exception != null)
            {
                user.DeleteAsync();
                FirebaseException firebaseException = (FirebaseException)defaultUserTask.Exception.GetBaseException();
                AuthError error = (AuthError)firebaseException.ErrorCode;
                string output = "Unkown Error, Please Try Again";

                switch (error)
                {
                    case AuthError.Cancelled:
                        output = "Update User Cancelled";
                        break;
                    case AuthError.SessionExpired:
                        output = "Session Expired";
                        break;                       
                }
                registerOutputText.text = output;
            }
            else
            {
                Debug.Log($"Firebase User Created Successfully: {user.DisplayName} ({user.UserId})");
                //ToDo: Send Verification Email

            }
        }
    }
}

//Write to FireStore Method

public void WriteStuff() { DocumentReference docRef = db.Collection("cities").Document("LA"); Dictionary<string, object> city = new Dictionary<string, object> { { "Name", "Los Angeles" }, { "State", "CA" }, { "Country", "USA" } }; docRef.SetAsync(city).ContinueWithOnMainThread(task => { Debug.Log("Added data to the LA document in the cities collection."); }); }

paulinon commented 1 year ago

Hi @og1337og,

So far, I haven't successfully built and replicated the issue. Is it possible for you to upload a sample project that reproduces the issue? You may do so by uploading this to a GitHub repository and adding me as a collaborator.

og1337og commented 1 year ago

I've created a repository that has the scripts used and the scenes used in the build and added you as a collaborator. Please let me know if there is anything else I can do to help. Thank you.

paulinon commented 1 year ago

Thanks for this, @og1337og. I'm getting a warning that a certain AuthManager is missing. Could you confrim if this script is needed?

og1337og commented 1 year ago

The AuthManager is not being used currently, I instead am using the Auth script instead but I added the old AuthManager script now for reference to the repository if that helps.

paulinon commented 1 year ago

Hi @og1337og,

After tinkering the repository you provided to make a working Unity project, I was able to replicate the issue you're facing. Upon inspecting your code, I noticed that Auth is being initialized in Awake() function while Firestore is being initialized in Start() function. Take note that Awake is called when an active GameObject that contains the script is initialized when a Scene loads while Start is called on the frame when a script is enabled.

With that, Firestore should work when you place its initializtion (followed by the conditional compilation) after the Auth initialization in the Awake() function. It should look something like this:

private void InitializeFirebase()
    {
        auth = FirebaseAuth.DefaultInstance;
        db = FirebaseFirestore.DefaultInstance;
#if UNITY_EDITOR
        db.Settings.PersistenceEnabled = false;
#endif
...
}

Let me know if the solution doesn't apply to your use case or if an issue arises.

og1337og commented 1 year ago

Hello, thank you so much that was 100% the problem calling one in the awake and one in the start. Works 100% now.