using System;
using System.Collections;
using System.IO;
using System.Text;
#if UNITY_EDITOR
using UnityEditor;
#endif
using TMPro;
using UnityEngine;
using UnityEngine.Networking;
using UnityEngine.SceneManagement;
using UnityEngine.UI;
public class LoginManager : MonoBehaviour
{
public const string AuthBaseUrl = "https://kickkingsapi.playpoolstudios.com";
const string PlayerPrefsAuthKey = "WalkInvest.AuthToken";
const int MIN_USERNAME_LENGTH = 3;
const int MAX_USERNAME_LENGTH = 16;
const int MIN_PASSWORD_LENGTH = 8;
const int MAX_PASSWORD_LENGTH = 20;
[Header("Login")]
[SerializeField] private TMP_InputField usernameInputLogin;
[SerializeField] private TMP_InputField passwordInputLogin;
[SerializeField] private Button btnLogin;
[Header("Register")]
[SerializeField] private TMP_InputField usernameInputRegister;
[SerializeField] private TMP_InputField passwordInputRegister;
[SerializeField] private Button btnRegister;
[Header("UI")]
[SerializeField] private TMP_Text error_txt;
bool _authInProgress;
public static string AuthToken { get; private set; }
/// Assigned side from the matchmaker your_team field. null until a room is found or after clear.
public static Team? MatchYourTeam { get; private set; }
/// Parses matchmaker strings such as "red" / "blue" (case-insensitive).
public static void SetMatchYourTeam(string matchmakerYourTeam)
{
MatchYourTeam = ParseMatchmakerTeam(matchmakerYourTeam);
}
static Team? ParseMatchmakerTeam(string s)
{
if (string.IsNullOrWhiteSpace(s))
return null;
switch (s.Trim().ToLowerInvariant())
{
case "red": return Team.Red;
case "blue": return Team.Blue;
default:
return null;
}
}
public static void ClearMatchYourTeam()
{
MatchYourTeam = null;
}
/// Latest user payload from the server. Reading this schedules a refresh (one in flight at a time).
public static UserData CurrentUser
{
get
{
if (instance != null)
instance.ScheduleUserDataRefreshOnRead();
return _currentUser;
}
}
public static Action OnUserDataUpdated;
static UserData _currentUser;
bool _userDataReadRefreshPending;
public static LoginManager instance;
void Awake()
{
error_txt.text = "";
AuthToken = PlayerPrefs.GetString(PlayerPrefsAuthKey, "");
if(instance !=null){
Destroy(gameObject);
return;
}
instance = this;
DontDestroyOnLoad(gameObject);
if (string.IsNullOrEmpty(AuthToken))
ClearLocalUserProfile();
}
void Start()
{
btnLogin.onClick.AddListener(OnLogin);
btnRegister.onClick.AddListener(OnRegister);
#if UNITY_EDITOR
// Dev: log in with clone-aware credentials instead of resuming a saved session.
AuthToken = "";
PlayerPrefs.DeleteKey(PlayerPrefsAuthKey);
PlayerPrefs.Save();
bool cloneWorkspace = EditorWorkspaceLooksLikeClone();
string devUser = cloneWorkspace ? "warlock2" : "warlock";
const string devPass = "12345678";
if (usernameInputLogin != null) usernameInputLogin.text = devUser;
if (passwordInputLogin != null) passwordInputLogin.text = devPass;
string loginJson = "{\"username\":\"" + EscapeJsonString(devUser) + "\",\"password\":\"" + EscapeJsonString(devPass) + "\"}";
// StartCoroutine(AuthPostCoroutine(AuthBaseUrl + "/auth/login", loginJson, "Signed in."));
#else
if (!string.IsNullOrEmpty(AuthToken))
{
SetBusy(true);
StartCoroutine(ResumeSessionCoroutine());
}
#endif
}
#if UNITY_EDITOR
public static bool EditorWorkspaceLooksLikeClone()
{
// Project folder is parent of Assets (same as editor project path).
string projectRoot = Path.GetDirectoryName(Application.dataPath);
if (!string.IsNullOrEmpty(projectRoot) &&
projectRoot.IndexOf("clone", StringComparison.OrdinalIgnoreCase) >= 0)
return true;
string product = PlayerSettings.productName;
if (!string.IsNullOrEmpty(product) &&
product.IndexOf("clone", StringComparison.OrdinalIgnoreCase) >= 0)
return true;
return false;
}
#endif
IEnumerator ResumeSessionCoroutine()
{
ClearLocalUserProfile();
bool ok = false;
string errMsg = null;
yield return StartCoroutine(FetchUserDataCoroutine((success, err, _) =>
{
ok = success;
errMsg = err;
}, true));
if (ok)
ProceedToMainMenu();
else
{
InvalidateStoredSession();
if (error_txt != null)
error_txt.text = string.IsNullOrEmpty(errMsg) ? "Session expired. Please sign in again." : errMsg;
}
SetBusy(false);
}
/// Clears cached profile only (token unchanged). Call when leaving a broken state without wiping prefs.
static void ClearLocalUserProfile()
{
_currentUser = null;
}
/// Removes auth token from memory and disk and clears cached profile (login screen recovery).
static void InvalidateStoredSession()
{
AuthToken = "";
ClearLocalUserProfile();
ClearMatchYourTeam();
PlayerPrefs.DeleteKey(PlayerPrefsAuthKey);
PlayerPrefs.Save();
}
static void ProceedToMainMenu()
{
if (LevelLoadManager.instance != null)
LevelLoadManager.LoadLevel("MainMenu");
else
SceneManager.LoadScene("MainMenu");
}
void SetBusy(bool busy)
{
_authInProgress = busy;
if (btnLogin != null) btnLogin.interactable = !busy;
if (btnRegister != null) btnRegister.interactable = !busy;
}
void OnLogin()
{
if (_authInProgress) return;
string username = usernameInputLogin.text;
string password = passwordInputLogin.text;
if (string.IsNullOrEmpty(username) || string.IsNullOrEmpty(password))
{
error_txt.text = "Please fill in all fields";
return;
}
if (username.Length < MIN_USERNAME_LENGTH || username.Length > MAX_USERNAME_LENGTH)
{
error_txt.text = "Username must be between " + MIN_USERNAME_LENGTH + " and " + MAX_USERNAME_LENGTH + " characters";
return;
}
if (password.Length < MIN_PASSWORD_LENGTH || password.Length > MAX_PASSWORD_LENGTH)
{
error_txt.text = "Password must be between " + MIN_PASSWORD_LENGTH + " and " + MAX_PASSWORD_LENGTH + " characters";
return;
}
string json = "{\"username\":\"" + EscapeJsonString(username) + "\",\"password\":\"" + EscapeJsonString(password) + "\"}";
StartCoroutine(AuthPostCoroutine(AuthBaseUrl + "/auth/login", json, "Signed in."));
}
void OnRegister()
{
if (_authInProgress) return;
string username = usernameInputRegister.text;
string password = passwordInputRegister.text;
if (string.IsNullOrEmpty(username) || string.IsNullOrEmpty(password))
{
error_txt.text = "Please fill in all fields";
return;
}
if (username.Length < MIN_USERNAME_LENGTH || username.Length > MAX_USERNAME_LENGTH)
{
error_txt.text = "Username must be between " + MIN_USERNAME_LENGTH + " and " + MAX_USERNAME_LENGTH + " characters";
return;
}
if (password.Length < MIN_PASSWORD_LENGTH || password.Length > MAX_PASSWORD_LENGTH)
{
error_txt.text = "Password must be between " + MIN_PASSWORD_LENGTH + " and " + MAX_PASSWORD_LENGTH + " characters";
return;
}
// Server register endpoint accepts username + password only (email field is optional UI-only).
string json = "{\"username\":\"" + EscapeJsonString(username) + "\",\"password\":\"" + EscapeJsonString(password) + "\"}";
StartCoroutine(AuthPostCoroutine(AuthBaseUrl + "/auth/register", json, "Account created."));
}
static string EscapeJsonString(string s)
{
if (string.IsNullOrEmpty(s)) return s;
return s.Replace("\\", "\\\\").Replace("\"", "\\\"");
}
IEnumerator AuthPostCoroutine(string url, string jsonBody, string successMessage)
{
SetBusy(true);
error_txt.text = "";
ClearLocalUserProfile();
using (var request = new UnityWebRequest(url, UnityWebRequest.kHttpVerbPOST))
{
byte[] bodyRaw = Encoding.UTF8.GetBytes(jsonBody);
request.uploadHandler = new UploadHandlerRaw(bodyRaw);
request.downloadHandler = new DownloadHandlerBuffer();
request.SetRequestHeader("Content-Type", "application/json");
yield return request.SendWebRequest();
string text = request.downloadHandler != null ? request.downloadHandler.text : "";
if (request.result != UnityWebRequest.Result.Success &&
request.result != UnityWebRequest.Result.ProtocolError)
{
error_txt.text = string.IsNullOrEmpty(request.error) ? "Network error" : request.error;
SetBusy(false);
yield break;
}
if (string.IsNullOrEmpty(text))
{
error_txt.text = "Empty response from server";
SetBusy(false);
yield break;
}
AuthApiResponse resp = JsonUtility.FromJson(text);
if (resp.ok && !string.IsNullOrEmpty(resp.token))
{
AuthToken = resp.token;
PlayerPrefs.SetString(PlayerPrefsAuthKey, resp.token);
PlayerPrefs.Save();
bool profileOk = false;
string profileErr = null;
yield return StartCoroutine(FetchUserDataCoroutine((success, err, _) =>
{
profileOk = success;
profileErr = err;
}, true));
if (profileOk)
ProceedToMainMenu();
else if (error_txt != null)
error_txt.text = string.IsNullOrEmpty(profileErr)
? "Signed in but could not load your profile. Check your connection and try again."
: profileErr;
}
else
{
error_txt.text = string.IsNullOrEmpty(resp.error) ? "Request failed" : resp.error;
}
}
SetBusy(false);
}
static void ApplyUserData(UserData u)
{
_currentUser = u;
OnUserDataUpdated?.Invoke(u);
}
void ScheduleUserDataRefreshOnRead()
{
if (string.IsNullOrEmpty(AuthToken)) return;
if (_userDataReadRefreshPending) return;
_userDataReadRefreshPending = true;
StartCoroutine(UserDataReadRefreshCoroutine());
}
IEnumerator UserDataReadRefreshCoroutine()
{
try
{
yield return StartCoroutine(FetchUserDataCoroutine(null, true));
}
finally
{
_userDataReadRefreshPending = false;
}
}
/// GET /auth/user; updates when successful. Callback (success, errorOrNull, userOrNull).
public void GetUserData(Action onComplete)
{
StartCoroutine(FetchUserDataCoroutine(onComplete, true));
}
/// GET /auth/rc-packs (no auth). Callback (success, errorOrNull, jsonOrNull).
public void GetRcPacks(Action onComplete)
{
StartCoroutine(GetRcPacksCoroutine(onComplete));
}
IEnumerator GetRcPacksCoroutine(Action onComplete)
{
string url = AuthBaseUrl + "/auth/rc-packs";
using (var request = UnityWebRequest.Get(url))
{
request.downloadHandler = new DownloadHandlerBuffer();
yield return request.SendWebRequest();
string text = request.downloadHandler != null ? request.downloadHandler.text : "";
if (request.result != UnityWebRequest.Result.Success &&
request.result != UnityWebRequest.Result.ProtocolError)
{
onComplete?.Invoke(false, string.IsNullOrEmpty(request.error) ? "Network error" : request.error, null);
yield break;
}
if (request.responseCode < 200 || request.responseCode >= 300)
{
onComplete?.Invoke(false, string.IsNullOrEmpty(text) ? "Request failed (" + request.responseCode + ")" : text, null);
yield break;
}
onComplete?.Invoke(true, null, text);
}
}
/// POST /auth/purchase-rc with {"pack":"usd_5"|...}. Refreshes profile on success. Callback (success, errorOrNull).
public void PurchaseRcPack(string packId, Action onComplete)
{
StartCoroutine(PurchaseRcCoroutine(packId, onComplete));
}
IEnumerator PurchaseRcCoroutine(string packId, Action onComplete)
{
if (string.IsNullOrEmpty(AuthToken))
{
onComplete?.Invoke(false, "Not signed in");
yield break;
}
if (!RcPack.IsValid(packId))
{
onComplete?.Invoke(false, "Invalid pack (use " + RcPack.Usd5 + ", " + RcPack.Usd20 + ", or " + RcPack.Usd50 + ")");
yield break;
}
string json = "{\"pack\":\"" + EscapeJsonString(packId) + "\"}";
string url = AuthBaseUrl + "/auth/purchase-rc";
using (var request = new UnityWebRequest(url, UnityWebRequest.kHttpVerbPOST))
{
byte[] bodyRaw = Encoding.UTF8.GetBytes(json);
request.uploadHandler = new UploadHandlerRaw(bodyRaw);
request.downloadHandler = new DownloadHandlerBuffer();
request.SetRequestHeader("Content-Type", "application/json");
request.SetRequestHeader("Authorization", "Bearer " + AuthToken);
yield return request.SendWebRequest();
string text = request.downloadHandler != null ? request.downloadHandler.text : "";
if (request.result != UnityWebRequest.Result.Success &&
request.result != UnityWebRequest.Result.ProtocolError)
{
onComplete?.Invoke(false, string.IsNullOrEmpty(request.error) ? "Network error" : request.error);
yield break;
}
if (request.responseCode < 200 || request.responseCode >= 300)
{
onComplete?.Invoke(false, string.IsNullOrEmpty(text) ? "Request failed (" + request.responseCode + ")" : text);
yield break;
}
if (string.IsNullOrEmpty(text))
{
onComplete?.Invoke(false, "Empty response from server");
yield break;
}
PurchaseRcApiResponse resp = JsonUtility.FromJson(text);
if (resp != null && resp.ok)
{
bool refreshOk = false;
string refreshErr = null;
yield return StartCoroutine(FetchUserDataCoroutine((ok, err, u) =>
{
refreshOk = ok;
refreshErr = err;
}, true));
if (refreshOk)
onComplete?.Invoke(true, null);
else
onComplete?.Invoke(false, string.IsNullOrEmpty(refreshErr) ? "Purchase ok but profile refresh failed" : refreshErr);
}
else
{
string err = resp != null && !string.IsNullOrEmpty(resp.error) ? resp.error : "Purchase failed";
onComplete?.Invoke(false, err);
}
}
}
IEnumerator FetchUserDataCoroutine(Action onComplete, bool applyToStatic)
{
if (string.IsNullOrEmpty(AuthToken))
{
onComplete?.Invoke(false, "Not signed in", null);
yield break;
}
string url = AuthBaseUrl + "/auth/user";
using (var request = UnityWebRequest.Get(url))
{
request.downloadHandler = new DownloadHandlerBuffer();
request.SetRequestHeader("Authorization", "Bearer " + AuthToken);
yield return request.SendWebRequest();
string text = request.downloadHandler != null ? request.downloadHandler.text : "";
if (request.result != UnityWebRequest.Result.Success &&
request.result != UnityWebRequest.Result.ProtocolError)
{
onComplete?.Invoke(false, string.IsNullOrEmpty(request.error) ? "Network error" : request.error, null);
yield break;
}
if (request.responseCode < 200 || request.responseCode >= 300)
{
onComplete?.Invoke(false, string.IsNullOrEmpty(text) ? "Request failed (" + request.responseCode + ")" : text, null);
yield break;
}
if (string.IsNullOrEmpty(text))
{
onComplete?.Invoke(false, "Empty response from server", null);
yield break;
}
AuthUserApiResponse resp = JsonUtility.FromJson(text);
if (resp != null && resp.ok && resp.user != null)
{
if (applyToStatic)
ApplyUserData(resp.user);
onComplete?.Invoke(true, null, resp.user);
}
else
{
string err = resp != null && !string.IsNullOrEmpty(resp.error) ? resp.error : "Request failed";
onComplete?.Invoke(false, err, null);
}
}
}
}