using System; using System.Collections.Generic; using System.Threading.Tasks; using Firebase; using Firebase.Auth; using Firebase.Database; using Firebase.Extensions; using UnityEngine; /// /// the general managing class for handling communication with the firebase backend /// (to be initialised by GameManager) /// public class Backend { /// /// enum for the result of the authentication process /// public enum AuthenticationResult { Ok, AlreadyAuthenticated, NonExistentUser, AlreadyExistingUser, InvalidEmail, InvalidCredentials, GenericError } /// /// generic enum for the result of a database transaction /// public enum DatabaseTransactionResult { Ok, Unauthenticated, Error } /// /// enum for the connection status of the firebase backend /// public enum FirebaseConnectionStatus { NotConnected, Connected, UpdateRequired, // "a required system component is out of date" Updating, // "a required system component is updating, retrying in a bit..." ExternalError, // "a system component is disabled, invalid, missing, or permissions are insufficient" InternalError // "an unknown error occurred" } /// /// callback functions to be invoked when the user signs in /// private readonly List> _onSignInCallback = new(); /// /// callback functions to be invoked when the user signs out /// private readonly List> _onSignOutCallback = new(); /// /// the firebase authentication object /// private FirebaseAuth _auth; /// /// the firebase database reference /// private DatabaseReference _db; /// /// the current user object, if authenticated /// private FirebaseUser _user; /// /// the current user's username, if authenticated /// private string _username; /// /// whether the backend is connected to the firebase backend /// public FirebaseConnectionStatus Status = FirebaseConnectionStatus.NotConnected; /// /// variable initialisation function /// public void Initialise() { FirebaseApp.CheckAndFixDependenciesAsync().ContinueWith(task => { // cher is this robust enough switch (task.Result) { case DependencyStatus.Available: _auth = FirebaseAuth.GetAuth(FirebaseApp.DefaultInstance); _auth.StateChanged += AuthStateChanged; _db = FirebaseDatabase.DefaultInstance.RootReference; Status = FirebaseConnectionStatus.Connected; break; case DependencyStatus.UnavailableDisabled: case DependencyStatus.UnavailableInvalid: case DependencyStatus.UnavilableMissing: case DependencyStatus.UnavailablePermission: Status = FirebaseConnectionStatus.ExternalError; break; case DependencyStatus.UnavailableUpdating: Status = FirebaseConnectionStatus.Updating; RetryInitialiseAfterDelay(); break; case DependencyStatus.UnavailableUpdaterequired: Status = FirebaseConnectionStatus.UpdateRequired; break; case DependencyStatus.UnavailableOther: default: Status = FirebaseConnectionStatus.InternalError; Debug.LogError("firebase ??? blew up or something," + task.Result); break; } Debug.Log("firebase status is" + Status); }); } /// /// async function to retry initialisation after a delay /// private async void RetryInitialiseAfterDelay() { await Task.Delay(TimeSpan.FromSeconds(10)); Initialise(); } /// /// cleanup function /// public void Cleanup() { SignOutUser(); _auth.StateChanged -= AuthStateChanged; _auth = null; } /// /// function to handle the authentication state change event /// /// the object that triggered the event /// the event arguments private void AuthStateChanged(object sender, EventArgs eventArgs) { // if the user hasn't changed, do nothing if (_auth.CurrentUser == _user) return; // if the user has changed, check if they've signed in or out var signedIn = _user != _auth.CurrentUser && _auth.CurrentUser != null; // if we're not signed in, but we still hold _user locally, we've signed out if (!signedIn && _user != null) { Debug.Log($"signed out successfully as {_user.UserId}"); foreach (var callback in _onSignOutCallback) callback(_user); } // they have signed in, update _user _user = _auth.CurrentUser; if (signedIn) { Debug.Log($"signed in successfully as {_user.UserId}"); RetrieveUsername(); foreach (var callback in _onSignInCallback) callback(_user); } } /// /// function to register a callback for when the user signs in /// /// callback function that takes in a FirebaseUser argument public void RegisterOnSignInCallback(Action callback) { _onSignInCallback.Add(callback); } /// /// function to register a callback for when the user signs out /// /// callback function that takes in a FirebaseUser argument public void RegisterOnSignOutCallback(Action callback) { _onSignOutCallback.Add(callback); } /// /// abstraction function to authenticate the user /// /// email string /// user raw password string /// callback function that takes in an AuthenticationResult argument /// whether to treat authentication as registration /// username string if registering public void AuthenticateUser( string email, string password, Action callback, bool registerUser = false, string registeringUsername = "") { if (GameManager.Instance.Backend.GetUser() != null) { callback(AuthenticationResult.AlreadyAuthenticated); return; } if (registerUser) { // register user _auth.CreateUserWithEmailAndPasswordAsync(email, password) .ContinueWithOnMainThread(createTask => { if (createTask.IsCompletedSuccessfully) { // store username _db.Child("users") .Child(_user.UserId) .Child("username") .SetValueAsync(registeringUsername) .ContinueWithOnMainThread(setUsernameTask => { if (setUsernameTask.IsCompletedSuccessfully) { _username = registeringUsername; callback(AuthenticationResult.Ok); } else { Debug.LogError(setUsernameTask.Exception); callback(AuthenticationResult.GenericError); } }); return; } if (createTask.Exception?.InnerException == null) { callback(AuthenticationResult.GenericError); return; } var error = (AuthError)((FirebaseException)createTask.Exception.InnerException).ErrorCode; // ReSharper disable once SwitchStatementHandlesSomeKnownEnumValuesWithDefault switch (error) { case AuthError.UserNotFound: callback(AuthenticationResult.NonExistentUser); return; case AuthError.InvalidEmail: callback(AuthenticationResult.InvalidEmail); return; case AuthError.WeakPassword: case AuthError.InvalidCredential: callback(AuthenticationResult.InvalidCredentials); return; case AuthError.AccountExistsWithDifferentCredentials: case AuthError.EmailAlreadyInUse: callback(AuthenticationResult.AlreadyExistingUser); return; case AuthError.Failure: default: Debug.LogError(error); Debug.LogError(createTask.Exception); callback(AuthenticationResult.GenericError); break; } }); return; } _auth.SignInWithEmailAndPasswordAsync(email, password) .ContinueWithOnMainThread(signInTask => { if (signInTask.IsCompletedSuccessfully) { callback(AuthenticationResult.Ok); return; } if (signInTask.Exception?.InnerException == null) { callback(AuthenticationResult.GenericError); return; } var error = (AuthError)((FirebaseException)signInTask.Exception.InnerException).ErrorCode; // ReSharper disable once SwitchStatementHandlesSomeKnownEnumValuesWithDefault switch (error) { case AuthError.UserNotFound: callback(AuthenticationResult.NonExistentUser); return; case AuthError.InvalidEmail: callback(AuthenticationResult.InvalidEmail); return; case AuthError.WeakPassword: case AuthError.InvalidCredential: callback(AuthenticationResult.InvalidCredentials); return; case AuthError.AccountExistsWithDifferentCredentials: case AuthError.EmailAlreadyInUse: callback(AuthenticationResult.AlreadyExistingUser); return; case AuthError.Failure: default: Debug.LogError(error); Debug.LogError(signInTask.Exception); callback(AuthenticationResult.GenericError); break; } }); } /// /// function to retrieve the user's username from the database /// private void RetrieveUsername() { if (!Status.Equals(FirebaseConnectionStatus.Connected)) return; _db.Child("users").Child(_user.UserId).Child("username").GetValueAsync().ContinueWith(task => { if (task.IsCompletedSuccessfully) { _username = task.Result.Value.ToString(); Debug.Log($"our username is {_username}"); } else { _username = "???"; Debug.LogError("failed to get username"); } }); } /// /// abstraction function to retrieve the user /// /// the firebase user object public FirebaseUser GetUser() { return _user; } public string GetUsername() { return _username; } /// /// abstraction function to sign out the user /// public void SignOutUser() { _auth.SignOut(); } /// /// abstraction function to submit a play to the database /// /// play data /// callback function that takes in one DatabaseTransactionResult argument public void SubmitPlay( PlayData playData, Action callback) { throw new NotImplementedException(); } /// /// abstraction function to get and calculate the user's rating from the database /// calculation is done locally, call UpdateUserRating to update the user's rating in the database /// /// callback function that takes in a DatabaseTransactionResult and float (user rating) argument public void CalculateUserRating( Action callback) { throw new NotImplementedException(); } /// /// abstraction function to update the user's rating in the database /// /// new user rating value as a float /// callback function that takes in one DatabaseTransactionResult argument public void UpdateUserRating( float newRating, Action callback) { throw new NotImplementedException(); } /// /// abstraction function to get the leaderboard from the database /// /// /// callback function that takes in a DatabaseTransactionResult and LeaderboardEntry[] argument /// public void GetLeaderboard( Action callback) { throw new NotImplementedException(); } // private void Playground() // { // // update username // _db.Child("users").Child(_user.UserId).Child("username").SetValueAsync("newusername").ContinueWith(task => { }); // // // update email // _user.SendEmailVerificationBeforeUpdatingEmailAsync("name@example.com").ContinueWith(task => { }); // // // update password // _user.UpdatePasswordAsync("password").ContinueWith(task => { }); // // // reset password // _auth.SendPasswordResetEmailAsync("name@example.com").ContinueWith(task => { }); // } /// /// struct for a leaderboard entry /// public struct LeaderboardEntry { public string Username; public float Rating; public int PlayCount; public LeaderboardEntry(string username, float rating, int playCount) { Username = username; Rating = rating; PlayCount = playCount; } } /// /// struct for play data /// public struct PlayData { public int RoundsPlayed; public float AverageOverallAccuracy; public float AverageLightnessAccuracy; public float AverageChromaAccuracy; public float AverageHueAccuracy; } }