1342 lines
46 KiB
C#
1342 lines
46 KiB
C#
using System;
|
|
using System.Collections;
|
|
using System.Globalization;
|
|
using System.IO;
|
|
using System.Text;
|
|
using System.Text.RegularExpressions;
|
|
#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://kkapi.playpoolstudios.com";
|
|
const string PlayerPrefsAuthKey = "WalkInvest.AuthToken";
|
|
const string PlayerPrefsDeviceIdKey = "WalkInvest.DeviceId";
|
|
const string AlreadyLoggedInCode = "already_logged_in";
|
|
|
|
const int MIN_USERNAME_LENGTH = 2;
|
|
const int MIN_REGISTER_PASSWORD_LENGTH = 6;
|
|
const int ShortAuthMessageMaxLength = 36;
|
|
const string PlayerConnectionError = "Please check your internet connection and try again.";
|
|
const string PlayerGenericError = "Something went wrong. Please try again.";
|
|
|
|
[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 emailInputRegister;
|
|
|
|
[SerializeField] private TMP_InputField passwordInputRegister;
|
|
[SerializeField] private Button btnRegister;
|
|
|
|
[Header("Server messages (GET /table_settings, no auth)")]
|
|
[Header("Update")]
|
|
[SerializeField] private GameObject serverUpdatePanel;
|
|
[SerializeField] private Button btnServerUpdate;
|
|
[Tooltip("Opened when the user taps Update on the forced-update panel (e.g. Telegram channel).")]
|
|
public string telegramUpdateChannelUrl = "";
|
|
|
|
[Header("Error")]
|
|
[SerializeField] private GameObject errorPanel;
|
|
[SerializeField] private TMP_Text serverErrorMessageTxt;
|
|
[SerializeField] private Button btnErrorClose;
|
|
|
|
[Header("Warning")]
|
|
[SerializeField] private GameObject warningPanel;
|
|
[SerializeField] private TMP_Text serverWarningMessageTxt;
|
|
[SerializeField] private Button btnWarningClose;
|
|
|
|
const float KeepaliveIntervalSeconds = 10f;
|
|
|
|
bool _authInProgress;
|
|
Coroutine _keepaliveRoutine;
|
|
bool _remoteLogoutInFlight;
|
|
bool _retireAfterRemoteLogout;
|
|
bool _appPaused;
|
|
bool _appFocused = true;
|
|
bool _handlingKeepaliveUnauthorized;
|
|
string _pendingAuthMessage;
|
|
string _pendingSessionPassword;
|
|
|
|
public static string AuthToken { get; private set; }
|
|
|
|
/// <summary>Username from the last successful login/register (memory only).</summary>
|
|
public static string SessionUsername { get; private set; }
|
|
|
|
/// <summary>Game password kept in memory only after a successful login/register. Cleared on logout.</summary>
|
|
public static string SessionPassword { get; private set; }
|
|
|
|
/// <summary>Assigned side from the matchmaker <c>your_team</c> field. <c>null</c> until a room is found or after clear.</summary>
|
|
public static Team? MatchYourTeam { get; private set; }
|
|
|
|
/// <summary>Parses matchmaker strings such as <c>"red"</c> / <c>"blue"</c> (case-insensitive).</summary>
|
|
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;
|
|
}
|
|
|
|
/// <summary>Latest user payload from the server. Reading this schedules a refresh (one in flight at a time).</summary>
|
|
public static UserData CurrentUser
|
|
{
|
|
get
|
|
{
|
|
if (instance != null)
|
|
instance.ScheduleUserDataRefreshOnRead();
|
|
return _currentUser;
|
|
}
|
|
}
|
|
|
|
public static Action<UserData> OnUserDataUpdated;
|
|
|
|
static UserData _currentUser;
|
|
bool _userDataReadRefreshPending;
|
|
|
|
public static LoginManager instance;
|
|
|
|
void Awake()
|
|
{
|
|
HideServerMessagePanels();
|
|
|
|
if (instance != null && instance != this)
|
|
{
|
|
instance.StopKeepalive();
|
|
SceneManager.sceneLoaded -= instance.OnSceneLoaded;
|
|
if (instance._remoteLogoutInFlight)
|
|
instance._retireAfterRemoteLogout = true;
|
|
else
|
|
Destroy(instance.gameObject);
|
|
}
|
|
|
|
instance = this;
|
|
DontDestroyOnLoad(gameObject);
|
|
SceneManager.sceneLoaded += OnSceneLoaded;
|
|
|
|
AuthToken = PlayerPrefs.GetString(PlayerPrefsAuthKey, "");
|
|
|
|
if (string.IsNullOrEmpty(AuthToken))
|
|
ClearLocalUserProfile();
|
|
#if !UNITY_EDITOR
|
|
else
|
|
StartKeepalive();
|
|
#endif
|
|
PushNotificationService.EnsureCreated();
|
|
}
|
|
|
|
void Start()
|
|
{
|
|
btnLogin.onClick.AddListener(OnLogin);
|
|
btnRegister.onClick.AddListener(OnRegister);
|
|
if (usernameInputLogin != null)
|
|
usernameInputLogin.characterLimit = 0;
|
|
if (usernameInputRegister != null)
|
|
usernameInputRegister.characterLimit = 0;
|
|
if (btnServerUpdate != null)
|
|
btnServerUpdate.onClick.AddListener(OnServerUpdateClick);
|
|
if (btnErrorClose != null)
|
|
btnErrorClose.onClick.AddListener(OnServerErrorCloseClick);
|
|
|
|
SetBusy(true);
|
|
StartCoroutine(StartupSequenceCoroutine());
|
|
}
|
|
|
|
IEnumerator StartupSequenceCoroutine()
|
|
{
|
|
bool mayContinue = false;
|
|
yield return StartCoroutine(CoApplyTableSettingsGate(r => mayContinue = r));
|
|
|
|
if (!mayContinue)
|
|
{
|
|
// Blocked by update or error panel — keep login UI disabled, but hold the session.
|
|
if (!string.IsNullOrEmpty(AuthToken))
|
|
StartKeepalive();
|
|
SetBusy(false);
|
|
yield break;
|
|
}
|
|
|
|
#if UNITY_EDITOR
|
|
// Dev: log in with clone-aware credentials instead of resuming a saved session.
|
|
StopKeepalive();
|
|
AuthToken = "";
|
|
SessionUsername = "";
|
|
SessionPassword = "";
|
|
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 = BuildLoginBody(devUser, devPass);
|
|
// _pendingSessionPassword = devPass;
|
|
// StartCoroutine(AuthPostCoroutine(AuthBaseUrl + "/auth/login", loginJson, "Signed in."));
|
|
SetBusy(false);
|
|
#else
|
|
if (!string.IsNullOrEmpty(AuthToken))
|
|
{
|
|
StartKeepalive();
|
|
yield return StartCoroutine(ResumeSessionCoroutine());
|
|
}
|
|
else
|
|
SetBusy(false);
|
|
#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)
|
|
{
|
|
PushNotificationService.OnGameSessionReady();
|
|
ProceedToMainMenu();
|
|
}
|
|
else
|
|
{
|
|
InvalidateStoredSession();
|
|
ShowAuthMessage(string.IsNullOrEmpty(errMsg) ? "Session expired. Please sign in again." : errMsg);
|
|
}
|
|
|
|
SetBusy(false);
|
|
}
|
|
|
|
/// <summary>Clears cached profile only (token unchanged). Call when leaving a broken state without wiping prefs.</summary>
|
|
static void ClearLocalUserProfile()
|
|
{
|
|
_currentUser = null;
|
|
}
|
|
|
|
/// <summary>Removes auth token from memory and disk and clears cached profile (login screen recovery).</summary>
|
|
static void InvalidateStoredSession()
|
|
{
|
|
if (instance != null)
|
|
instance.StopKeepalive();
|
|
AuthToken = "";
|
|
SessionUsername = "";
|
|
SessionPassword = "";
|
|
ClearLocalUserProfile();
|
|
ClearMatchYourTeam();
|
|
PlayerPrefs.DeleteKey(PlayerPrefsAuthKey);
|
|
PlayerPrefs.Save();
|
|
}
|
|
|
|
/// <summary>Clears the server session, then local session state and PlayerPrefs.</summary>
|
|
public static void Logout()
|
|
{
|
|
if (MainMenuManager.instance != null)
|
|
MainMenuManager.instance.ClearWithdrawalAddress();
|
|
|
|
string token = AuthToken;
|
|
if (instance != null)
|
|
{
|
|
instance.StopKeepalive();
|
|
if (!string.IsNullOrEmpty(token))
|
|
{
|
|
instance._remoteLogoutInFlight = true;
|
|
instance.StartCoroutine(instance.RemoteLogoutCoroutine(token));
|
|
}
|
|
}
|
|
|
|
AuthToken = "";
|
|
SessionUsername = "";
|
|
SessionPassword = "";
|
|
ClearLocalUserProfile();
|
|
MainMenuManager.ClearCachedSettingsProfile();
|
|
ClearMatchYourTeam();
|
|
OnUserDataUpdated = null;
|
|
SupportClient.ClearSession();
|
|
Cupid.ClearSession();
|
|
CupidLobby.ClearUsername();
|
|
GameManager.ClearMatchSession();
|
|
if (LevelLoadManager.instance != null)
|
|
LevelLoadManager.instance.ResetSession();
|
|
NetPlayer.RedPlayer = null;
|
|
NetPlayer.BluePlayer = null;
|
|
|
|
string deviceId = PlayerPrefs.GetString(PlayerPrefsDeviceIdKey, "");
|
|
PlayerPrefs.DeleteAll();
|
|
if (IsUsableDeviceId(deviceId))
|
|
PlayerPrefs.SetString(PlayerPrefsDeviceIdKey, deviceId);
|
|
PlayerPrefs.Save();
|
|
}
|
|
|
|
/// <summary>Full logout, then reload the login scene so a fresh <see cref="LoginManager"/> can bind UI.</summary>
|
|
public static void LogoutAndReturnToLogin()
|
|
{
|
|
PushNotificationService.UnregisterThen(FinishLogoutAndReturnToLogin);
|
|
}
|
|
|
|
static void FinishLogoutAndReturnToLogin()
|
|
{
|
|
Logout();
|
|
AddressableSceneLoader.Load(AddressableSceneLoader.IntroScene);
|
|
}
|
|
|
|
static void ProceedToMainMenu()
|
|
{
|
|
if (LevelLoadManager.instance != null)
|
|
LevelLoadManager.LoadLevel("MainMenu");
|
|
else
|
|
AddressableSceneLoader.Load(AddressableSceneLoader.MainMenuScene);
|
|
}
|
|
|
|
void SetBusy(bool busy)
|
|
{
|
|
_authInProgress = busy;
|
|
SetButtonInteractable(btnLogin, !busy);
|
|
SetButtonInteractable(btnRegister, !busy);
|
|
}
|
|
|
|
static void SetButtonInteractable(Button button, bool interactable)
|
|
{
|
|
if (button == null)
|
|
return;
|
|
|
|
if (button.interactable == interactable)
|
|
return;
|
|
|
|
button.interactable = interactable;
|
|
if (!interactable)
|
|
return;
|
|
|
|
// Disabling mid-click swallows pointer-up, and the button then ignores later clicks.
|
|
button.enabled = false;
|
|
button.enabled = true;
|
|
}
|
|
|
|
void ShowAuthMessage(string message)
|
|
{
|
|
if (string.IsNullOrEmpty(message))
|
|
return;
|
|
|
|
message = ToPlayerError(message, PlayerGenericError);
|
|
|
|
if (MessageBoxDialog.instance == null)
|
|
{
|
|
_pendingAuthMessage = message;
|
|
return;
|
|
}
|
|
|
|
_pendingAuthMessage = null;
|
|
if (message.Length <= ShortAuthMessageMaxLength)
|
|
MessageBoxDialog.instance.ShowMessageBox(message);
|
|
else
|
|
MessageBoxDialog.instance.ShowMessageBox("Error", message);
|
|
}
|
|
|
|
void HideAuthMessage()
|
|
{
|
|
if (MessageBoxDialog.instance != null)
|
|
MessageBoxDialog.instance.HideMessageBox();
|
|
}
|
|
|
|
void OnSceneLoaded(Scene scene, LoadSceneMode mode)
|
|
{
|
|
if (string.IsNullOrEmpty(_pendingAuthMessage) || MessageBoxDialog.instance == null)
|
|
return;
|
|
|
|
string message = _pendingAuthMessage;
|
|
_pendingAuthMessage = null;
|
|
ShowAuthMessage(message);
|
|
}
|
|
|
|
void OnLogin()
|
|
{
|
|
if (_authInProgress) return;
|
|
// A failed attempt can leave a token and keepalive without leaving this screen.
|
|
// Drop that session so this click performs a new connection check.
|
|
if (_keepaliveRoutine != null && !string.IsNullOrEmpty(AuthToken))
|
|
InvalidateStoredSession();
|
|
|
|
string username = usernameInputLogin.text != null ? usernameInputLogin.text.Trim() : "";
|
|
string password = passwordInputLogin.text ?? "";
|
|
if (string.IsNullOrEmpty(username) || string.IsNullOrEmpty(password))
|
|
{
|
|
ShowAuthMessage("Please fill in all fields");
|
|
return;
|
|
}
|
|
|
|
if (username.Length < MIN_USERNAME_LENGTH)
|
|
{
|
|
ShowAuthMessage("Username must be at least " + MIN_USERNAME_LENGTH + " characters");
|
|
return;
|
|
}
|
|
|
|
_pendingSessionPassword = password;
|
|
StartCoroutine(AuthPostCoroutine(AuthBaseUrl + "/auth/login", BuildLoginBody(username, password), "Signed in."));
|
|
}
|
|
|
|
void OnRegister()
|
|
{
|
|
if (_authInProgress) return;
|
|
if (_keepaliveRoutine != null && !string.IsNullOrEmpty(AuthToken))
|
|
InvalidateStoredSession();
|
|
|
|
string username = usernameInputRegister.text != null ? usernameInputRegister.text.Trim() : "";
|
|
string email = emailInputRegister != null ? emailInputRegister.text : "";
|
|
string password = passwordInputRegister.text ?? "";
|
|
if (string.IsNullOrEmpty(username) || string.IsNullOrEmpty(email) || string.IsNullOrEmpty(password))
|
|
{
|
|
ShowAuthMessage("Please fill in all fields");
|
|
return;
|
|
}
|
|
|
|
email = email.Trim().ToLowerInvariant();
|
|
if (!IsValidEmailFormat(email))
|
|
{
|
|
ShowAuthMessage("Please enter a valid email address");
|
|
return;
|
|
}
|
|
|
|
if (username.Length < MIN_USERNAME_LENGTH)
|
|
{
|
|
ShowAuthMessage("Username must be at least " + MIN_USERNAME_LENGTH + " characters");
|
|
return;
|
|
}
|
|
|
|
if (password.Length < MIN_REGISTER_PASSWORD_LENGTH)
|
|
{
|
|
ShowAuthMessage("Password must be at least " + MIN_REGISTER_PASSWORD_LENGTH + " characters");
|
|
return;
|
|
}
|
|
|
|
_pendingSessionPassword = password;
|
|
StartCoroutine(AuthPostCoroutine(AuthBaseUrl + "/auth/register", BuildRegisterBody(username, email, password), "Account created."));
|
|
}
|
|
|
|
static bool IsValidEmailFormat(string email)
|
|
{
|
|
if (string.IsNullOrEmpty(email))
|
|
return false;
|
|
|
|
int at = email.IndexOf('@');
|
|
if (at <= 0)
|
|
return false;
|
|
|
|
if (email.IndexOf('@', at + 1) >= 0)
|
|
return false;
|
|
|
|
string domain = email.Substring(at + 1);
|
|
int dot = domain.LastIndexOf('.');
|
|
if (dot <= 0 || dot >= domain.Length - 1)
|
|
return false;
|
|
|
|
for (int i = 0; i < email.Length; i++)
|
|
{
|
|
if (char.IsWhiteSpace(email[i]))
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
static string EscapeJsonString(string s)
|
|
{
|
|
if (string.IsNullOrEmpty(s)) return s;
|
|
return s.Replace("\\", "\\\\").Replace("\"", "\\\"");
|
|
}
|
|
|
|
static string BuildLoginBody(string username, string password)
|
|
{
|
|
return "{\"username\":\"" + EscapeJsonString(username)
|
|
+ "\",\"password\":\"" + EscapeJsonString(password)
|
|
+ "\",\"device_id\":\"" + EscapeJsonString(GetDeviceId()) + "\"}";
|
|
}
|
|
|
|
static string BuildRegisterBody(string username, string email, string password)
|
|
{
|
|
return "{\"username\":\"" + EscapeJsonString(username)
|
|
+ "\",\"email\":\"" + EscapeJsonString(email)
|
|
+ "\",\"password\":\"" + EscapeJsonString(password)
|
|
+ "\",\"device_id\":\"" + EscapeJsonString(GetDeviceId()) + "\"}";
|
|
}
|
|
|
|
/// <summary>
|
|
/// Stable id sent on register and login. The server keeps it across logout, so a login
|
|
/// from this device can still match while logout is in flight.
|
|
/// </summary>
|
|
static string GetDeviceId()
|
|
{
|
|
string id = SystemInfo.deviceUniqueIdentifier;
|
|
if (IsUsableDeviceId(id))
|
|
return id;
|
|
|
|
id = PlayerPrefs.GetString(PlayerPrefsDeviceIdKey, "");
|
|
if (IsUsableDeviceId(id))
|
|
return id;
|
|
|
|
id = Guid.NewGuid().ToString("N");
|
|
PlayerPrefs.SetString(PlayerPrefsDeviceIdKey, id);
|
|
PlayerPrefs.Save();
|
|
return id;
|
|
}
|
|
|
|
static bool IsUsableDeviceId(string id)
|
|
{
|
|
return !string.IsNullOrWhiteSpace(id) && id != SystemInfo.unsupportedIdentifier;
|
|
}
|
|
|
|
/// <summary>JsonUtility cannot read <c>"id":"42"</c> into <see cref="UserData.id"/>.</summary>
|
|
static string CoerceQuotedNumericIds(string json)
|
|
{
|
|
if (string.IsNullOrEmpty(json))
|
|
return json;
|
|
return Regex.Replace(json, "\"id\"\\s*:\\s*\"(\\d+)\"", "\"id\":$1");
|
|
}
|
|
|
|
static bool IsTransportFailure(UnityWebRequest request)
|
|
{
|
|
return request.result != UnityWebRequest.Result.Success &&
|
|
request.result != UnityWebRequest.Result.ProtocolError;
|
|
}
|
|
|
|
static string TransportError(UnityWebRequest request)
|
|
{
|
|
string technical = request == null || string.IsNullOrEmpty(request.error) ? "Network error" : request.error;
|
|
string url = request != null ? request.url : "";
|
|
Logger.Log("Network error: " + technical + " url=" + url);
|
|
return PlayerConnectionError;
|
|
}
|
|
|
|
static string PlayerFacingHttpError(string body, long responseCode)
|
|
{
|
|
if (responseCode == 401)
|
|
return "Session expired. Please sign in again.";
|
|
|
|
string apiError = TryReadApiErrorField(body);
|
|
string candidate = !string.IsNullOrWhiteSpace(apiError) ? apiError : body;
|
|
if (string.IsNullOrWhiteSpace(candidate))
|
|
{
|
|
Logger.Log("HTTP " + responseCode + " with no error message");
|
|
return PlayerGenericError;
|
|
}
|
|
|
|
return ToPlayerError(candidate, PlayerGenericError);
|
|
}
|
|
|
|
static string TryReadApiErrorField(string body)
|
|
{
|
|
if (string.IsNullOrWhiteSpace(body))
|
|
return null;
|
|
|
|
string trimmed = body.TrimStart();
|
|
if (trimmed.Length == 0 || trimmed[0] != '{')
|
|
return null;
|
|
|
|
try
|
|
{
|
|
AuthApiResponse resp = JsonUtility.FromJson<AuthApiResponse>(body);
|
|
return resp != null ? resp.error : null;
|
|
}
|
|
catch (Exception)
|
|
{
|
|
return null;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Rewrites engine and transport text ("Cannot resolve destination host", HTTP dumps, raw JSON)
|
|
/// before it reaches the player. Readable server messages are left as-is.
|
|
/// </summary>
|
|
static string ToPlayerError(string message, string fallback)
|
|
{
|
|
if (IsConnectivityError(message))
|
|
{
|
|
Logger.Log("Replaced technical error: " + message);
|
|
return PlayerConnectionError;
|
|
}
|
|
|
|
if (!IsPlayerReadable(message))
|
|
{
|
|
if (!string.IsNullOrWhiteSpace(message))
|
|
Logger.Log("Replaced technical error: " + message);
|
|
return fallback;
|
|
}
|
|
|
|
return message.Trim();
|
|
}
|
|
|
|
static bool IsConnectivityError(string message)
|
|
{
|
|
if (string.IsNullOrWhiteSpace(message))
|
|
return false;
|
|
|
|
string m = message.ToLowerInvariant();
|
|
return m.Contains("resolve host")
|
|
|| m.Contains("resolve destination")
|
|
|| m.Contains("could not resolve")
|
|
|| m.Contains("couldn't resolve")
|
|
|| m.Contains("cannot resolve")
|
|
|| m.Contains("can't resolve")
|
|
|| m.Contains("unable to resolve")
|
|
|| m.Contains("unknown host")
|
|
|| m.Contains("name resolution")
|
|
|| m.Contains("name or service")
|
|
|| m.Contains("destination host")
|
|
|| m.Contains("cannot connect")
|
|
|| m.Contains("can't connect")
|
|
|| m.Contains("could not connect")
|
|
|| m.Contains("couldn't connect")
|
|
|| m.Contains("connection refused")
|
|
|| m.Contains("connection reset")
|
|
|| m.Contains("connection aborted")
|
|
|| m.Contains("connection timed")
|
|
|| m.Contains("network is unreachable")
|
|
|| m.Contains("network error")
|
|
|| m.Contains("no internet")
|
|
|| m.Contains("no route to host")
|
|
|| m.Contains("host unreachable")
|
|
|| m.Contains("failed to receive")
|
|
|| m.Contains("request timeout")
|
|
|| m.Contains("timed out")
|
|
|| m.Contains("timeout")
|
|
|| m.Contains("ssl")
|
|
|| m.Contains("certificate")
|
|
|| m.Contains("curl error")
|
|
|| m.Contains("unknown error")
|
|
|| m.Contains("insecure connection");
|
|
}
|
|
|
|
static bool IsPlayerReadable(string message)
|
|
{
|
|
if (string.IsNullOrWhiteSpace(message) || IsConnectivityError(message))
|
|
return false;
|
|
|
|
string trimmed = message.Trim();
|
|
if (trimmed.Length > 180)
|
|
return false;
|
|
|
|
char c = trimmed[0];
|
|
if (c == '{' || c == '<' || c == '[')
|
|
return false;
|
|
|
|
string m = trimmed.ToLowerInvariant();
|
|
return m.IndexOf("http/1") < 0
|
|
&& m.IndexOf("request failed (") < 0
|
|
&& m.IndexOf("empty response") < 0
|
|
&& m.IndexOf("exception") < 0
|
|
&& m.IndexOf("stack trace") < 0
|
|
&& m.IndexOf("stacktrace") < 0
|
|
&& m.IndexOf("nullreference") < 0
|
|
&& m.IndexOf("data processing error") < 0
|
|
&& m.IndexOf("malformed url") < 0
|
|
&& m.IndexOf('\n') < 0
|
|
&& m.IndexOf('\r') < 0;
|
|
}
|
|
|
|
IEnumerator AuthPostCoroutine(string url, string jsonBody, string successMessage)
|
|
{
|
|
SetBusy(true);
|
|
HideAuthMessage();
|
|
ClearLocalUserProfile();
|
|
|
|
bool enteredMenu = false;
|
|
try
|
|
{
|
|
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();
|
|
|
|
if (IsTransportFailure(request))
|
|
{
|
|
ShowAuthMessage(TransportError(request));
|
|
yield break;
|
|
}
|
|
|
|
string text = CoerceQuotedNumericIds(ReadResponseBody(request));
|
|
|
|
if (string.IsNullOrEmpty(text))
|
|
{
|
|
ShowAuthMessage(PlayerGenericError);
|
|
yield break;
|
|
}
|
|
|
|
AuthApiResponse resp = null;
|
|
try
|
|
{
|
|
resp = JsonUtility.FromJson<AuthApiResponse>(text);
|
|
}
|
|
catch (Exception e)
|
|
{
|
|
Logger.Log("Auth JSON error: " + e.Message);
|
|
}
|
|
|
|
if (resp != null && resp.ok && !string.IsNullOrEmpty(resp.token))
|
|
{
|
|
AuthToken = resp.token;
|
|
PlayerPrefs.SetString(PlayerPrefsAuthKey, resp.token);
|
|
PlayerPrefs.Save();
|
|
|
|
bool profileOk = resp.user != null && !string.IsNullOrEmpty(resp.user.username);
|
|
string profileErr = null;
|
|
if (profileOk)
|
|
ApplyUserData(resp.user);
|
|
else
|
|
{
|
|
yield return StartCoroutine(FetchUserDataCoroutine((success, err, _) =>
|
|
{
|
|
profileOk = success;
|
|
profileErr = err;
|
|
}, true));
|
|
}
|
|
|
|
if (profileOk)
|
|
{
|
|
SessionUsername = _currentUser != null ? _currentUser.username : "";
|
|
SessionPassword = _pendingSessionPassword ?? "";
|
|
_pendingSessionPassword = null;
|
|
enteredMenu = true;
|
|
StartKeepalive();
|
|
PushNotificationService.OnGameSessionReady();
|
|
ProceedToMainMenu();
|
|
}
|
|
else
|
|
{
|
|
_pendingSessionPassword = null;
|
|
InvalidateStoredSession();
|
|
ShowAuthMessage(string.IsNullOrEmpty(profileErr)
|
|
? PlayerConnectionError
|
|
: profileErr);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
_pendingSessionPassword = null;
|
|
string serverError = resp != null ? resp.error : null;
|
|
if (string.IsNullOrEmpty(serverError) && resp != null && resp.code == AlreadyLoggedInCode)
|
|
serverError = "Already logged in on another device";
|
|
ShowAuthMessage(string.IsNullOrEmpty(serverError)
|
|
? "Login failed. Please try again."
|
|
: serverError);
|
|
}
|
|
}
|
|
}
|
|
finally
|
|
{
|
|
if (!enteredMenu)
|
|
{
|
|
_pendingSessionPassword = null;
|
|
SetBusy(false);
|
|
}
|
|
}
|
|
}
|
|
|
|
static string ReadResponseBody(UnityWebRequest request)
|
|
{
|
|
if (request == null || request.downloadHandler == null)
|
|
return "";
|
|
|
|
try
|
|
{
|
|
return request.downloadHandler.text ?? "";
|
|
}
|
|
catch (Exception e)
|
|
{
|
|
Logger.Log("Auth response unreadable: " + e.Message);
|
|
return "";
|
|
}
|
|
}
|
|
|
|
void HideServerMessagePanels()
|
|
{
|
|
if (serverUpdatePanel != null) serverUpdatePanel.SetActive(false);
|
|
if (errorPanel != null) errorPanel.SetActive(false);
|
|
if (warningPanel != null) warningPanel.SetActive(false);
|
|
}
|
|
|
|
void OnServerUpdateClick()
|
|
{
|
|
if (!string.IsNullOrWhiteSpace(telegramUpdateChannelUrl))
|
|
Application.OpenURL(telegramUpdateChannelUrl.Trim());
|
|
}
|
|
|
|
void OnServerErrorCloseClick()
|
|
{
|
|
#if UNITY_EDITOR
|
|
EditorApplication.ExitPlaymode();
|
|
#else
|
|
Application.Quit();
|
|
#endif
|
|
}
|
|
|
|
/// <summary>
|
|
/// GET <c>/table_settings</c> (public) and applies version / error / warning gates.
|
|
/// Callback receives <c>true</c> when login may continue to the main menu.
|
|
/// </summary>
|
|
IEnumerator CoApplyTableSettingsGate(Action<bool> onComplete)
|
|
{
|
|
CupidTableSettings table = null;
|
|
yield return StartCoroutine(FetchTableSettingsCoroutine(t => table = t));
|
|
|
|
if (table == null)
|
|
{
|
|
onComplete?.Invoke(true);
|
|
yield break;
|
|
}
|
|
|
|
if (IsClientBelowRequiredVersion(table.version))
|
|
{
|
|
if (serverUpdatePanel != null)
|
|
serverUpdatePanel.SetActive(true);
|
|
onComplete?.Invoke(false);
|
|
yield break;
|
|
}
|
|
|
|
if (!string.IsNullOrWhiteSpace(table.error_msg))
|
|
{
|
|
if (serverErrorMessageTxt != null)
|
|
serverErrorMessageTxt.text = table.error_msg.Trim();
|
|
if (errorPanel != null)
|
|
errorPanel.SetActive(true);
|
|
onComplete?.Invoke(false);
|
|
yield break;
|
|
}
|
|
|
|
if (!string.IsNullOrWhiteSpace(table.warning_msg))
|
|
yield return CoWaitForWarningDismiss(table.warning_msg.Trim());
|
|
|
|
onComplete?.Invoke(true);
|
|
}
|
|
|
|
IEnumerator FetchTableSettingsCoroutine(Action<CupidTableSettings> onResult)
|
|
{
|
|
string url = AuthBaseUrl + "/table_settings";
|
|
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)
|
|
{
|
|
onResult?.Invoke(null);
|
|
yield break;
|
|
}
|
|
|
|
if (request.responseCode < 200 || request.responseCode >= 300 || string.IsNullOrWhiteSpace(text))
|
|
{
|
|
onResult?.Invoke(null);
|
|
yield break;
|
|
}
|
|
|
|
try
|
|
{
|
|
CupidTableSettings parsed = JsonUtility.FromJson<CupidTableSettings>(text);
|
|
onResult?.Invoke(parsed);
|
|
}
|
|
catch (Exception e)
|
|
{
|
|
Logger.Log("table_settings JSON error: " + e.Message + " body=" + text);
|
|
onResult?.Invoke(null);
|
|
}
|
|
}
|
|
}
|
|
|
|
static bool IsClientBelowRequiredVersion(string requiredVersion)
|
|
{
|
|
if (string.IsNullOrWhiteSpace(requiredVersion))
|
|
return false;
|
|
if (!TryParseVersionFloat(requiredVersion, out float required))
|
|
return false;
|
|
if (!TryParseVersionFloat(Application.version, out float client))
|
|
return false;
|
|
return client < required;
|
|
}
|
|
|
|
static bool TryParseVersionFloat(string version, out float value)
|
|
{
|
|
value = 0f;
|
|
if (string.IsNullOrWhiteSpace(version))
|
|
return false;
|
|
return float.TryParse(version.Trim(), NumberStyles.Float, CultureInfo.InvariantCulture, out value);
|
|
}
|
|
|
|
IEnumerator CoWaitForWarningDismiss(string message)
|
|
{
|
|
if (serverWarningMessageTxt != null)
|
|
serverWarningMessageTxt.text = message;
|
|
if (warningPanel != null)
|
|
warningPanel.SetActive(true);
|
|
|
|
bool dismissed = false;
|
|
void OnDismiss()
|
|
{
|
|
dismissed = true;
|
|
if (warningPanel != null)
|
|
warningPanel.SetActive(false);
|
|
}
|
|
|
|
if (btnWarningClose != null)
|
|
{
|
|
btnWarningClose.onClick.RemoveListener(OnDismiss);
|
|
btnWarningClose.onClick.AddListener(OnDismiss);
|
|
}
|
|
|
|
yield return new WaitUntil(() => dismissed);
|
|
|
|
if (btnWarningClose != null)
|
|
btnWarningClose.onClick.RemoveListener(OnDismiss);
|
|
}
|
|
|
|
static void ApplyUserData(UserData u)
|
|
{
|
|
_currentUser = u;
|
|
if (u != null)
|
|
SupportClient.ClearSessionIfDifferentUser(u.username);
|
|
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;
|
|
}
|
|
}
|
|
|
|
/// <summary>GET /auth/user; updates <see cref="CurrentUser"/> when successful. Callback (success, errorOrNull, userOrNull).</summary>
|
|
public void GetUserData(Action<bool, string, UserData> onComplete)
|
|
{
|
|
StartCoroutine(FetchUserDataCoroutine(onComplete, true));
|
|
}
|
|
|
|
/// <summary>GET /auth/rc-packs (no auth). Callback (success, errorOrNull, jsonOrNull).</summary>
|
|
public void GetRcPacks(Action<bool, string, string> onComplete)
|
|
{
|
|
StartCoroutine(GetRcPacksCoroutine(onComplete));
|
|
}
|
|
|
|
IEnumerator GetRcPacksCoroutine(Action<bool, string, string> 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 (IsTransportFailure(request))
|
|
{
|
|
onComplete?.Invoke(false, TransportError(request), null);
|
|
yield break;
|
|
}
|
|
|
|
if (request.responseCode < 200 || request.responseCode >= 300)
|
|
{
|
|
onComplete?.Invoke(false, PlayerFacingHttpError(text, request.responseCode), null);
|
|
yield break;
|
|
}
|
|
|
|
onComplete?.Invoke(true, null, text);
|
|
}
|
|
}
|
|
|
|
/// <summary>POST /auth/purchase-rc with <c>{"pack":"usd_5"|...}</c>. Refreshes profile on success. Callback (success, errorOrNull).</summary>
|
|
public void PurchaseRcPack(string packId, Action<bool, string> onComplete)
|
|
{
|
|
StartCoroutine(PurchaseRcCoroutine(packId, onComplete));
|
|
}
|
|
|
|
/// <summary>POST /auth/ping-report with <c>{"match_id":123,"ping":42}</c>. Callback (success, errorOrNull, reportOrNull).</summary>
|
|
public void ReportPing(int matchId, int ping, Action<bool, string, PingReportData> onComplete)
|
|
{
|
|
StartCoroutine(ReportPingCoroutine(matchId, ping, onComplete));
|
|
}
|
|
|
|
IEnumerator PurchaseRcCoroutine(string packId, Action<bool, string> 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 (IsTransportFailure(request))
|
|
{
|
|
onComplete?.Invoke(false, TransportError(request));
|
|
yield break;
|
|
}
|
|
|
|
if (request.responseCode < 200 || request.responseCode >= 300)
|
|
{
|
|
onComplete?.Invoke(false, PlayerFacingHttpError(text, request.responseCode));
|
|
yield break;
|
|
}
|
|
|
|
if (string.IsNullOrEmpty(text))
|
|
{
|
|
onComplete?.Invoke(false, PlayerGenericError);
|
|
yield break;
|
|
}
|
|
|
|
PurchaseRcApiResponse resp = JsonUtility.FromJson<PurchaseRcApiResponse>(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) ? PlayerGenericError : ToPlayerError(refreshErr, PlayerGenericError));
|
|
}
|
|
else
|
|
{
|
|
string err = resp != null && !string.IsNullOrEmpty(resp.error) ? resp.error : null;
|
|
onComplete?.Invoke(false, ToPlayerError(err, "Purchase failed. Please try again."));
|
|
}
|
|
}
|
|
}
|
|
|
|
IEnumerator ReportPingCoroutine(int matchId, int ping, Action<bool, string, PingReportData> onComplete)
|
|
{
|
|
if (string.IsNullOrEmpty(AuthToken))
|
|
{
|
|
onComplete?.Invoke(false, "Not signed in", null);
|
|
yield break;
|
|
}
|
|
|
|
string json = "{\"match_id\":" + matchId + ",\"ping\":" + ping + "}";
|
|
string url = AuthBaseUrl + "/auth/ping-report";
|
|
|
|
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 (IsTransportFailure(request))
|
|
{
|
|
onComplete?.Invoke(false, TransportError(request), null);
|
|
yield break;
|
|
}
|
|
|
|
if (request.responseCode < 200 || request.responseCode >= 300)
|
|
{
|
|
onComplete?.Invoke(false, PlayerFacingHttpError(text, request.responseCode), null);
|
|
yield break;
|
|
}
|
|
|
|
if (string.IsNullOrEmpty(text))
|
|
{
|
|
onComplete?.Invoke(false, PlayerGenericError, null);
|
|
yield break;
|
|
}
|
|
|
|
PingReportApiResponse resp = JsonUtility.FromJson<PingReportApiResponse>(text);
|
|
if (resp != null && resp.ok && resp.report != null)
|
|
onComplete?.Invoke(true, null, resp.report);
|
|
else
|
|
{
|
|
string err = resp != null && !string.IsNullOrEmpty(resp.error) ? resp.error : null;
|
|
onComplete?.Invoke(false, ToPlayerError(err, PlayerGenericError), null);
|
|
}
|
|
}
|
|
}
|
|
|
|
IEnumerator FetchUserDataCoroutine(Action<bool, string, UserData> 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 (IsTransportFailure(request))
|
|
{
|
|
onComplete?.Invoke(false, TransportError(request), null);
|
|
yield break;
|
|
}
|
|
|
|
if (request.responseCode < 200 || request.responseCode >= 300)
|
|
{
|
|
onComplete?.Invoke(false, PlayerFacingHttpError(text, request.responseCode), null);
|
|
yield break;
|
|
}
|
|
|
|
if (string.IsNullOrEmpty(text))
|
|
{
|
|
onComplete?.Invoke(false, PlayerGenericError, null);
|
|
yield break;
|
|
}
|
|
|
|
AuthUserApiResponse resp = JsonUtility.FromJson<AuthUserApiResponse>(CoerceQuotedNumericIds(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 : null;
|
|
onComplete?.Invoke(false, ToPlayerError(err, PlayerGenericError), null);
|
|
}
|
|
}
|
|
}
|
|
|
|
bool IsAppInForeground => !_appPaused && _appFocused;
|
|
|
|
void OnApplicationPause(bool paused)
|
|
{
|
|
_appPaused = paused;
|
|
}
|
|
|
|
void OnApplicationFocus(bool hasFocus)
|
|
{
|
|
_appFocused = hasFocus;
|
|
}
|
|
|
|
void OnDestroy()
|
|
{
|
|
SceneManager.sceneLoaded -= OnSceneLoaded;
|
|
StopKeepalive();
|
|
if (instance == this)
|
|
instance = null;
|
|
}
|
|
|
|
void StartKeepalive()
|
|
{
|
|
if (string.IsNullOrEmpty(AuthToken)) return;
|
|
if (_keepaliveRoutine != null) return;
|
|
_handlingKeepaliveUnauthorized = false;
|
|
_keepaliveRoutine = StartCoroutine(KeepaliveLoopCoroutine());
|
|
}
|
|
|
|
void StopKeepalive()
|
|
{
|
|
if (_keepaliveRoutine == null) return;
|
|
StopCoroutine(_keepaliveRoutine);
|
|
_keepaliveRoutine = null;
|
|
}
|
|
|
|
IEnumerator KeepaliveLoopCoroutine()
|
|
{
|
|
while (!string.IsNullOrEmpty(AuthToken))
|
|
{
|
|
while (!IsAppInForeground && !string.IsNullOrEmpty(AuthToken))
|
|
yield return null;
|
|
|
|
if (string.IsNullOrEmpty(AuthToken))
|
|
break;
|
|
|
|
yield return KeepalivePingCoroutine();
|
|
if (string.IsNullOrEmpty(AuthToken))
|
|
break;
|
|
|
|
float elapsed = 0f;
|
|
while (elapsed < KeepaliveIntervalSeconds && !string.IsNullOrEmpty(AuthToken))
|
|
{
|
|
if (!IsAppInForeground)
|
|
{
|
|
while (!IsAppInForeground && !string.IsNullOrEmpty(AuthToken))
|
|
yield return null;
|
|
break;
|
|
}
|
|
|
|
elapsed += Time.unscaledDeltaTime;
|
|
yield return null;
|
|
}
|
|
}
|
|
|
|
_keepaliveRoutine = null;
|
|
}
|
|
|
|
IEnumerator RemoteLogoutCoroutine(string token)
|
|
{
|
|
try
|
|
{
|
|
string url = AuthBaseUrl + "/auth/logout";
|
|
using (var request = new UnityWebRequest(url, UnityWebRequest.kHttpVerbPOST))
|
|
{
|
|
request.uploadHandler = new UploadHandlerRaw(Array.Empty<byte>());
|
|
request.downloadHandler = new DownloadHandlerBuffer();
|
|
request.SetRequestHeader("Authorization", "Bearer " + token);
|
|
|
|
yield return request.SendWebRequest();
|
|
|
|
if (IsTransportFailure(request))
|
|
Logger.Log("auth/logout network error: " + (string.IsNullOrEmpty(request.error) ? "Network error" : request.error));
|
|
else if (request.responseCode < 200 || request.responseCode >= 300)
|
|
Logger.Log("auth/logout failed: HTTP " + request.responseCode);
|
|
}
|
|
}
|
|
finally
|
|
{
|
|
_remoteLogoutInFlight = false;
|
|
if (_retireAfterRemoteLogout)
|
|
Destroy(gameObject);
|
|
}
|
|
}
|
|
|
|
IEnumerator KeepalivePingCoroutine()
|
|
{
|
|
if (string.IsNullOrEmpty(AuthToken))
|
|
yield break;
|
|
|
|
string url = AuthBaseUrl + "/auth/keepalive";
|
|
using (var request = new UnityWebRequest(url, UnityWebRequest.kHttpVerbPOST))
|
|
{
|
|
request.uploadHandler = new UploadHandlerRaw(Array.Empty<byte>());
|
|
request.downloadHandler = new DownloadHandlerBuffer();
|
|
request.SetRequestHeader("Content-Type", "application/json");
|
|
request.SetRequestHeader("Authorization", "Bearer " + AuthToken);
|
|
|
|
yield return request.SendWebRequest();
|
|
|
|
if (request.responseCode == 401)
|
|
{
|
|
string text = request.downloadHandler != null ? request.downloadHandler.text : "";
|
|
string err = "Session expired. Please sign in again.";
|
|
if (!string.IsNullOrEmpty(text))
|
|
{
|
|
AuthApiResponse resp = JsonUtility.FromJson<AuthApiResponse>(text);
|
|
if (resp != null && !string.IsNullOrEmpty(resp.error))
|
|
err = ToPlayerError(resp.error, err);
|
|
}
|
|
|
|
HandleKeepaliveUnauthorized(err);
|
|
yield break;
|
|
}
|
|
|
|
if (request.result != UnityWebRequest.Result.Success &&
|
|
request.result != UnityWebRequest.Result.ProtocolError)
|
|
{
|
|
Logger.Log("auth/keepalive network error: " + (string.IsNullOrEmpty(request.error) ? "Network error" : request.error));
|
|
yield break;
|
|
}
|
|
|
|
if (request.responseCode < 200 || request.responseCode >= 300)
|
|
Logger.Log("auth/keepalive failed: HTTP " + request.responseCode);
|
|
}
|
|
}
|
|
|
|
void HandleKeepaliveUnauthorized(string message)
|
|
{
|
|
if (_handlingKeepaliveUnauthorized) return;
|
|
_handlingKeepaliveUnauthorized = true;
|
|
|
|
SetBusy(false);
|
|
ShowAuthMessage(string.IsNullOrEmpty(message) ? "Session expired. Please sign in again." : message);
|
|
LogoutAndReturnToLogin();
|
|
}
|
|
|
|
}
|