This repository has been archived on 2024-11-20. You can view files and clone it, but cannot push or open issues or pull requests.
colourmeok/ColourMeOKGame/Assets/Scripts/AccountUI.cs

632 lines
22 KiB
C#
Raw Normal View History

2024-11-17 07:29:22 +08:00
using System;
using System.Net.Mail;
2024-11-17 19:10:01 +08:00
using Firebase.Auth;
2024-11-17 07:29:22 +08:00
using UnityEngine;
using UnityEngine.Assertions;
using UnityEngine.UIElements;
2024-11-17 19:10:01 +08:00
using Button = UnityEngine.UIElements.Button;
2024-11-17 07:29:22 +08:00
/// <summary>
/// class to handle the account view ui
/// </summary>
public class AccountUI : MonoBehaviour
{
/// <summary>
2024-11-17 19:10:01 +08:00
/// current state of the account view
2024-11-17 07:29:22 +08:00
/// </summary>
2024-11-17 19:10:01 +08:00
[SerializeField] private State state = State.UnassociatedState;
2024-11-17 07:29:22 +08:00
/// <summary>
2024-11-17 19:10:01 +08:00
/// default text colour
2024-11-17 07:29:22 +08:00
/// </summary>
2024-11-17 19:10:01 +08:00
private readonly Color _defaultInputFieldValueTextColour = new(5.88f, 5.1f, 10.59f);
2024-11-17 07:29:22 +08:00
/// <summary>
2024-11-17 19:10:01 +08:00
/// error text colour
2024-11-17 07:29:22 +08:00
/// </summary>
2024-11-17 19:10:01 +08:00
private readonly Color _errorInputFieldValueTextColour = new(1f, 50.59f, 50.2f);
2024-11-17 07:29:22 +08:00
/// <summary>
2024-11-17 19:10:01 +08:00
/// accompanying text for the account input fields, used when an error/notice is needed
2024-11-17 07:29:22 +08:00
/// </summary>
2024-11-17 19:10:01 +08:00
private Label _accompanyingText;
2024-11-17 07:29:22 +08:00
/// <summary>
/// email text field
/// </summary>
private TextField _emailField;
2024-11-17 19:10:01 +08:00
2024-11-17 07:29:22 +08:00
/// <summary>
2024-11-17 19:10:01 +08:00
/// button to update the email
2024-11-17 07:29:22 +08:00
/// </summary>
2024-11-17 19:10:01 +08:00
private Button _emailUpdateButton;
2024-11-17 07:29:22 +08:00
/// <summary>
2024-11-17 19:10:01 +08:00
/// account view header text
2024-11-17 07:29:22 +08:00
/// </summary>
2024-11-17 19:10:01 +08:00
private Label _header;
2024-11-17 07:29:22 +08:00
/// <summary>
2024-11-17 19:10:01 +08:00
/// password text field
2024-11-17 07:29:22 +08:00
/// </summary>
2024-11-17 19:10:01 +08:00
private TextField _passwordField;
2024-11-17 07:29:22 +08:00
/// <summary>
/// button to update the password
/// </summary>
private Button _passwordUpdateButton;
2024-11-17 19:10:01 +08:00
2024-11-17 07:29:22 +08:00
/// <summary>
/// either 'continue', 'log in', or 'sign out' button
/// (in order of 'initial', 'after', and 'post' states)
/// </summary>
private Button _primaryActionButton;
2024-11-17 19:10:01 +08:00
2024-11-17 07:29:22 +08:00
/// <summary>
/// either 'forgot password' or 'create an account'
/// (in order of 'initial' and 'after' states, is hidden in 'post' state)
/// </summary>
private Button _secondaryActionButton;
2024-11-17 19:10:01 +08:00
/// <summary>
/// username text field
/// </summary>
private TextField _usernameField;
/// <summary>
/// button to update the username
/// </summary>
private Button _usernameUpdateButton;
2024-11-17 22:31:03 +08:00
private void Awake()
{
GameManager.Instance.Backend.RegisterOnSignInCallback(OnSignInCallback);
}
public void Start()
2024-11-17 19:10:01 +08:00
{
if (state == State.UnassociatedState) throw new Exception("unreachable state");
// GameManager.Instance.Backend.RegisterOnSignInCallback(OnSignInCallback);
2024-11-17 19:10:01 +08:00
}
2024-11-17 07:29:22 +08:00
/// <summary>
/// function to subscribe button events to their respective functions
/// </summary>
public void OnEnable()
{
var ui = GetComponent<UIDocument>().rootVisualElement;
_header = ui.Q<Label>("AccountHeader");
2024-11-17 19:10:01 +08:00
2024-11-17 07:29:22 +08:00
_usernameField = ui.Q<TextField>("UsernameField");
_emailField = ui.Q<TextField>("EmailField");
_passwordField = ui.Q<TextField>("PasswordField");
_usernameUpdateButton = ui.Q<Button>("UsernameUpdateButton");
_usernameUpdateButton.clicked += OnUsernameUpdateButtonClick;
2024-11-17 19:10:01 +08:00
2024-11-17 07:29:22 +08:00
_emailUpdateButton = ui.Q<Button>("EmailUpdateButton");
_emailUpdateButton.clicked += OnEmailUpdateButtonClick;
2024-11-17 19:10:01 +08:00
2024-11-17 07:29:22 +08:00
_passwordUpdateButton = ui.Q<Button>("PasswordUpdateButton");
_passwordUpdateButton.clicked += OnPasswordUpdateButtonClick;
2024-11-17 19:10:01 +08:00
2024-11-17 07:29:22 +08:00
_accompanyingText = ui.Q<Label>("AccompanyingText");
2024-11-17 19:10:01 +08:00
2024-11-17 07:29:22 +08:00
_primaryActionButton = ui.Q<Button>("PrimaryActionButton");
_primaryActionButton.clicked += OnPrimaryActionButtonClick;
2024-11-17 19:10:01 +08:00
2024-11-17 07:29:22 +08:00
_secondaryActionButton = ui.Q<Button>("SecondaryActionButton");
_secondaryActionButton.clicked += OnSecondaryActionButtonClick;
TransitionStateTo(State.NotSignedIn);
2024-11-17 22:05:04 +08:00
}
2024-11-17 19:10:01 +08:00
private void OnSignInCallback(FirebaseUser user)
2024-11-17 07:29:22 +08:00
{
Debug.Log("sign in account ui callback");
2024-11-17 19:10:01 +08:00
var username = GameManager.Instance.Backend.GetUsername();
_usernameField.value = username;
_emailField.value = GameManager.Instance.Backend.GetUser().Email;
_passwordField.value = "";
_header.text = $"Signed in as {username}";
}
2024-11-17 07:29:22 +08:00
private void TransitionStateTo(State newState, bool keepAccompanyingText = false)
{
// if we're transitioning to the same state, do nothing
if (state == newState) return;
2024-11-17 19:10:01 +08:00
2024-11-17 07:29:22 +08:00
// 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.";
2024-11-17 19:10:01 +08:00
2024-11-17 07:29:22 +08:00
_usernameField.style.display = DisplayStyle.None;
_emailField.style.display = DisplayStyle.Flex;
_passwordField.style.display = DisplayStyle.Flex;
2024-11-17 19:10:01 +08:00
2024-11-17 07:29:22 +08:00
_primaryActionButton.style.display = DisplayStyle.Flex;
_secondaryActionButton.style.display = DisplayStyle.Flex;
_primaryActionButton.text = "Continue \u2192";
_secondaryActionButton.text = "Forgot Password \u2192";
break;
2024-11-17 19:10:01 +08:00
2024-11-17 07:29:22 +08:00
case State.AfterContinue:
_header.text = "You are not signed in.";
2024-11-17 19:10:01 +08:00
2024-11-17 07:29:22 +08:00
_usernameField.style.display = DisplayStyle.None;
_emailField.style.display = DisplayStyle.Flex;
_passwordField.style.display = DisplayStyle.Flex;
2024-11-17 19:10:01 +08:00
2024-11-17 07:29:22 +08:00
_primaryActionButton.style.display = DisplayStyle.Flex;
_secondaryActionButton.style.display = DisplayStyle.Flex;
2024-11-17 19:10:01 +08:00
2024-11-17 07:29:22 +08:00
_primaryActionButton.text = "Retry Log In \u2192";
_secondaryActionButton.text = "Create an Account \u2192";
break;
2024-11-17 19:10:01 +08:00
2024-11-17 07:29:22 +08:00
case State.SignedIn:
2024-11-17 19:10:01 +08:00
Debug.Log("transitioning to signed in state");
2024-11-17 07:29:22 +08:00
var username = GameManager.Instance.Backend.GetUsername();
_header.text = string.IsNullOrEmpty(username) ? "You are signed in." : $"Signed in as {username}";
2024-11-17 19:10:01 +08:00
2024-11-17 07:29:22 +08:00
_usernameField.style.display = DisplayStyle.Flex;
_emailField.style.display = DisplayStyle.Flex;
_passwordField.style.display = DisplayStyle.Flex;
2024-11-17 19:10:01 +08:00
2024-11-17 07:29:22 +08:00
_primaryActionButton.style.display = DisplayStyle.Flex;
_secondaryActionButton.style.display = DisplayStyle.None;
_primaryActionButton.text = "Sign Out \u2192";
break;
2024-11-17 19:10:01 +08:00
2024-11-17 07:29:22 +08:00
case State.UnassociatedState:
default:
throw new ArgumentOutOfRangeException();
}
2024-11-17 19:10:01 +08:00
2024-11-17 07:29:22 +08:00
state = newState;
}
2024-11-17 19:10:01 +08:00
2024-11-17 22:05:04 +08:00
private void ValidateUsername(TextField usernameField)
2024-11-17 19:10:01 +08:00
{
// just has to be min. 5 characters
2024-11-17 22:05:04 +08:00
usernameField.style.color = _defaultInputFieldValueTextColour;
if (usernameField.value.Length >= 5) return;
usernameField.style.color = _errorInputFieldValueTextColour;
2024-11-17 19:10:01 +08:00
throw new Exception("Username must be at least 5 characters long.");
}
2024-11-17 07:29:22 +08:00
/// <summary>
2024-11-17 19:10:01 +08:00
/// validate the email field
2024-11-17 07:29:22 +08:00
/// </summary>
/// <param name="emailField">the email field to validate</param>
2024-11-17 19:10:01 +08:00
/// <exception cref="Exception">if the email field is invalid</exception>
private void ValidateEmailField(TextField emailField)
2024-11-17 07:29:22 +08:00
{
2024-11-17 19:10:01 +08:00
emailField.style.color = _defaultInputFieldValueTextColour;
2024-11-17 07:29:22 +08:00
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
{
2024-11-17 19:10:01 +08:00
emailField.style.color = _errorInputFieldValueTextColour;
2024-11-17 07:29:22 +08:00
throw new Exception("Invalid email.");
}
2024-11-17 19:10:01 +08:00
}
2024-11-17 07:29:22 +08:00
2024-11-17 19:10:01 +08:00
/// <summary>
/// validate the password field
/// </summary>
/// <param name="passwordField">the password field to validate</param>
/// <exception cref="Exception">if the password field is invalid</exception>
private void ValidatePasswordField(TextField passwordField)
{
passwordField.style.color = _defaultInputFieldValueTextColour;
2024-11-17 07:29:22 +08:00
if (passwordField.value.Length >= 10) return;
2024-11-17 19:10:01 +08:00
passwordField.style.color = _errorInputFieldValueTextColour;
throw new Exception("Password must be at least 10 characters long.");
2024-11-17 07:29:22 +08:00
}
2024-11-17 19:10:01 +08:00
/// <summary>
/// validate both the email and password fields
/// </summary>
/// <param name="emailField">the email field to validate</param>
/// <param name="passwordField">the password field to validate</param>
/// <exception cref="Exception">if either the email or password field is invalid</exception>
private void ValidateFields(TextField emailField, TextField passwordField)
{
var invalidEmail = false;
var invalidPassword = false;
try
{
2024-11-17 22:05:04 +08:00
ValidateEmailField(emailField);
2024-11-17 19:10:01 +08:00
}
2024-11-17 22:05:04 +08:00
catch (Exception)
2024-11-17 19:10:01 +08:00
{
invalidEmail = true;
}
try
{
2024-11-17 22:05:04 +08:00
ValidatePasswordField(passwordField);
2024-11-17 19:10:01 +08:00
}
2024-11-17 22:05:04 +08:00
catch (Exception)
2024-11-17 19:10:01 +08:00
{
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);
}
/// <summary>
/// validate both the email and password fields
/// </summary>
/// <param name="emailField">the email field to validate</param>
/// <param name="passwordField">the password field to validate</param>
/// <param name="usernameField">the username field to validate</param>
/// <exception cref="Exception">if either the email or password field is invalid</exception>
private void ValidateFields(TextField emailField, TextField passwordField, TextField usernameField)
{
var invalidEmail = false;
var invalidPassword = false;
var invalidUsername = false;
try
{
2024-11-17 22:05:04 +08:00
ValidateEmailField(emailField);
2024-11-17 19:10:01 +08:00
}
2024-11-17 22:05:04 +08:00
catch (Exception)
2024-11-17 19:10:01 +08:00
{
invalidEmail = true;
}
try
{
2024-11-17 22:05:04 +08:00
ValidatePasswordField(passwordField);
2024-11-17 19:10:01 +08:00
}
2024-11-17 22:05:04 +08:00
catch (Exception)
2024-11-17 19:10:01 +08:00
{
invalidPassword = true;
}
try
{
2024-11-17 22:05:04 +08:00
ValidateUsername(usernameField);
2024-11-17 19:10:01 +08:00
}
2024-11-17 22:05:04 +08:00
catch (Exception)
2024-11-17 19:10:01 +08:00
{
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);
}
/// <summary>
/// function to handle the username update button click
/// </summary>
2024-11-17 07:29:22 +08:00
private void OnUsernameUpdateButtonClick()
{
2024-11-17 19:10:01 +08:00
try
{
2024-11-17 22:05:04 +08:00
ValidateUsername(_usernameField);
2024-11-17 19:10:01 +08:00
}
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."
};
});
2024-11-17 07:29:22 +08:00
}
2024-11-17 19:10:01 +08:00
/// <summary>
/// function to handle the email update button click
/// </summary>
2024-11-17 07:29:22 +08:00
private void OnEmailUpdateButtonClick()
{
2024-11-17 19:10:01 +08:00
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."
};
});
2024-11-17 07:29:22 +08:00
}
2024-11-17 19:10:01 +08:00
/// <summary>
/// function to handle the password update button click
/// </summary>
2024-11-17 07:29:22 +08:00
private void OnPasswordUpdateButtonClick()
{
2024-11-17 19:10:01 +08:00
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."
};
});
2024-11-17 07:29:22 +08:00
}
2024-11-17 19:10:01 +08:00
/// <summary>
/// function to handle the primary action button click
/// </summary>
2024-11-17 07:29:22 +08:00
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;
}
2024-11-17 19:10:01 +08:00
2024-11-17 07:29:22 +08:00
GameManager.Instance.Backend.AuthenticateUser(
_emailField.value,
_passwordField.value,
2024-11-17 19:10:01 +08:00
result =>
2024-11-17 07:29:22 +08:00
{
switch (result)
{
case Backend.AuthenticationResult.Ok:
case Backend.AuthenticationResult.AlreadyAuthenticated:
TransitionStateTo(State.SignedIn);
break;
case Backend.AuthenticationResult.NonExistentUser:
TransitionStateTo(State.AfterContinue);
break;
2024-11-17 19:10:01 +08:00
2024-11-17 07:29:22 +08:00
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;
2024-11-17 19:10:01 +08:00
2024-11-17 07:29:22 +08:00
case Backend.AuthenticationResult.AlreadyExistingUser:
case Backend.AuthenticationResult.GenericError:
default:
TransitionStateTo(State.AfterContinue);
_accompanyingText.style.display = DisplayStyle.Flex;
2024-11-17 19:10:01 +08:00
_accompanyingText.text =
"There was an error. Either the account does not exist, or the credentials are invalid. Try again.";
2024-11-17 07:29:22 +08:00
break;
}
});
break;
2024-11-17 19:10:01 +08:00
2024-11-17 07:29:22 +08:00
// sign out button
case State.SignedIn:
GameManager.Instance.Backend.SignOutUser();
TransitionStateTo(State.NotSignedIn);
break;
2024-11-17 19:10:01 +08:00
2024-11-17 07:29:22 +08:00
case State.UnassociatedState:
default:
throw new ArgumentOutOfRangeException();
}
}
2024-11-17 19:10:01 +08:00
/// <summary>
/// function to handle the secondary action button click
/// </summary>
2024-11-17 07:29:22 +08:00
private void OnSecondaryActionButtonClick()
{
switch (state)
{
// forgot password button
case State.NotSignedIn:
2024-11-17 19:10:01 +08:00
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;
2024-11-17 07:29:22 +08:00
// create an account button
case State.AfterContinue:
try
{
2024-11-17 19:10:01 +08:00
ValidateFields(_emailField, _passwordField, _usernameField);
2024-11-17 07:29:22 +08:00
}
catch (Exception e)
{
_accompanyingText.style.display = DisplayStyle.Flex;
_accompanyingText.text = e.Message;
return;
}
2024-11-17 19:10:01 +08:00
2024-11-17 07:29:22 +08:00
// 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;
}
2024-11-17 19:10:01 +08:00
2024-11-17 07:29:22 +08:00
GameManager.Instance.Backend.AuthenticateUser(
_emailField.value,
_passwordField.value,
2024-11-17 19:10:01 +08:00
result =>
2024-11-17 07:29:22 +08:00
{
switch (result)
{
case Backend.AuthenticationResult.Ok:
case Backend.AuthenticationResult.AlreadyAuthenticated:
TransitionStateTo(State.SignedIn);
break;
2024-11-17 19:10:01 +08:00
2024-11-17 07:29:22 +08:00
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;
2024-11-17 19:10:01 +08:00
2024-11-17 07:29:22 +08:00
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;
2024-11-17 19:10:01 +08:00
2024-11-17 07:29:22 +08:00
case State.SignedIn:
case State.UnassociatedState:
default:
throw new ArgumentOutOfRangeException();
}
}
2024-11-17 19:10:01 +08:00
/// <summary>
/// state of the account view
/// </summary>
private enum State
{
UnassociatedState, // (start)
NotSignedIn, // initial
AfterContinue, // after
SignedIn // post
}
}