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

688 lines
No EOL
25 KiB
C#

using System;
using System.Net.Mail;
using UnityEngine;
using UnityEngine.Assertions;
using UnityEngine.UIElements;
using Button = UnityEngine.UIElements.Button;
/// <summary>
/// class to handle the account view ui
/// </summary>
public class AccountUI : MonoBehaviour
{
/// <summary>
/// current state of the account view
/// </summary>
[SerializeField] private State state = State.UnassociatedState;
/// <summary>
/// default text colour
/// </summary>
private readonly StyleColor _defaultInputFieldValueTextColour = new(new Color(0.0588f, 0.051f, 0.1059f));
/// <summary>
/// error text colour
/// </summary>
private readonly StyleColor _errorInputFieldValueTextColour = new Color(1f, 0.5059f, 0.502f);
/// <summary>
/// accompanying text for the account input fields, used when an error/notice is needed
/// </summary>
private Label _accompanyingText;
/// <summary>
/// email text field
/// </summary>
private TextField _emailField;
/// <summary>
/// button to update the email
/// </summary>
private Button _emailUpdateButton;
/// <summary>
/// account view header text
/// </summary>
private Label _header;
/// <summary>
/// password text field
/// </summary>
private TextField _passwordField;
/// <summary>
/// button to update the password
/// </summary>
private Button _passwordUpdateButton;
/// <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>
/// either 'delete local data' or 'delete account'
/// (in order of 'initial', and 'post' states, is hidden in 'after' state)
/// </summary>
private Button _tertiaryActionButton;
/// <summary>
/// username text field
/// </summary>
private TextField _usernameField;
/// <summary>
/// button to update the username
/// </summary>
private Button _usernameUpdateButton;
/// <summary>
/// function called when the object is enabled,
/// subscribes button events to their respective functions
/// </summary>
private 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;
_tertiaryActionButton = ui.Q<Button>("TertiaryActionButton");
_tertiaryActionButton.clicked += OnTertiaryActionButtonClick;
TransitionStateTo(State.NotSignedIn);
if (state == State.UnassociatedState) throw new Exception("unreachable state");
GameManager.Instance.Backend.RegisterOnSignInCallback(_ =>
{
Debug.Log("post-authentication callback, updating AccountView fields");
var username = GameManager.Instance.Backend.GetUsername();
_header.text = $"Signed in as {username}";
_passwordField.value = "";
_usernameField.value = username;
_emailField.value = GameManager.Instance.Backend.GetUser().Email;
});
GameManager.Instance.RegisterOnLocalPlayerDataChangeCallback(OnLocalPlayerDataChangeCallback);
}
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;
_tertiaryActionButton.style.display = DisplayStyle.Flex;
_primaryActionButton.text = "Continue \u2192";
_secondaryActionButton.text = "Forgot Password \u2192";
_tertiaryActionButton.text = "Delete Local Data \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;
_tertiaryActionButton.style.display = DisplayStyle.None;
_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;
_tertiaryActionButton.style.display = DisplayStyle.Flex;
_primaryActionButton.text = "Sign Out \u2192";
_tertiaryActionButton.text = "Delete Account \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.");
}
/// <summary>
/// validate the email field
/// </summary>
/// <param name="emailField">the email field to validate</param>
/// <exception cref="Exception">if the email field is invalid</exception>
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.");
}
}
/// <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;
if (passwordField.value.Length >= 10) return;
passwordField.style.color = _errorInputFieldValueTextColour;
throw new Exception("Password must be at least 10 characters long.");
}
/// <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
{
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);
}
/// <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
{
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);
}
/// <summary>
/// function to handle the username update button click
/// </summary>
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,
res =>
{
_accompanyingText.style.display = DisplayStyle.Flex;
_accompanyingText.text = res switch
{
Backend.TransactionResult.Ok => "Username updated!",
Backend.TransactionResult.Unauthenticated =>
"You are not signed in. Please sign in to update your username.",
_ => "An error occurred updating the username. Please try again."
};
});
}
/// <summary>
/// function to handle the email update button click
/// </summary>
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.TransactionResult.Ok =>
$"Verification email sent to {_emailField.value}! You may want to sign in again to see the changes.",
Backend.TransactionResult.Unauthenticated =>
"You are not signed in. Please sign in to update your email.",
_ => "An error occurred updating the email. Please try again."
};
});
}
/// <summary>
/// function to handle the password update button click
/// </summary>
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.TransactionResult.Ok => "Password updated!",
Backend.TransactionResult.Unauthenticated =>
"You are not signed in. Please sign in to update your password.",
_ => "An error occurred updating the password. Please try again."
};
});
}
/// <summary>
/// function to handle the primary action button click
/// </summary>
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:
Debug.LogError($"tertiary button clicked in illogical state {state} (unreachable?)");
break;
}
}
/// <summary>
/// function to handle the secondary action button click
/// </summary>
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.ResetUserPassword(_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.UsernameAlreadyTaken:
_accompanyingText.style.display = DisplayStyle.Flex;
_accompanyingText.text = "Username already taken. Please try another.";
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.UnassociatedState:
default:
Debug.LogError($"tertiary button clicked in illogical state {state} (unreachable?)");
break;
}
}
private void OnTertiaryActionButtonClick()
{
switch (state)
{
// delete local data
case State.NotSignedIn:
PlayerPrefs.DeleteAll();
_accompanyingText.style.display = DisplayStyle.Flex;
_accompanyingText.text = "Local data deleted.";
GameManager.Instance.Data.LoadFromTheWorld(GameManager.Instance.FireLocalPlayerDataChangeCallbacks);
break;
// delete user account
case State.SignedIn:
GameManager.Instance.Backend.DeleteUser(result =>
{
_accompanyingText.style.display = DisplayStyle.Flex;
_accompanyingText.text = result switch
{
Backend.TransactionResult.Ok => "Account deleted.",
Backend.TransactionResult.Unauthenticated => "You are not signed in.",
_ => "An error occurred deleting the account. Please try again."
};
});
break;
case State.AfterContinue:
case State.UnassociatedState:
default:
Debug.LogError($"tertiary button clicked in illogical state {state} (unreachable?)");
break;
}
}
/// <summary>
/// populate the fields with the given username and email,
/// used as a callback to when local player data is changed
/// </summary>
private void OnLocalPlayerDataChangeCallback(LocalPlayerData data)
{
Debug.Log(
$"updating AccountView ui with lkUsername={data.LastKnownUsername} and lkEmail={data.LastKnownEmail}");
_usernameField.value = data.LastKnownUsername;
_emailField.value = data.LastKnownEmail;
if (state == State.SignedIn) _header.text = $"Signed in as {data.LastKnownUsername}";
}
/// <summary>
/// state of the account view
/// </summary>
private enum State
{
UnassociatedState, // (start)
NotSignedIn, // initial
AfterContinue, // after
SignedIn // post
}
}