using System;
using System.Net.Mail;
using Firebase.Auth;
using UnityEngine;
using UnityEngine.Assertions;
using UnityEngine.UIElements;
using Button = UnityEngine.UIElements.Button;
///
/// class to handle the account view ui
///
public class AccountUI : MonoBehaviour
{
///
/// current state of the account view
///
[SerializeField] private State state = State.UnassociatedState;
///
/// default text colour
///
private readonly Color _defaultInputFieldValueTextColour = new(5.88f, 5.1f, 10.59f);
///
/// error text colour
///
private readonly Color _errorInputFieldValueTextColour = new(1f, 50.59f, 50.2f);
///
/// accompanying text for the account input fields, used when an error/notice is needed
///
private Label _accompanyingText;
///
/// email text field
///
private TextField _emailField;
///
/// button to update the email
///
private Button _emailUpdateButton;
///
/// account view header text
///
private Label _header;
///
/// password text field
///
private TextField _passwordField;
///
/// button to update the password
///
private Button _passwordUpdateButton;
///
/// either 'continue', 'log in', or 'sign out' button
/// (in order of 'initial', 'after', and 'post' states)
///
private Button _primaryActionButton;
///
/// either 'forgot password' or 'create an account'
/// (in order of 'initial' and 'after' states, is hidden in 'post' state)
///
private Button _secondaryActionButton;
///
/// username text field
///
private TextField _usernameField;
///
/// button to update the username
///
private Button _usernameUpdateButton;
public void Start()
{
if (state == State.UnassociatedState) throw new Exception("unreachable state");
// GameManager.Instance.Backend.RegisterOnSignInCallback(OnSignInCallback);
}
///
/// function to subscribe button events to their respective functions
///
public void OnEnable()
{
var ui = GetComponent().rootVisualElement;
_header = ui.Q("AccountHeader");
_usernameField = ui.Q("UsernameField");
_emailField = ui.Q("EmailField");
_passwordField = ui.Q("PasswordField");
_usernameUpdateButton = ui.Q("UsernameUpdateButton");
_usernameUpdateButton.clicked += OnUsernameUpdateButtonClick;
_emailUpdateButton = ui.Q("EmailUpdateButton");
_emailUpdateButton.clicked += OnEmailUpdateButtonClick;
_passwordUpdateButton = ui.Q("PasswordUpdateButton");
_passwordUpdateButton.clicked += OnPasswordUpdateButtonClick;
_accompanyingText = ui.Q("AccompanyingText");
_primaryActionButton = ui.Q("PrimaryActionButton");
_primaryActionButton.clicked += OnPrimaryActionButtonClick;
_secondaryActionButton = ui.Q("SecondaryActionButton");
_secondaryActionButton.clicked += OnSecondaryActionButtonClick;
TransitionStateTo(State.NotSignedIn);
}
private void Awake()
{
GameManager.Instance.Backend.RegisterOnSignInCallback(OnSignInCallback);
}
private void OnSignInCallback(FirebaseUser user)
{
Debug.Log("sign in account ui callback");
var username = GameManager.Instance.Backend.GetUsername();
_usernameField.value = username;
_emailField.value = GameManager.Instance.Backend.GetUser().Email;
_passwordField.value = "";
_header.text = $"Signed in as {username}";
}
///
/// populate the fields with the given username and email, used by GameManager after local player data is loaded
///
public void PopulateFields(string username, string email)
{
_usernameField.value = username;
_emailField.value = email;
}
private void TransitionStateTo(State newState, bool keepAccompanyingText = false)
{
// if we're transitioning to the same state, do nothing
if (state == newState) return;
// else, hide the accompanying text
if (!keepAccompanyingText) _accompanyingText.style.display = DisplayStyle.None;
// hide update buttons if not signed in
if (newState != State.SignedIn)
{
_usernameUpdateButton.style.display = DisplayStyle.None;
_passwordUpdateButton.style.display = DisplayStyle.None;
_emailUpdateButton.style.display = DisplayStyle.None;
}
else
{
_usernameUpdateButton.style.display = DisplayStyle.Flex;
_passwordUpdateButton.style.display = DisplayStyle.Flex;
_emailUpdateButton.style.display = DisplayStyle.Flex;
}
// set primary/secondary buttons
switch (newState)
{
case State.NotSignedIn:
_header.text = "You are not signed in.";
_usernameField.style.display = DisplayStyle.None;
_emailField.style.display = DisplayStyle.Flex;
_passwordField.style.display = DisplayStyle.Flex;
_primaryActionButton.style.display = DisplayStyle.Flex;
_secondaryActionButton.style.display = DisplayStyle.Flex;
_primaryActionButton.text = "Continue \u2192";
_secondaryActionButton.text = "Forgot Password \u2192";
break;
case State.AfterContinue:
_header.text = "You are not signed in.";
_usernameField.style.display = DisplayStyle.None;
_emailField.style.display = DisplayStyle.Flex;
_passwordField.style.display = DisplayStyle.Flex;
_primaryActionButton.style.display = DisplayStyle.Flex;
_secondaryActionButton.style.display = DisplayStyle.Flex;
_primaryActionButton.text = "Retry Log In \u2192";
_secondaryActionButton.text = "Create an Account \u2192";
break;
case State.SignedIn:
Debug.Log("transitioning to signed in state");
var username = GameManager.Instance.Backend.GetUsername();
_header.text = string.IsNullOrEmpty(username) ? "You are signed in." : $"Signed in as {username}";
_usernameField.style.display = DisplayStyle.Flex;
_emailField.style.display = DisplayStyle.Flex;
_passwordField.style.display = DisplayStyle.Flex;
_primaryActionButton.style.display = DisplayStyle.Flex;
_secondaryActionButton.style.display = DisplayStyle.None;
_primaryActionButton.text = "Sign Out \u2192";
break;
case State.UnassociatedState:
default:
throw new ArgumentOutOfRangeException();
}
state = newState;
}
private void ValidateUsername(TextField usernameField)
{
// just has to be min. 5 characters
usernameField.style.color = _defaultInputFieldValueTextColour;
if (usernameField.value.Length >= 5) return;
usernameField.style.color = _errorInputFieldValueTextColour;
throw new Exception("Username must be at least 5 characters long.");
}
///
/// validate the email field
///
/// the email field to validate
/// if the email field is invalid
private void ValidateEmailField(TextField emailField)
{
emailField.style.color = _defaultInputFieldValueTextColour;
try
{
var dot = emailField.value.LastIndexOf(".", StringComparison.Ordinal);
// check if a dot exists
Assert.IsTrue(dot != -1);
// if a dot exists, check if the first part has the @ symbol
// so emails like 'hello.bro@domain' are invalid (I mean, they are, but not for this)
Assert.IsTrue(emailField.value.IndexOf("@", StringComparison.Ordinal) < dot);
_ = new MailAddress(emailField.value);
}
catch
{
emailField.style.color = _errorInputFieldValueTextColour;
throw new Exception("Invalid email.");
}
}
///
/// validate the password field
///
/// the password field to validate
/// if the password field is invalid
private void ValidatePasswordField(TextField passwordField)
{
passwordField.style.color = _defaultInputFieldValueTextColour;
if (passwordField.value.Length >= 10) return;
passwordField.style.color = _errorInputFieldValueTextColour;
throw new Exception("Password must be at least 10 characters long.");
}
///
/// validate both the email and password fields
///
/// the email field to validate
/// the password field to validate
/// if either the email or password field is invalid
private void ValidateFields(TextField emailField, TextField passwordField)
{
var invalidEmail = false;
var invalidPassword = false;
try
{
ValidateEmailField(emailField);
}
catch (Exception)
{
invalidEmail = true;
}
try
{
ValidatePasswordField(passwordField);
}
catch (Exception)
{
invalidPassword = true;
}
if (!invalidEmail && !invalidPassword) return;
var errorMessage = (invalidEmail, invalidPassword) switch
{
(true, true) => "Invalid email and password is too short.",
(true, false) => "Invalid email.",
(false, true) => "Passwords must be at least 10 characters long.",
_ => null
};
throw new Exception(errorMessage);
}
///
/// validate both the email and password fields
///
/// the email field to validate
/// the password field to validate
/// the username field to validate
/// if either the email or password field is invalid
private void ValidateFields(TextField emailField, TextField passwordField, TextField usernameField)
{
var invalidEmail = false;
var invalidPassword = false;
var invalidUsername = false;
try
{
ValidateEmailField(emailField);
}
catch (Exception)
{
invalidEmail = true;
}
try
{
ValidatePasswordField(passwordField);
}
catch (Exception)
{
invalidPassword = true;
}
try
{
ValidateUsername(usernameField);
}
catch (Exception)
{
invalidUsername = true;
}
if (!invalidEmail && !invalidPassword && !invalidUsername) return;
var errorMessage = (invalidEmail, invalidPassword, invalidUsername) switch
{
(true, true, true) => "Invalid email, password is too short, and username is too short.",
(true, true, false) => "Invalid email, and password is too short.",
(true, false, true) => "Invalid email, and username is too short.",
(true, false, false) => "Invalid email.",
(false, true, true) => "Password is too short, and username is too short.",
(false, true, false) => "Password is too short.",
(false, false, true) => "Username is too short.",
_ => null
};
throw new Exception(errorMessage);
}
///
/// function to handle the username update button click
///
private void OnUsernameUpdateButtonClick()
{
try
{
ValidateUsername(_usernameField);
}
catch (Exception e)
{
_accompanyingText.style.display = DisplayStyle.Flex;
_accompanyingText.text = e.Message;
return;
}
GameManager.Instance.Backend.UpdateUserAccountDetail(Backend.UserAccountDetailTargetEnum.Username,
_usernameField.value,
dtr =>
{
_accompanyingText.style.display = DisplayStyle.Flex;
_accompanyingText.text = dtr switch
{
Backend.DatabaseTransactionResult.Ok => "Username updated!",
Backend.DatabaseTransactionResult.Unauthenticated =>
"You are not signed in. Please sign in to update your username.",
_ => "An error occurred updating the username. Please try again."
};
});
}
///
/// function to handle the email update button click
///
private void OnEmailUpdateButtonClick()
{
try
{
ValidateEmailField(_emailField);
}
catch (Exception e)
{
_accompanyingText.style.display = DisplayStyle.Flex;
_accompanyingText.text = e.Message;
return;
}
GameManager.Instance.Backend.UpdateUserAccountDetail(Backend.UserAccountDetailTargetEnum.Email,
_emailField.value,
callback =>
{
_accompanyingText.style.display = DisplayStyle.Flex;
_accompanyingText.text = callback switch
{
Backend.DatabaseTransactionResult.Ok =>
$"Verification email sent to {_emailField.value}! You may want to sign in again to see the changes.",
Backend.DatabaseTransactionResult.Unauthenticated =>
"You are not signed in. Please sign in to update your email.",
_ => "An error occurred updating the email. Please try again."
};
});
}
///
/// function to handle the password update button click
///
private void OnPasswordUpdateButtonClick()
{
try
{
ValidatePasswordField(_passwordField);
}
catch (Exception e)
{
_accompanyingText.style.display = DisplayStyle.Flex;
_accompanyingText.text = e.Message;
return;
}
GameManager.Instance.Backend.UpdateUserAccountDetail(Backend.UserAccountDetailTargetEnum.Password,
_passwordField.value,
callback =>
{
_accompanyingText.style.display = DisplayStyle.Flex;
_accompanyingText.text = callback switch
{
Backend.DatabaseTransactionResult.Ok => "Password updated!",
Backend.DatabaseTransactionResult.Unauthenticated =>
"You are not signed in. Please sign in to update your password.",
_ => "An error occurred updating the password. Please try again."
};
});
}
///
/// function to handle the primary action button click
///
private void OnPrimaryActionButtonClick()
{
switch (state)
{
// - continue button
// (attempt to figure out to log in or give the option to create an account)
// - log in button
case State.NotSignedIn:
case State.AfterContinue:
try
{
ValidateFields(_emailField, _passwordField);
}
catch (Exception e)
{
_accompanyingText.style.display = DisplayStyle.Flex;
_accompanyingText.text = e.Message;
return;
}
GameManager.Instance.Backend.AuthenticateUser(
_emailField.value,
_passwordField.value,
result =>
{
switch (result)
{
case Backend.AuthenticationResult.Ok:
case Backend.AuthenticationResult.AlreadyAuthenticated:
TransitionStateTo(State.SignedIn);
break;
case Backend.AuthenticationResult.NonExistentUser:
TransitionStateTo(State.AfterContinue);
break;
case Backend.AuthenticationResult.InvalidEmail:
_accompanyingText.style.display = DisplayStyle.Flex;
_accompanyingText.text = "Invalid email. Please try again.";
break;
case Backend.AuthenticationResult.InvalidCredentials:
_accompanyingText.style.display = DisplayStyle.Flex;
_accompanyingText.text = "Invalid credentials. Please try again.";
break;
case Backend.AuthenticationResult.AlreadyExistingUser:
case Backend.AuthenticationResult.GenericError:
default:
TransitionStateTo(State.AfterContinue);
_accompanyingText.style.display = DisplayStyle.Flex;
_accompanyingText.text =
"There was an error. Either the account does not exist, or the credentials are invalid. Try again.";
break;
}
});
break;
// sign out button
case State.SignedIn:
GameManager.Instance.Backend.SignOutUser();
TransitionStateTo(State.NotSignedIn);
break;
case State.UnassociatedState:
default:
throw new ArgumentOutOfRangeException();
}
}
///
/// function to handle the secondary action button click
///
private void OnSecondaryActionButtonClick()
{
switch (state)
{
// forgot password button
case State.NotSignedIn:
try
{
ValidateEmailField(_emailField);
}
catch (Exception e)
{
_accompanyingText.style.display = DisplayStyle.Flex;
_accompanyingText.text = e.Message;
return;
}
GameManager.Instance.Backend.ForgotPassword(_emailField.value, result =>
{
_accompanyingText.style.display = DisplayStyle.Flex;
_accompanyingText.text =
result
? "Password reset email sent. Check your inbox! :D"
: "An error occurred sending the email. Please try again.";
});
break;
// create an account button
case State.AfterContinue:
try
{
ValidateFields(_emailField, _passwordField, _usernameField);
}
catch (Exception e)
{
_accompanyingText.style.display = DisplayStyle.Flex;
_accompanyingText.text = e.Message;
return;
}
// if the username field is hidden, show it and then immediately return
if (_usernameField.style.display == DisplayStyle.None)
{
_usernameField.style.display = DisplayStyle.Flex;
_accompanyingText.style.display = DisplayStyle.Flex;
_accompanyingText.text = "Set a username for your account.";
return;
}
GameManager.Instance.Backend.AuthenticateUser(
_emailField.value,
_passwordField.value,
result =>
{
switch (result)
{
case Backend.AuthenticationResult.Ok:
case Backend.AuthenticationResult.AlreadyAuthenticated:
TransitionStateTo(State.SignedIn);
break;
case Backend.AuthenticationResult.InvalidEmail:
_accompanyingText.style.display = DisplayStyle.Flex;
_accompanyingText.text = "Invalid email. Please try again.";
break;
case Backend.AuthenticationResult.InvalidCredentials:
_accompanyingText.style.display = DisplayStyle.Flex;
_accompanyingText.text = "Invalid credentials. Please try again.";
break;
case Backend.AuthenticationResult.NonExistentUser:
case Backend.AuthenticationResult.AlreadyExistingUser:
case Backend.AuthenticationResult.GenericError:
default:
_accompanyingText.style.display = DisplayStyle.Flex;
_accompanyingText.text = "An error occurred. Please try again.";
Debug.LogError(result);
break;
}
},
true,
_usernameField.value);
break;
case State.SignedIn:
case State.UnassociatedState:
default:
throw new ArgumentOutOfRangeException();
}
}
///
/// state of the account view
///
private enum State
{
UnassociatedState, // (start)
NotSignedIn, // initial
AfterContinue, // after
SignedIn // post
}
}