using System; using System.Collections; using System.Text; using TMPro; using UnityEngine; using UnityEngine.Networking; using UnityEngine.SceneManagement; using UnityEngine.UI; public class LoginManager : MonoBehaviour { const string AuthBaseUrl = "http://vps.playpoolstudios.com:2612"; 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; } /// 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; [Serializable] class AuthApiResponse { public bool ok; public string error; public string token; } [Serializable] class AuthUserApiResponse { public bool ok; public string error; public UserData user; } [Serializable] class PurchaseRcApiResponse { public bool ok; public string error; } public static LoginManager instance; void Awake() { error_txt.text = ""; AuthToken = PlayerPrefs.GetString(PlayerPrefsAuthKey, ""); if(instance !=null){ Destroy(gameObject); return; } instance = this; DontDestroyOnLoad(gameObject); } void Start() { btnLogin.onClick.AddListener(OnLogin); btnRegister.onClick.AddListener(OnRegister); if (!string.IsNullOrEmpty(AuthToken)) { SetBusy(true); StartCoroutine(ResumeSessionCoroutine()); } } IEnumerator ResumeSessionCoroutine() { yield return StartCoroutine(FetchUserDataCoroutine(null, true)); ProceedToMainMenu(); SetBusy(false); } 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 = ""; 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(); yield return StartCoroutine(FetchUserDataCoroutine(null, true)); ProceedToMainMenu(); } 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); } } } } [Serializable] public class UserData { public int id; public string username; public int cc; public int rc; public string created_at; public string last_logged_at; } /// Typical pack ids for ; prefer for the live list. public static class RcPack { public const string Usd5 = "usd_5"; public const string Usd20 = "usd_20"; public const string Usd50 = "usd_50"; public static bool IsValid(string packId) { return packId == Usd5 || packId == Usd20 || packId == Usd50; } } public static class CoinHelper{ public static float Format(int coins){ int whole = coins / 4; int fraction = coins % 4; return float.Parse(whole + "." + fraction); } public static string FormatString(int coins){ return Format(coins).ToString("N2"); } }