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

400 lines
14 KiB
C#
Raw Normal View History

2024-11-17 07:29:22 +08:00
using System;
using System.Net.Mail;
using UnityEngine;
using UnityEngine.Assertions;
using UnityEngine.UIElements;
/// <summary>
/// class to handle the account view ui
/// </summary>
public class AccountUI : MonoBehaviour
{
/// <summary>
/// state of the account view
/// </summary>
private enum State
{
UnassociatedState, // (start)
NotSignedIn, // initial
AfterContinue, // after
SignedIn // post
}
/// <summary>
/// current state of the account view
/// </summary>
[SerializeField] private State state = State.UnassociatedState;
/// <summary>
/// account view header text
/// </summary>
private Label _header;
/// <summary>
/// username text field
/// </summary>
private TextField _usernameField;
/// <summary>
/// email text field
/// </summary>
private TextField _emailField;
/// <summary>
/// password text field
/// </summary>
private TextField _passwordField;
/// <summary>
/// button to update the username
/// </summary>
private Button _usernameUpdateButton;
/// <summary>
/// button to update the email
/// </summary>
private Button _emailUpdateButton;
/// <summary>
/// button to update the password
/// </summary>
private Button _passwordUpdateButton;
/// <summary>
/// accompanying text for the account input fields, used when an error/notice is needed
/// </summary>
private Label _accompanyingText;
/// <summary>
/// either 'continue', 'log in', or 'sign out' button
/// (in order of 'initial', 'after', and 'post' states)
/// </summary>
private Button _primaryActionButton;
/// <summary>
/// either 'forgot password' or 'create an account'
/// (in order of 'initial' and 'after' states, is hidden in 'post' state)
/// </summary>
private Button _secondaryActionButton;
/// <summary>
/// function to subscribe button events to their respective functions
/// </summary>
public void OnEnable()
{
var ui = GetComponent<UIDocument>().rootVisualElement;
_header = ui.Q<Label>("AccountHeader");
_usernameField = ui.Q<TextField>("UsernameField");
_emailField = ui.Q<TextField>("EmailField");
_passwordField = ui.Q<TextField>("PasswordField");
_usernameUpdateButton = ui.Q<Button>("UsernameUpdateButton");
_usernameUpdateButton.clicked += OnUsernameUpdateButtonClick;
_emailUpdateButton = ui.Q<Button>("EmailUpdateButton");
_emailUpdateButton.clicked += OnEmailUpdateButtonClick;
_passwordUpdateButton = ui.Q<Button>("PasswordUpdateButton");
_passwordUpdateButton.clicked += OnPasswordUpdateButtonClick;
_accompanyingText = ui.Q<Label>("AccompanyingText");
_primaryActionButton = ui.Q<Button>("PrimaryActionButton");
_primaryActionButton.clicked += OnPrimaryActionButtonClick;
_secondaryActionButton = ui.Q<Button>("SecondaryActionButton");
_secondaryActionButton.clicked += OnSecondaryActionButtonClick;
TransitionStateTo(State.NotSignedIn);
}
public void Start()
{
if (state == State.UnassociatedState)
{
throw new Exception("unreachable state");
}
}
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;
}
Debug.Log($"transitioning to {newState}");
// 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:
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;
_usernameField.value = GameManager.Instance.Backend.GetUsername();
_emailField.value = GameManager.Instance.Backend.GetUser().Email;
_passwordField.value = "";
_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;
}
/// <summary>
/// validate 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 the email or password fields are invalid</exception>
private void ValidateFields(TextField emailField, TextField passwordField)
{
emailField.style.color = Color.black;
passwordField.style.color = Color.black;
// validate email
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 = Color.red;
throw new Exception("Invalid email.");
}
// validate password
if (passwordField.value.Length >= 10) return;
passwordField.style.color = Color.red;
throw new Exception("Invalid password.");
}
private void OnUsernameUpdateButtonClick()
{
// TODO
throw new NotImplementedException();
}
private void OnEmailUpdateButtonClick()
{
// TODO
throw new NotImplementedException();
}
private void OnPasswordUpdateButtonClick()
{
// TODO
throw new NotImplementedException();
}
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 = "Either the account does not exist, or there was an internal error. 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();
}
}
private void OnSecondaryActionButtonClick()
{
switch (state)
{
// forgot password button
case State.NotSignedIn:
throw new NotImplementedException();
// create an account button
case State.AfterContinue:
try
{
ValidateFields(_emailField, _passwordField);
}
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();
}
}
}