Commit 9fa6f827 authored by Yousef Sameh's avatar Yousef Sameh

better auth error messages & leaderboard always include current player

parent cbf5e7b9
......@@ -13,8 +13,91 @@ public class SupabaseAuthentication
public bool IsLoading { get; private set; }
private readonly Dictionary<string, string> _arTranslations = new()
{
{ "Invalid email or password", "البريد الإلكتروني أو كلمة المرور غير صحيحة" },
{ "Incorrect password", "كلمة المرور غير صحيحة" },
{ "Invalid login credentials", "بيانات الدخول غير صالحة" },
{ "Invalid email address format", "صيغة البريد الإلكتروني غير صالحة" },
{ "Please verify your email address", "يرجى التحقق من عنوان بريدك الإلكتروني" },
{ "An account with this email already exists", "يوجد حساب بهذا البريد الإلكتروني مسبقاً" },
{ "Too many attempts. Please try again later", "محاولات كثيرة جداً. يرجى المحاولة لاحقاً" },
{ "Missing required information", "معلومات مطلوبة مفقودة" },
{ "Session expired. Please log in again", "انتهت صلاحية الجلسة. يرجى تسجيل الدخول مجدداً" },
{ "No active session found", "لم يتم العثور على جلسة نشطة" },
{ "No internet connection", "لا يوجد اتصال بالإنترنت" },
{ "Password does not meet requirements", "كلمة المرور لا تستوفي المتطلبات. كلمة المرور يجب ان تكون اكبر من 6" },
{ "Invalid email address", "عنوان البريد الإلكتروني غير صالح" },
{ "Supabase not initialized", "لم يتم تهيئة Supabase" },
{ "Anonymous sign in failed", "فشل تسجيل الدخول كضيف" },
{ "No guest session found.", "لم يتم العثور على جلسة ضيف" },
{ "Conversion failed: Unknown error", "فشل التحويل: خطأ غير معروف" },
{ "No valid session", "جلسة غير صالحة" },
{ "No active session found. Please log in again.", "لم يتم العثور على جلسة نشطة. يرجى تسجيل الدخول مجدداً" },
{ "Server error during deletion. Please try again later.", "خطأ في الخادم أثناء الحذف. يرجى المحاولة لاحقاً" },
{ "Account deleted successfully", "تم حذف الحساب بنجاح" }
};
public bool UseArabicErrors { get; set; } = true;
private SupabaseAuthentication() { }
private string HandleException(Exception ex, string context)
{
Debug.LogError($"[Auth {context}] {ex.Message}");
if (ex is GotrueException gex)
return ParseAuthError(gex);
return Localize(ex.Message);
}
private string Localize(string message)
{
if (!UseArabicErrors || string.IsNullOrEmpty(message))
return message;
return _arTranslations.TryGetValue(message, out var translated) ? translated : message;
}
private string ParseAuthError(GotrueException ex)
{
FailureHint.DetectReason(ex);
switch (ex.Reason)
{
case FailureHint.Reason.UserBadMultiple:
return Localize("Invalid email or password");
case FailureHint.Reason.UserBadPassword:
return Localize("Password does not meet requirements");
case FailureHint.Reason.UserBadLogin:
return Localize("Invalid login credentials");
case FailureHint.Reason.UserBadEmailAddress:
return Localize("Invalid email address format");
case FailureHint.Reason.UserEmailNotConfirmed:
return Localize("Please verify your email address");
case FailureHint.Reason.UserAlreadyRegistered:
return Localize("An account with this email already exists");
case FailureHint.Reason.UserTooManyRequests:
return Localize("Too many attempts. Please try again later");
case FailureHint.Reason.UserMissingInformation:
return Localize("Missing required information");
case FailureHint.Reason.InvalidRefreshToken:
case FailureHint.Reason.ExpiredRefreshToken:
return Localize("Session expired. Please log in again");
case FailureHint.Reason.NoSessionFound:
return Localize("No active session found");
case FailureHint.Reason.Offline:
return Localize("No internet connection");
default:
if (ex.Message.Contains("password", StringComparison.OrdinalIgnoreCase) ||
ex.Content.Contains("password", StringComparison.OrdinalIgnoreCase))
return Localize("Password does not meet requirements");
if (ex.Message.Contains("email", StringComparison.OrdinalIgnoreCase) ||
ex.Content.Contains("email", StringComparison.OrdinalIgnoreCase))
return Localize("Invalid email address");
return Localize(ex.Message);
}
}
public async UniTask<OneOf<Success, string>> EnsureSession()
{
try
......@@ -23,7 +106,7 @@ public class SupabaseAuthentication
var client = SupabaseManager.Instance.Supabase();
if (client == null)
return "Supabase not initialized";
return Localize("Supabase not initialized");
client.Auth.LoadSession();
......@@ -38,11 +121,10 @@ public class SupabaseAuthentication
catch
{
Debug.LogError("[Auth] Session refresh failed, signing out");
// Refresh failed — fall through to anonymous
}
}
return "No valid session";
return Localize("No valid session");
// var session = await client.Auth.SignInAnonymously();
// Debug.Log("[Auth] Signed in anonymously" + $" (user ID: {session?.User.Id})");
......@@ -50,15 +132,9 @@ public class SupabaseAuthentication
// ? new Success()
// : "Anonymous sign in failed";
}
catch (GotrueException ex)
{
Debug.LogError($"[Auth] {ex.Message}");
return ex.Message;
}
catch (Exception ex)
{
Debug.LogError($"[Auth] {ex.Message}");
return ex.Message;
return HandleException(ex, "Session");
}
finally
{
......@@ -73,11 +149,10 @@ public class SupabaseAuthentication
IsLoading = true;
var client = SupabaseManager.Instance.Supabase();
if (client == null) return "Supabase not initialized";
if (client == null) return Localize("Supabase not initialized");
// 1. Verify we actually have a guest user logged in
if (client.Auth.CurrentUser == null)
return "No guest session found.";
return Localize("No guest session found.");
// 2. Update the user with the new credentials
// This 'upgrades' the current anonymous user record
......@@ -93,22 +168,16 @@ public class SupabaseAuthentication
{
Debug.Log($"[Auth] Guest converted to email: {email}");
// Note: If 'Confirm Email' is ON in Supabase, the user
// Note: If 'Confirm Email' is ON in Supabase, the user
// might need to verify their email before the change is final.
return new Success();
}
return "Conversion failed: Unknown error";
}
catch (GotrueException ex)
{
Debug.LogError($"[Auth Convert] {ex.Message}");
return ex.Message;
return Localize("Conversion failed: Unknown error");
}
catch (Exception ex)
{
Debug.LogError($"[Auth Convert] {ex.Message}");
return ex.Message;
return HandleException(ex, "Convert");
}
finally
{
......@@ -124,18 +193,14 @@ public class SupabaseAuthentication
var client = SupabaseManager.Instance.Supabase();
if (client == null)
return "Supabase not initialized";
return Localize("Supabase not initialized");
await client.Auth.SignIn(email, password);
return new Success();
}
catch (GotrueException ex)
{
return ex.Message;
}
catch (Exception ex)
{
return ex.Message;
return HandleException(ex, "Login");
}
finally
{
......@@ -151,21 +216,17 @@ public class SupabaseAuthentication
var client = SupabaseManager.Instance.Supabase();
if (client == null)
return "Supabase not initialized";
return Localize("Supabase not initialized");
var session = await client.Auth.SignInAnonymously();
Debug.Log("[Auth] Signed in anonymously" + $" (user ID: {session?.User.Id})");
return session?.User != null
? new Success()
: "Anonymous sign in failed";
}
catch (GotrueException ex)
{
return ex.Message;
: Localize("Anonymous sign in failed");
}
catch (Exception ex)
{
return ex.Message;
return HandleException(ex, "Anon");
}
finally
{
......@@ -181,19 +242,15 @@ public class SupabaseAuthentication
var client = SupabaseManager.Instance.Supabase();
if (client == null)
return "Supabase not initialized";
return Localize("Supabase not initialized");
await client.Auth.SignOut();
UserService.Instance.ClearUser();
return new Success();
}
catch (GotrueException ex)
{
return ex.Message;
}
catch (Exception ex)
{
return ex.Message;
return HandleException(ex, "Logout");
}
finally
{
......@@ -210,9 +267,8 @@ public class SupabaseAuthentication
var client = SupabaseManager.Instance.Supabase();
if (client == null)
return "Supabase not initialized";
return Localize("Supabase not initialized");
// Optional: Include metadata like 'username' so it's available in auth.users
var options = new Supabase.Gotrue.SignUpOptions();
if (!string.IsNullOrEmpty(username))
{
......@@ -224,19 +280,11 @@ public class SupabaseAuthentication
await client.Auth.SignUp(email.Trim(), password, options);
// Note: If 'Confirm Email' is ON in your Supabase dashboard,
// the user won't be able to sign in until they click the link.
return new Success();
}
catch (GotrueException ex)
{
Debug.LogError($"[Auth Signup] {ex.Message}");
return ex.Message;
}
catch (Exception ex)
{
Debug.LogError($"[Auth Signup] {ex.Message}");
return ex.Message;
return HandleException(ex, "Signup");
}
finally
{
......@@ -253,16 +301,9 @@ public class SupabaseAuthentication
return new Success();
}
catch (GotrueException ex)
{
Debug.LogError($"[Auth Delete] {ex.Message}");
return ex.Message;
}
catch (Exception ex)
{
// If the function returns a 400 or 500, it often lands here
Debug.LogError($"[Auth Delete] {ex.Message}");
return "Server error during deletion. Please try again later.";
return HandleException(ex, "Session");
}
finally
{
......@@ -287,16 +328,9 @@ public class SupabaseAuthentication
return new Success();
}
catch (GotrueException ex)
{
Debug.LogError($"[Auth Delete] {ex.Message}");
return ex.Message;
}
catch (Exception ex)
{
// If the function returns a 400 or 500, it often lands here
Debug.LogError($"[Auth Delete] {ex.Message}");
return "Server error during deletion. Please try again later.";
return HandleException(ex, "Password");
}
finally
{
......@@ -328,16 +362,9 @@ public class SupabaseAuthentication
return new Success();
}
catch (GotrueException ex)
{
Debug.LogError($"[Auth Delete] {ex.Message}");
return ex.Message;
}
catch (Exception ex)
{
// If the function returns a 400 or 500, it often lands here
Debug.LogError($"[Auth Delete] {ex.Message}");
return "Server error during deletion. Please try again later.";
return HandleException(ex, "Reset");
}
finally
{
......@@ -353,55 +380,41 @@ public class SupabaseAuthentication
IsLoading = true;
var client = SupabaseManager.Instance.Supabase();
if (client == null) return "Supabase not initialized";
if (client == null) return Localize("Supabase not initialized");
// 1. Ensure we have a valid session
if (client.Auth.CurrentSession == null)
return "No active session found. Please log in again.";
return Localize("No active session found. Please log in again.");
// 2. Refresh the session to ensure the JWT isn't expired
// This is the most common cause of 401 errors
try
{
await client.Auth.RefreshSession();
}
catch (Exception)
{
return "Session expired. Please log in again.";
return Localize("Session expired. Please log in again.");
}
// 3. Explicitly create the Authorization header
var headers = new Dictionary<string, string>
{
{ "Authorization", $"Bearer {client.Auth.CurrentSession.AccessToken}" }
};
{
{ "Authorization", $"Bearer {client.Auth.CurrentSession.AccessToken}" }
};
var options = new Supabase.Functions.Client.InvokeFunctionOptions
{
Headers = headers
};
// 4. Call the Edge Function with explicit headers
// Pass 'null' for the body if you don't need to send extra data
await client.Functions.Invoke("delete-self", client.Auth.CurrentSession.AccessToken, options);
Debug.Log("[Auth] Account deleted successfully.");
// 5. Cleanup local state
AppRouter.Logout();
return new Success();
}
catch (GotrueException ex)
{
Debug.LogError($"[Auth Delete] {ex.Message}");
return ex.Message;
return Localize("Account deleted successfully");
}
catch (Exception ex)
{
// If the function returns a 400 or 500, it often lands here
Debug.LogError($"[Auth Delete] {ex.Message}");
return "Server error during deletion. Please try again later.";
return HandleException(ex, "Delete");
}
finally
{
......
......@@ -11,16 +11,18 @@ public class LeaderboardService
private Client supabase => SupabaseManager.Instance.Supabase();
public async UniTask<OneOf<List<LeaderboardPlayerModel>, string>> LoadTop100Players()
public async UniTask<OneOf<List<LeaderboardPlayerModel>, string>> LoadTop100PlayersAndCurrentPlayer()
{
var currentUserId = UserService.Instance.CurrentUser?.Id;
if (string.IsNullOrEmpty(currentUserId))
return new List<LeaderboardPlayerModel>();
try
{
// Queries the 'leaderboard' view directly
var response = await supabase
.From<LeaderboardPlayerModel>()
.Get();
.Rpc<List<LeaderboardPlayerModel>>("get_leaderboard_with_user", new { target_user_id = currentUserId });
return response.Models;
return response;
}
catch (Exception e)
{
......
using System;
using System.Collections.Generic;
using System.Threading; // Required for CancellationTokenSource
using System.Threading;
using Cysharp.Threading.Tasks;
using UnityEngine;
using UnityEngine.UIElements;
......@@ -12,17 +12,16 @@ public class LeaderboardController : MonoBehaviour, IDisposable
private VisualElement allPlayersContainer;
private VisualElement root;
// Stores the active cancellation token
private CancellationTokenSource _loadCts;
public int CurrentPlayerSlotIndex { get; private set; } = -1;
void Start()
{
root = leaderboardDocument.rootVisualElement;
allPlayersContainer = root.Q<VisualElement>("AllPlayerContainer");
// Fixed Event Subscription: Use a named method to avoid memory leaks!
UserService.Instance.OnUserChanged += OnUserChanged;
LoadLeaderboard().Forget();
}
......@@ -52,31 +51,34 @@ public class LeaderboardController : MonoBehaviour, IDisposable
try
{
// Note: If you can edit LeaderboardService, update LoadTop100Players()
// to accept this CancellationToken to cancel the actual web request!
// Example: await LeaderboardService.Instance.LoadTop100Players(token);
var result = await LeaderboardService.Instance.LoadTop100Players();
var result = await LeaderboardService.Instance.LoadTop100PlayersAndCurrentPlayer();
// 3. Check if a newer request cancelled this one while we were waiting
if (token.IsCancellationRequested)
return;
result.Switch(
(List<LeaderboardPlayerModel> players) =>
{
CurrentPlayerSlotIndex = -1;
for (int i = 0; i < players.Count; i++)
{
if (i < 3)
ApplyTopThree(i, players[i]);
var player = players[i];
var isOwner = player.Id == UserService.Instance.CurrentUser?.Id;
if (isOwner)
CurrentPlayerSlotIndex = i;
var entry = new CustomLeaderboardSlot
{
PlayerName = player.UserName,
Rank = player.Rank,
XP = player.Points.ToString(),
Index = player.Position.ToString(),
IsOwner = player.Id == UserService.Instance.CurrentUser?.Id
IsOwner = isOwner
};
allPlayersContainer.Add(entry);
......@@ -124,13 +126,11 @@ public class LeaderboardController : MonoBehaviour, IDisposable
public void Dispose()
{
// Now safely removes the event listener
if (UserService.Instance != null)
{
UserService.Instance.OnUserChanged -= OnUserChanged;
}
// Cancel any pending load when this object is destroyed/disposed
if (_loadCts != null)
{
_loadCts.Cancel();
......@@ -139,7 +139,6 @@ public class LeaderboardController : MonoBehaviour, IDisposable
}
}
// Good practice in Unity to ensure Dispose runs if the GameObject is destroyed
private void OnDestroy()
{
Dispose();
......
......@@ -3,13 +3,7 @@ using System;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UIElements;
[Serializable]
public class SupabaseError
{
public int code;
public string error_code;
public string msg;
}
public class LoginPageAnimation : MonoBehaviour
{
[SerializeField] UIDocument loginPage;
......@@ -24,6 +18,8 @@ public class LoginPageAnimation : MonoBehaviour
PlayerPrefs.SetInt("IsGuest", 0);
SupabaseAuthentication.Instance.UseArabicErrors = true;
if (Application.internetReachability == NetworkReachability.NotReachable)
{
ShowUIMessage.Instance.ShowMessage("يرجي الأتصال بالإنترنت وأعد المحاولة", true);
......@@ -162,7 +158,7 @@ public class LoginPageAnimation : MonoBehaviour
},
error =>
{
ShowUIMessage.Instance.ShowMessage("فشل تسجيل الدخول، يرجى التحقق من البريد الإلكتروني وكلمة المرور");
ShowUIMessage.Instance.ShowMessage(error);
loginButton.SetEnabled(true);
loginButton.text = "تسجيل الدخول";
}
......@@ -281,10 +277,8 @@ public class LoginPageAnimation : MonoBehaviour
},
error =>
{
// debug error code and message
Debug.LogError($"ConvertGuestToEmail error: {error}");
ShowUIMessage.Instance.ShowMessage("فشل إنشاء الحساب، يرجى المحاولة مرة أخرى");
ShowUIMessage.Instance.ShowMessage(error);
registerButton.SetEnabled(true);
registerButton.text = "تسجيل";
}
......@@ -313,19 +307,7 @@ public class LoginPageAnimation : MonoBehaviour
},
error =>
{
var json = error.ToString();
var parsed = JsonUtility.FromJson<SupabaseError>(json);
if (parsed.code == 422)
{
ShowUIMessage.Instance.ShowMessage("الحساب مسجل بالفعل، يرجى تسجيل الدخول");
}
else
{
ShowUIMessage.Instance.ShowMessage("فشل إنشاء الحساب، يرجى المحاولة مرة أخرى");
}
ShowUIMessage.Instance.ShowMessage(error);
registerButton.SetEnabled(true);
registerButton.text = "تسجيل";
}
......@@ -397,7 +379,7 @@ public class LoginPageAnimation : MonoBehaviour
error =>
{
Debug.LogError("Registration error: " + error);
ShowUIMessage.Instance.ShowMessage("فشل إنشاء الحساب، يرجى المحاولة مرة أخرى");
ShowUIMessage.Instance.ShowMessage(error);
registerButton.SetEnabled(true);
registerButton.text = "تسجيل";
}
......@@ -466,15 +448,17 @@ public class LoginPageAnimation : MonoBehaviour
sendEmail.SetEnabled(false);
closeForgetPasswordPanel.SetEnabled(false);
try
{
await SupabaseAuthentication.Instance.ResetPasswordRequest(forgetPasswordEmailField.text);
ShowUIMessage.Instance.ShowMessage("تم إرسال بريد إعادة تعيين كلمة المرور، يرجى التحقق من بريدك الإلكتروني");
}
catch (Exception ex)
{
Debug.LogException(ex);
}
var result = await SupabaseAuthentication.Instance.ResetPasswordRequest(forgetPasswordEmailField.text);
result.Switch(
success =>
{
ShowUIMessage.Instance.ShowMessage("تم إرسال بريد إعادة تعيين كلمة المرور، يرجى التحقق من بريدك الإلكتروني");
},
error =>
{
ShowUIMessage.Instance.ShowMessage(error);
}
);
sendEmail.SetEnabled(true);
closeForgetPasswordPanel.SetEnabled(true);
......@@ -523,15 +507,17 @@ public class LoginPageAnimation : MonoBehaviour
updatePassword.SetEnabled(false);
try
{
await SupabaseAuthentication.Instance.UpdatePassword(newPasswordField.text);
ShowUIMessage.Instance.ShowMessage("تم تحديث كلمة المرور بنجاح، يرجى تسجيل الدخول مرة أخرى");
}
catch (Exception ex)
{
Debug.LogException(ex);
}
var result = await SupabaseAuthentication.Instance.UpdatePassword(newPasswordField.text);
result.Switch(
success =>
{
ShowUIMessage.Instance.ShowMessage("تم تحديث كلمة المرور بنجاح، يرجى تسجيل الدخول مرة أخرى");
},
error =>
{
ShowUIMessage.Instance.ShowMessage(error);
}
);
updatePassword.SetEnabled(true);
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment