From e556262effca007c9691a13f96de9b02365fcab3 Mon Sep 17 00:00:00 2001 From: Nik Ho Date: Tue, 5 Aug 2025 20:47:27 +1200 Subject: [PATCH 01/31] feat(passport): add passport manager prefab --- .../Immutable.Passport.Runtime.Private.asmdef | 4 +- .../Public/Immutable.Passport.Runtime.asmdef | 8 +- .../Runtime/Scripts/Public/PassportManager.cs | 444 ++++++++++++++++++ .../Scripts/Public/PassportManager.cs.meta | 11 + 4 files changed, 465 insertions(+), 2 deletions(-) create mode 100644 src/Packages/Passport/Runtime/Scripts/Public/PassportManager.cs create mode 100644 src/Packages/Passport/Runtime/Scripts/Public/PassportManager.cs.meta diff --git a/src/Packages/Passport/Runtime/Scripts/Private/Immutable.Passport.Runtime.Private.asmdef b/src/Packages/Passport/Runtime/Scripts/Private/Immutable.Passport.Runtime.Private.asmdef index bbb0c0aa..a171ed6a 100644 --- a/src/Packages/Passport/Runtime/Scripts/Private/Immutable.Passport.Runtime.Private.asmdef +++ b/src/Packages/Passport/Runtime/Scripts/Private/Immutable.Passport.Runtime.Private.asmdef @@ -6,7 +6,9 @@ "UniTask", "Immutable.Browser.Core", "Immutable.Browser.Gree", - "Immutable.Passport.Core.Logging" + "Immutable.Passport.Core.Logging", + "Unity.Modules.JSONSerialize", + "Unity.Modules.Core" ], "includePlatforms": [ "Android", diff --git a/src/Packages/Passport/Runtime/Scripts/Public/Immutable.Passport.Runtime.asmdef b/src/Packages/Passport/Runtime/Scripts/Public/Immutable.Passport.Runtime.asmdef index 73c95a28..4ce93041 100644 --- a/src/Packages/Passport/Runtime/Scripts/Public/Immutable.Passport.Runtime.asmdef +++ b/src/Packages/Passport/Runtime/Scripts/Public/Immutable.Passport.Runtime.asmdef @@ -7,7 +7,13 @@ "Immutable.Browser.Gree", "Immutable.Passport.Runtime.Private", "Immutable.Passport.Core.Logging", - "VoltstroStudios.UnityWebBrowser" + "VoltstroStudios.UnityWebBrowser", + "Unity.Modules.UnityWebRequest", + "Unity.Modules.AndroidJNI", + "Unity.Modules.JSONSerialize", + "Unity.Modules.Core", + "Unity.Modules.UI", + "Unity.TextMeshPro" ], "includePlatforms": [ "Android", diff --git a/src/Packages/Passport/Runtime/Scripts/Public/PassportManager.cs b/src/Packages/Passport/Runtime/Scripts/Public/PassportManager.cs new file mode 100644 index 00000000..aa420cb4 --- /dev/null +++ b/src/Packages/Passport/Runtime/Scripts/Public/PassportManager.cs @@ -0,0 +1,444 @@ +using UnityEngine; +using UnityEngine.UI; +using TMPro; +using Immutable.Passport; +using Immutable.Passport.Core.Logging; +using Immutable.Passport.Model; +using System; +using Cysharp.Threading.Tasks; + +namespace Immutable.Passport +{ + /// + /// A convenient manager component for Immutable Passport that can be dropped into any scene. + /// Automatically handles Passport initialization and provides easy configuration options. + /// + public class PassportManager : MonoBehaviour + { + [Header("Passport Configuration")] + [SerializeField] private string clientId = "your-client-id-here"; + + [SerializeField] private string environment = Immutable.Passport.Model.Environment.SANDBOX; + + [Header("Redirect URIs (required for authentication)")] + [SerializeField] + [Tooltip("The redirect URI for successful login (e.g., 'mygame://callback')")] + private string redirectUri = ""; + [SerializeField] + [Tooltip("The redirect URI for logout (e.g., 'mygame://logout')")] + private string logoutRedirectUri = ""; + + [Header("Settings")] + [SerializeField] private bool autoInitialize = true; + [SerializeField] private bool autoLogin = false; + [SerializeField] private DirectLoginMethod directLoginMethod = DirectLoginMethod.None; + [SerializeField] private LogLevel logLevel = LogLevel.Info; + [SerializeField] private bool redactTokensInLogs = true; + + [Header("UI Integration (Optional)")] + [SerializeField] + [Tooltip("Button to trigger default login (will be automatically configured)")] + private Button loginButton; + [SerializeField] + [Tooltip("Button to trigger Google login (will be automatically configured)")] + private Button googleLoginButton; + [SerializeField] + [Tooltip("Button to trigger Apple login (will be automatically configured)")] + private Button appleLoginButton; + [SerializeField] + [Tooltip("Button to trigger Facebook login (will be automatically configured)")] + private Button facebookLoginButton; + [SerializeField] + [Tooltip("Button to trigger logout (will be automatically configured)")] + private Button logoutButton; + [SerializeField] + [Tooltip("Legacy Text component to display authentication status. Use this OR TextMeshPro Status Text below.")] + private Text statusText; + [SerializeField] + [Tooltip("TextMeshPro component to display authentication status. Use this OR Legacy Status Text above.")] + private TextMeshProUGUI statusTextTMP; + [SerializeField] + [Tooltip("Legacy Text component to display user information after login. Use this OR TextMeshPro User Info Text below.")] + private Text userInfoText; + [SerializeField] + [Tooltip("TextMeshPro component to display user information after login. Use this OR Legacy User Info Text above.")] + private TextMeshProUGUI userInfoTextTMP; + + [Header("Events")] + public UnityEngine.Events.UnityEvent OnPassportInitialized; + public UnityEngine.Events.UnityEvent OnPassportError; + public UnityEngine.Events.UnityEvent OnLoginSucceeded; + public UnityEngine.Events.UnityEvent OnLoginFailed; + public UnityEngine.Events.UnityEvent OnLogoutSucceeded; + public UnityEngine.Events.UnityEvent OnLogoutFailed; + + public static PassportManager Instance { get; private set; } + public Passport PassportInstance { get; private set; } + public bool IsInitialized { get; private set; } + public bool IsLoggedIn { get; private set; } + + private void Awake() + { + // Singleton pattern + if (Instance == null) + { + Instance = this; + DontDestroyOnLoad(gameObject); + } + else + { + Destroy(gameObject); + return; + } + } + + private void Start() + { + // Configure UI elements if provided + ConfigureUIElements(); + + if (autoInitialize) + { + InitializePassport(); + } + } + + /// + /// Initialize Passport with the configured settings + /// + public async void InitializePassport() + { + if (IsInitialized) + { + Debug.LogWarning("[PassportManager] Passport is already initialized."); + return; + } + + if (string.IsNullOrEmpty(clientId) || clientId == "your-client-id-here") + { + string error = "Please set a valid Client ID in the PassportManager component"; + Debug.LogError($"[PassportManager] {error}"); + OnPassportError?.Invoke(error); + return; + } + + try + { + // Configure logging + Passport.LogLevel = logLevel; + Passport.RedactTokensInLogs = redactTokensInLogs; + + // Auto-configure redirect URIs if not set + string finalRedirectUri = GetRedirectUri(); + string finalLogoutRedirectUri = GetLogoutRedirectUri(); + + Debug.Log($"[PassportManager] Initializing Passport with Client ID: {clientId}"); + + // Initialize Passport + PassportInstance = await Passport.Init(clientId, environment, finalRedirectUri, finalLogoutRedirectUri); + + IsInitialized = true; + Debug.Log("[PassportManager] Passport initialized successfully!"); + OnPassportInitialized?.Invoke(); + + // Update UI state after initialization + UpdateUIState(); + + // Auto-login if enabled + if (autoLogin) + { + Debug.Log("[PassportManager] Auto-login enabled, attempting login..."); + await LoginAsync(); + var accessToken = await PassportInstance.GetAccessToken(); + Debug.Log($"[PassportManager] Access token: {accessToken}"); + } + } + catch (Exception ex) + { + string error = $"Failed to initialize Passport: {ex.Message}"; + Debug.LogError($"[PassportManager] {error}"); + OnPassportError?.Invoke(error); + } + } + + /// + /// Get the redirect URI - must be configured in Inspector + /// + private string GetRedirectUri() + { + if (string.IsNullOrEmpty(redirectUri)) + { + throw new System.InvalidOperationException( + "Redirect URI must be configured in the PassportManager Inspector. " + + "Example: 'yourapp://callback'"); + } + + return redirectUri; + } + + /// + /// Get the logout redirect URI - must be configured in Inspector + /// + private string GetLogoutRedirectUri() + { + if (string.IsNullOrEmpty(logoutRedirectUri)) + { + throw new System.InvalidOperationException( + "Logout Redirect URI must be configured in the PassportManager Inspector. " + + "Example: 'yourapp://logout'"); + } + + return logoutRedirectUri; + } + + /// + /// Quick access to login functionality using the configured direct login method + /// + public async void Login() + { + await LoginAsync(); + } + + /// + /// Login with a specific direct login method + /// + /// The login method to use (Google, Apple, Facebook, or None for default) + public async void Login(DirectLoginMethod loginMethod) + { + await LoginAsync(loginMethod); + } + + /// + /// Internal async login method + /// + /// Optional login method override. If not provided, uses the configured directLoginMethod + private async UniTask LoginAsync(DirectLoginMethod? loginMethod = null) + { + if (!IsInitialized || PassportInstance == null) + { + Debug.LogError("[PassportManager] Passport not initialized. Call InitializePassport() first."); + return; + } + + try + { + DirectLoginMethod methodToUse = loginMethod ?? directLoginMethod; + string loginMethodText = methodToUse == DirectLoginMethod.None + ? "default method" + : methodToUse.ToString(); + Debug.Log($"[PassportManager] Attempting login with {loginMethodText}..."); + + bool loginSuccess = await PassportInstance.Login(useCachedSession: false, directLoginMethod: methodToUse); + if (loginSuccess) + { + IsLoggedIn = true; + Debug.Log("[PassportManager] Login successful!"); + OnLoginSucceeded?.Invoke(); + + // Update UI state after successful login + UpdateUIState(); + } + else + { + string failureMessage = "Login was cancelled or failed"; + Debug.LogWarning($"[PassportManager] {failureMessage}"); + OnLoginFailed?.Invoke(failureMessage); + + // Update UI state after failed login + UpdateUIState(); + } + } + catch (Exception ex) + { + string errorMessage = $"Login failed: {ex.Message}"; + Debug.LogError($"[PassportManager] {errorMessage}"); + OnLoginFailed?.Invoke(errorMessage); + } + } + + /// + /// Quick access to logout functionality + /// + public async void Logout() + { + if (!IsInitialized || PassportInstance == null) + { + string errorMessage = "Passport not initialized"; + Debug.LogError($"[PassportManager] {errorMessage}"); + OnLogoutFailed?.Invoke(errorMessage); + return; + } + + try + { + await PassportInstance.Logout(); + IsLoggedIn = false; + Debug.Log("[PassportManager] Logout successful!"); + OnLogoutSucceeded?.Invoke(); + + // Update UI state after logout + UpdateUIState(); + } + catch (Exception ex) + { + string errorMessage = $"Logout failed: {ex.Message}"; + Debug.LogError($"[PassportManager] {errorMessage}"); + OnLogoutFailed?.Invoke(errorMessage); + } + } + + #region UI Integration + + /// + /// Configure UI elements if they have been assigned + /// + private void ConfigureUIElements() + { + // Set up button listeners + if (loginButton != null) + { + loginButton.onClick.AddListener(() => Login()); + loginButton.interactable = IsInitialized && !IsLoggedIn; + } + + if (googleLoginButton != null) + { + googleLoginButton.onClick.AddListener(() => Login(DirectLoginMethod.Google)); + googleLoginButton.interactable = IsInitialized && !IsLoggedIn; + } + + if (appleLoginButton != null) + { + appleLoginButton.onClick.AddListener(() => Login(DirectLoginMethod.Apple)); + appleLoginButton.interactable = IsInitialized && !IsLoggedIn; + } + + if (facebookLoginButton != null) + { + facebookLoginButton.onClick.AddListener(() => Login(DirectLoginMethod.Facebook)); + facebookLoginButton.interactable = IsInitialized && !IsLoggedIn; + } + + if (logoutButton != null) + { + logoutButton.onClick.AddListener(() => Logout()); + logoutButton.interactable = IsInitialized && IsLoggedIn; + } + + // Update initial UI state + UpdateUIState(); + } + + /// + /// Update the state of UI elements based on current authentication status + /// + private void UpdateUIState() + { + bool isInitialized = IsInitialized; + bool isLoggedIn = IsLoggedIn; + + // Update button states + if (loginButton != null) + loginButton.interactable = isInitialized && !isLoggedIn; + if (googleLoginButton != null) + googleLoginButton.interactable = isInitialized && !isLoggedIn; + if (appleLoginButton != null) + appleLoginButton.interactable = isInitialized && !isLoggedIn; + if (facebookLoginButton != null) + facebookLoginButton.interactable = isInitialized && !isLoggedIn; + if (logoutButton != null) + logoutButton.interactable = isInitialized && isLoggedIn; + + // Update status text (supports both Legacy Text and TextMeshPro) + string statusMessage; + Color statusColor; + + if (!isInitialized) + { + statusMessage = "Initializing Passport..."; + statusColor = Color.yellow; + } + else if (isLoggedIn) + { + statusMessage = "[LOGGED IN] Logged In"; + statusColor = Color.green; + } + else + { + statusMessage = "Ready to login"; + statusColor = Color.white; + } + + SetStatusText(statusMessage, statusColor); + + // Update user info (supports both Legacy Text and TextMeshPro) + if (isLoggedIn) + { + UpdateUserInfoDisplay(); + } + else + { + SetUserInfoText(""); + } + } + + /// + /// Update the user info display with current user data + /// + private async void UpdateUserInfoDisplay() + { + if ((userInfoText != null || userInfoTextTMP != null) && PassportInstance != null) + { + try + { + string accessToken = await PassportInstance.GetAccessToken(); + string tokenPreview = accessToken.Length > 20 ? accessToken.Substring(0, 20) + "..." : accessToken; + string userInfo = $"Logged in (Token: {tokenPreview})"; + SetUserInfoText(userInfo); + } + catch (Exception ex) + { + string errorMessage = $"Error loading user info: {ex.Message}"; + SetUserInfoText(errorMessage); + Debug.LogWarning($"[PassportManager] Failed to load user info: {ex.Message}"); + } + } + } + + /// + /// Set status text on both Legacy Text and TextMeshPro components + /// + private void SetStatusText(string message, Color color) + { + if (statusText != null) + { + statusText.text = message; + statusText.color = color; + } + + if (statusTextTMP != null) + { + statusTextTMP.text = message; + statusTextTMP.color = color; + } + } + + /// + /// Set user info text on both Legacy Text and TextMeshPro components + /// + private void SetUserInfoText(string message) + { + if (userInfoText != null) + { + userInfoText.text = message; + } + + if (userInfoTextTMP != null) + { + userInfoTextTMP.text = message; + } + } + + #endregion + } +} \ No newline at end of file diff --git a/src/Packages/Passport/Runtime/Scripts/Public/PassportManager.cs.meta b/src/Packages/Passport/Runtime/Scripts/Public/PassportManager.cs.meta new file mode 100644 index 00000000..5989b8c9 --- /dev/null +++ b/src/Packages/Passport/Runtime/Scripts/Public/PassportManager.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 249e79783e53ea54185c0766985a05ad +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: From 8b3fc3b6083ba9f240e9922cdd1243f3aa2e6127 Mon Sep 17 00:00:00 2001 From: Nik Ho Date: Tue, 5 Aug 2025 20:48:33 +1200 Subject: [PATCH 02/31] feat(passport): add prefab to package samples --- .../PassportManager.prefab | 53 +++++++++++++++++++ .../PassportManager.prefab.meta | 7 +++ src/Packages/Passport/package.json | 12 ++++- 3 files changed, 70 insertions(+), 2 deletions(-) create mode 100644 src/Packages/Passport/Samples~/PassportManagerPrefab/PassportManager.prefab create mode 100644 src/Packages/Passport/Samples~/PassportManagerPrefab/PassportManager.prefab.meta diff --git a/src/Packages/Passport/Samples~/PassportManagerPrefab/PassportManager.prefab b/src/Packages/Passport/Samples~/PassportManagerPrefab/PassportManager.prefab new file mode 100644 index 00000000..9cb1bfad --- /dev/null +++ b/src/Packages/Passport/Samples~/PassportManagerPrefab/PassportManager.prefab @@ -0,0 +1,53 @@ +%YAML 1.1 +%TAG !u! tag:unity3d.com,2011: +--- !u!1 &2471608869895950180 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 2471608869895950181} + - component: {fileID: 2471608869895950182} + m_Layer: 0 + m_Name: PassportManager + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!4 &2471608869895950181 +Transform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 2471608869895950180} + m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} + m_LocalPosition: {x: 0, y: 0, z: 0} + m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 + m_Children: [] + m_Father: {fileID: 0} + m_RootOrder: 0 + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} +--- !u!114 &2471608869895950182 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 2471608869895950180} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 249e79783e53ea54185c0766985a05ad, type: 3} + m_Name: + m_EditorClassIdentifier: + clientId: your-client-id-here + environment: SANDBOX + redirectUri: + logoutRedirectUri: + autoInitialize: 1 + autoLogin: 1 + directLoginMethod: 0 \ No newline at end of file diff --git a/src/Packages/Passport/Samples~/PassportManagerPrefab/PassportManager.prefab.meta b/src/Packages/Passport/Samples~/PassportManagerPrefab/PassportManager.prefab.meta new file mode 100644 index 00000000..106c7685 --- /dev/null +++ b/src/Packages/Passport/Samples~/PassportManagerPrefab/PassportManager.prefab.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: 1234567890abcdef1234567890abcdef +PrefabImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: \ No newline at end of file diff --git a/src/Packages/Passport/package.json b/src/Packages/Passport/package.json index 662805c2..624a0015 100644 --- a/src/Packages/Passport/package.json +++ b/src/Packages/Passport/package.json @@ -18,7 +18,10 @@ ], "dependencies": { "com.unity.nuget.newtonsoft-json": "3.2.0", - "com.cysharp.unitask": "2.3.3" + "com.cysharp.unitask": "2.3.1", + "com.unity.modules.unitywebrequest": "1.0.0", + "com.unity.modules.jsonserialize": "1.0.0", + "com.unity.modules.androidjni": "1.0.0" }, "keywords": [ "unity", @@ -27,9 +30,14 @@ ], "unity": "2021.3", "samples": [ + { + "displayName": "PassportManager Prefab", + "description": "Drag-and-drop prefab for easy Passport integration with example scripts and documentation.", + "path": "Samples~/PassportManagerPrefab" + }, { "displayName": "Sample Scenes and Scripts", - "description": "Includes sample scenes and scripts for the Immutable Unity SDK.", + "description": "Complete sample scenes and scripts demonstrating Passport SDK features.", "path": "Samples~/SamplesScenesScripts" } ] From 20df6445ed574bf98f0e90613d9d364e73ee353e Mon Sep 17 00:00:00 2001 From: Nik Ho Date: Tue, 5 Aug 2025 20:49:00 +1200 Subject: [PATCH 03/31] feat(passport): add ui controller example --- .../PassportUIController.cs | 405 ++++++++++++++++++ 1 file changed, 405 insertions(+) create mode 100644 src/Packages/Passport/Samples~/PassportManagerPrefab/PassportUIController.cs diff --git a/src/Packages/Passport/Samples~/PassportManagerPrefab/PassportUIController.cs b/src/Packages/Passport/Samples~/PassportManagerPrefab/PassportUIController.cs new file mode 100644 index 00000000..aa664797 --- /dev/null +++ b/src/Packages/Passport/Samples~/PassportManagerPrefab/PassportUIController.cs @@ -0,0 +1,405 @@ +using UnityEngine; +using UnityEngine.UI; +using TMPro; +using Immutable.Passport; + +/// +/// UI Controller that can be wired up to PassportManager events through the Inspector. +/// This demonstrates the Inspector-based event handling approach. +/// +/// ⚠️ IMPORTANT: This controller uses aggressive cursor management for demo purposes. +/// It continuously unlocks the cursor to ensure UI remains clickable. For production games, +/// review and customize the cursor behavior to match your game's requirements. +/// +public class PassportUIController : MonoBehaviour +{ + [Header("UI Elements")] + [Tooltip("Button to trigger login")] + public Button loginButton; + + [Tooltip("Button to trigger logout")] + public Button logoutButton; + + [Header("Text Components (use Legacy Text OR TextMeshPro)")] + [Tooltip("Legacy Text component to show current status")] + public Text statusText; + + [Tooltip("TextMeshPro component to show current status (alternative to Legacy Text)")] + public TextMeshProUGUI statusTextTMP; + + [Tooltip("Legacy Text component to show user information")] + public Text userInfoText; + + [Tooltip("TextMeshPro component to show user information (alternative to Legacy Text)")] + public TextMeshProUGUI userInfoTextTMP; + + [Header("Settings")] + [Tooltip("Automatically manage button states based on authentication")] + public bool autoManageButtons = true; + + [Tooltip("⚠️ DEMO FEATURE: Aggressively keeps cursor unlocked for demo purposes. Disable for production games that need cursor control.")] + public bool forceCursorAlwaysAvailable = true; + + [Tooltip("Show detailed debug logs (enable for troubleshooting)")] + public bool debugLogging = false; + + void Start() + { + // Set up button listeners + if (loginButton != null) + { + loginButton.onClick.AddListener(OnLoginButtonClicked); + } + + if (logoutButton != null) + { + logoutButton.onClick.AddListener(OnLogoutButtonClicked); + } + + // Initial UI update + UpdateUIState(); + + // Start continuous cursor management if enabled + if (forceCursorAlwaysAvailable) + { + InvokeRepeating(nameof(EnsureCursorAvailable), 1.0f, 1.0f); + } + + if (debugLogging) + { + Debug.Log("[PassportUIController] Started and ready"); + } + + FixCursorState(); + } + + void OnDestroy() + { + // Cancel continuous cursor management + CancelInvoke(nameof(EnsureCursorAvailable)); + } + + #region Button Handlers + + void OnLoginButtonClicked() + { + if (debugLogging) + { + Debug.Log("[PassportUIController] Login button clicked"); + } + + var manager = PassportManager.Instance; + if (manager != null && manager.IsInitialized) + { + SetStatus("Logging in...", Color.yellow); + Debug.Log("[PassportUIController] Calling PassportManager.Login()..."); + Debug.Log($"[PassportUIController] Manager state - IsLoggedIn: {manager.IsLoggedIn}, PassportInstance: {(manager.PassportInstance != null ? "Valid" : "NULL")}"); + manager.Login(); + } + else + { + SetStatus("[ERROR] Passport not ready", Color.red); + Debug.Log($"[PassportUIController] Cannot login - Manager: {(manager != null ? "Found" : "NULL")}, Initialized: {(manager?.IsInitialized ?? false)}"); + } + } + + void OnLogoutButtonClicked() + { + if (debugLogging) + { + Debug.Log("[PassportUIController] Logout button clicked"); + } + + var manager = PassportManager.Instance; + if (manager != null && manager.IsInitialized) + { + SetStatus("Logging out...", Color.yellow); + Debug.Log("[PassportUIController] Calling PassportManager.Logout()..."); + manager.Logout(); + } + else + { + SetStatus("[ERROR] Passport not ready", Color.red); + Debug.Log($"[PassportUIController] Cannot logout - Manager: {(manager != null ? "Found" : "NULL")}, Initialized: {(manager?.IsInitialized ?? false)}"); + } + } + + #endregion + + #region Event Handlers (Wire these up in PassportManager Inspector!) + + /// + /// Call this from PassportManager's OnPassportInitialized event + /// + public void OnPassportInitialized() + { + if (debugLogging) + { + Debug.Log("[PassportUIController] 🎉 Passport initialized!"); + } + + SetStatus("[READY] Passport ready", Color.green); + UpdateUIState(); + + // Fix cursor state after initialization in case it was left in a bad state + FixCursorState(); + } + + /// + /// Call this from PassportManager's OnPassportError event + /// + public void OnPassportError(string error) + { + if (debugLogging) + { + Debug.LogError($"[PassportUIController] ❌ Passport error: {error}"); + } + + SetStatus($"[ERROR] {error}", Color.red); + UpdateUIState(); + } + + /// + /// Call this from PassportManager's OnLoginSucceeded event + /// + public void OnLoginSucceeded() + { + if (debugLogging) + { + Debug.Log("[PassportUIController] 🎉 Login successful!"); + } + + SetStatus("[SUCCESS] Logged in successfully!", Color.green); + UpdateUIState(); + LoadUserInfo(); + + // Fix cursor issues after authentication + FixCursorState(); + } + + /// + /// Call this from PassportManager's OnLoginFailed event + /// + public void OnLoginFailed(string error) + { + if (debugLogging) + { + Debug.LogWarning($"[PassportUIController] ⚠️ Login failed: {error}"); + } + + SetStatus($"[FAILED] Login failed: {error}", Color.red); + UpdateUIState(); + + // Fix cursor issues after failed authentication + FixCursorState(); + } + + /// + /// Call this from PassportManager's OnLogoutSucceeded event + /// + public void OnLogoutSucceeded() + { + if (debugLogging) + { + Debug.Log("[PassportUIController] 👋 Logout successful!"); + } + + SetStatus("[LOGGED OUT] Logged out", Color.blue); + UpdateUIState(); + ClearUserInfo(); + } + + /// + /// Call this from PassportManager's OnLogoutFailed event + /// + public void OnLogoutFailed(string error) + { + if (debugLogging) + { + Debug.LogError($"[PassportUIController] ❌ Logout failed: {error}"); + } + + SetStatus($"[ERROR] Logout failed: {error}", Color.red); + UpdateUIState(); + } + + #endregion + + #region UI Management + + void UpdateUIState() + { + if (!autoManageButtons) return; + + var manager = PassportManager.Instance; + bool isInitialized = manager != null && manager.IsInitialized; + bool isLoggedIn = manager != null && manager.IsLoggedIn; + + if (loginButton != null) + { + loginButton.interactable = isInitialized && !isLoggedIn; + if (debugLogging) + { + Debug.Log($"[PassportUIController] Login button: interactable={loginButton.interactable}"); + } + } + + if (logoutButton != null) + { + logoutButton.interactable = isInitialized && isLoggedIn; + if (debugLogging) + { + Debug.Log($"[PassportUIController] Logout button: interactable={logoutButton.interactable}"); + } + } + } + + void SetStatus(string message, Color color) + { + // Update Legacy Text + if (statusText != null) + { + statusText.text = message; + statusText.color = color; + } + + // Update TextMeshPro + if (statusTextTMP != null) + { + statusTextTMP.text = message; + statusTextTMP.color = color; + } + + if (debugLogging) + { + Debug.Log($"[PassportUIController] Status: {message}"); + } + } + + async void LoadUserInfo() + { + var manager = PassportManager.Instance; + if (manager?.PassportInstance == null || (userInfoText == null && userInfoTextTMP == null)) + return; + + try + { + string accessToken = await manager.PassportInstance.GetAccessToken(); + string tokenPreview = accessToken.Length > 20 ? accessToken.Substring(0, 20) + "..." : accessToken; + string userInfo = $"Logged in (Token: {tokenPreview})"; + SetUserInfo(userInfo); + + if (debugLogging) + { + Debug.Log($"[PassportUIController] User info loaded with access token"); + } + } + catch (System.Exception ex) + { + string errorInfo = $"Error loading user info: {ex.Message}"; + SetUserInfo(errorInfo); + Debug.LogError($"[PassportUIController] Failed to load user info: {ex.Message}"); + } + } + + void ClearUserInfo() + { + SetUserInfo(""); + } + + void SetUserInfo(string message) + { + // Update Legacy Text + if (userInfoText != null) + { + userInfoText.text = message; + } + + // Update TextMeshPro + if (userInfoTextTMP != null) + { + userInfoTextTMP.text = message; + } + } + + void FixCursorState() + { + // Fix common cursor issues after authentication + try + { + Cursor.lockState = CursorLockMode.None; + Cursor.visible = true; + + if (debugLogging) + { + Debug.Log("[PassportUIController] Cursor state restored"); + } + } + catch (System.Exception ex) + { + Debug.LogWarning($"[PassportUIController] Could not fix cursor state: {ex.Message}"); + } + } + + void EnsureCursorAvailable() + { + // ⚠️ AGGRESSIVE CURSOR MANAGEMENT FOR DEMO PURPOSES + // This method forcibly unlocks the cursor every second to ensure demo UI remains clickable. + // For production games: Disable 'forceCursorAlwaysAvailable' and implement custom cursor logic. + try + { + if (Cursor.lockState != CursorLockMode.None || !Cursor.visible) + { + Cursor.lockState = CursorLockMode.None; + Cursor.visible = true; + + if (debugLogging) + { + Debug.Log("[PassportUIController] Cursor state corrected (was locked or invisible)"); + } + } + } + catch (System.Exception ex) + { + Debug.LogWarning($"[PassportUIController] Could not ensure cursor availability: {ex.Message}"); + } + } + + #endregion + + #region Debug Helpers + + // Debug panel (only shows when debug logging is enabled) + void OnGUI() + { + if (!debugLogging) return; + + GUILayout.BeginArea(new Rect(Screen.width - 220, 10, 200, 150)); + GUILayout.Box("Passport Debug Panel"); + + var manager = PassportManager.Instance; + if (manager != null) + { + GUILayout.Label($"Initialized: {manager.IsInitialized}"); + GUILayout.Label($"Logged In: {manager.IsLoggedIn}"); + + if (GUILayout.Button("Test Login")) + { + OnLoginButtonClicked(); + } + + if (GUILayout.Button("Test Logout")) + { + OnLogoutButtonClicked(); + } + } + else + { + GUILayout.Label("PassportManager: NULL"); + } + + GUILayout.EndArea(); + } + + #endregion +} \ No newline at end of file From b43ecd768e9ce783ea03c09fca8661635f1715e6 Mon Sep 17 00:00:00 2001 From: Nik Ho Date: Tue, 5 Aug 2025 20:58:40 +1200 Subject: [PATCH 04/31] docs(passport): add passport manager samples readme --- .../Samples~/PassportManagerPrefab/README.md | 229 ++++++++++++++++++ 1 file changed, 229 insertions(+) create mode 100644 src/Packages/Passport/Samples~/PassportManagerPrefab/README.md diff --git a/src/Packages/Passport/Samples~/PassportManagerPrefab/README.md b/src/Packages/Passport/Samples~/PassportManagerPrefab/README.md new file mode 100644 index 00000000..9e76585f --- /dev/null +++ b/src/Packages/Passport/Samples~/PassportManagerPrefab/README.md @@ -0,0 +1,229 @@ +# PassportManager Prefab + +The **PassportManager** is a drag-and-drop prefab that provides an easy way to integrate Immutable Passport authentication into your Unity game. + +## 🚀 Quick Start + +### Option 1: Just Authentication (Code-based) + +1. **Import the Sample**: In Unity Package Manager, find "Immutable Passport" and import the "PassportManager Prefab" sample +2. **Drag the Prefab**: Drag `PassportManager.prefab` from the Samples folder into your scene +3. **Configure Settings**: In the Inspector, set your Client ID and redirect URIs +4. **Use Events or Code**: Subscribe to events or call methods directly +5. **Test**: Hit Play - the prefab will automatically initialize Passport! + +### Option 2: Complete UI Integration (No Code Required!) + +1. **Import the Sample**: Same as above +2. **Drag the Prefab**: Same as above +3. **Configure Settings**: Same as above +4. **Add PassportUIController**: Add the `PassportUIController` script to a GameObject and assign your UI elements (supports both Legacy Text and TextMeshPro) +5. **Wire Events**: In PassportManager Inspector, connect the events to PassportUIController methods +6. **⚠️ Production Note**: Review cursor management settings for your game type +7. **Test**: Hit Play - full authentication flow with automatic UI management! + +## ⚙️ Configuration + +### Required Settings + +- **Client ID**: Your Immutable Passport client ID +- **Redirect URI**: Authentication callback URL (e.g., `mygame://callback`) +- **Logout Redirect URI**: Logout callback URL (e.g., `mygame://logout`) + +### Optional Settings + +- **Environment**: `SANDBOX` (default) or `PRODUCTION` +- **Auto Initialize**: Automatically initialize on Start (default: true) +- **Auto Login**: Automatically attempt login after initialization (default: false) +- **Direct Login Method**: Pre-select login method (None, Google, Apple, Facebook) +- **Log Level**: Control debug output verbosity + +### UI Integration (Optional - No Code Required!) + +Simply drag UI elements from your scene into these fields and they'll be automatically configured: + +- **Login Button**: Triggers default login method +- **Google Login Button**: Triggers Google-specific login +- **Apple Login Button**: Triggers Apple-specific login +- **Facebook Login Button**: Triggers Facebook-specific login +- **Logout Button**: Triggers logout +- **Status Text**: Shows current authentication status (Legacy Text OR TextMeshPro) +- **User Info Text**: Shows logged-in user's access token preview (Legacy Text OR TextMeshPro) + +**✨ Magic**: Buttons are automatically enabled/disabled based on authentication state! + +## 🎯 No-Code Setup Example + +Want authentication with zero scripting? Here's how: + +1. **Create UI**: Add Canvas → Button (Login), Button (Logout), Text (Status) +2. **Drag Components**: In PassportUIController Inspector, drag the **COMPONENTS** (not GameObjects) to the UI fields: + - Expand the GameObject in hierarchy + - Drag the **Button** component (not the GameObject) + - For text: Drag **Text** component OR **TextMeshPro - Text (UI)** component (not the GameObject) + - PassportUIController supports both text types - use whichever your project uses +3. **Configure**: Set your Client ID and redirect URIs +4. **Done!**: Hit Play - your buttons now handle authentication automatically + +That's it! No scripts, no event handling, no code. Pure drag-and-drop! 🚀 + +## 🎮 Using Events + +The prefab exposes Unity Events that you can wire up in the Inspector: + +### Initialization Events + +- `OnPassportInitialized`: Fired when Passport is ready +- `OnPassportError`: Fired if initialization fails + +### Authentication Events + +- `OnLoginSucceeded`: User successfully logged in +- `OnLoginFailed`: Login failed or was cancelled +- `OnLogoutSucceeded`: User successfully logged out +- `OnLogoutFailed`: Logout failed + +## 💻 Using from Code + +You can also interact with the PassportManager from your scripts: + +```csharp +// Access the singleton instance +var manager = PassportManager.Instance; + +// Check states +if (manager.IsInitialized && manager.IsLoggedIn) +{ + // Access the underlying Passport instance + var email = await manager.PassportInstance.GetEmail(); + var accessToken = await manager.PassportInstance.GetAccessToken(); + + // Note: GetAddress() requires IMX connection via ConnectImx() + // var address = await manager.PassportInstance.GetAddress(); +} + +// Manual login with specific method +manager.Login(DirectLoginMethod.Google); + +// Manual logout +manager.Logout(); +``` + +## 📁 Sample Scripts + +Check out the included example script: + +- `PassportUIController.cs`: Complete no-code UI integration (recommended) + +## ⚠️ Important: Cursor Management for Production + +The `PassportUIController` uses **aggressive cursor management** designed for demos and prototypes. It continuously unlocks the cursor to ensure UI remains clickable. + +### For Production Games: + +1. **Disable `forceCursorAlwaysAvailable`** in PassportUIController Inspector +2. **Implement custom cursor logic** based on your game's needs: + - **FPS games**: May want `Cursor.lockState = CursorLockMode.Locked` during gameplay + - **RTS games**: May want `Cursor.lockState = CursorLockMode.None` always + - **Menu-driven games**: May want cursor unlocked for UI, locked for gameplay +3. **Handle cursor state** in the authentication event handlers: + + ```csharp + void OnLoginSucceeded() { + // Your game-specific cursor behavior + Cursor.lockState = YourGame.GetDesiredCursorMode(); + } + ``` + +## 🔗 Deep Linking Setup + +For native platforms (Windows/Mac), you'll need to register your custom URL scheme: + +### Windows + +The SDK automatically handles Windows deep linking registration. + +## 🆘 Troubleshooting + +### "Can't drag UI elements to Inspector fields" + +**Issue**: You're dragging the GameObject instead of the component, or using wrong text type. + +**Solution**: + +1. In the Hierarchy, click the **arrow** next to your GameObject to expand it +2. For buttons: Drag the **Button** component (with the icon), NOT the GameObject name +3. For text components: Drag either: + - **Text** component (Legacy UI) → to "Status Text" or "User Info Text" fields + - **TextMeshPro - Text (UI)** component → to "Status Text TMP" or "User Info Text TMP" fields +4. The field should highlight blue when you can drop it + +### "Redirect URI must be configured" + +Make sure you've set both redirect URIs in the Inspector. + +### "Client ID not set" + +Enter your Immutable Passport client ID in the Inspector. + +### "Error loading user info: No IMX provider" + +**Issue**: This occurs when trying to access IMX-specific features (like wallet address) without connecting to IMX first. + +**Solution**: The PassportManager now uses access token instead of wallet address for user info display. If you need wallet addresses, call `ConnectImx()` first: + +```csharp +await PassportManager.Instance.PassportInstance.ConnectImx(); +var address = await PassportManager.Instance.PassportInstance.GetAddress(); +``` + +### Deep linking issues on Windows + +- Run Unity as Administrator if you get permission errors +- Check Windows Firewall settings +- Ensure no other applications are using the same URL scheme + +### "UI buttons don't work" + +- Make sure you dragged the **Button component**, not the GameObject +- Check that the PassportManager prefab is in the scene (not just in Project window) +- Verify the buttons are actually assigned in the PassportManager or PassportUIController Inspector +- If using PassportUIController, ensure the script is enabled on the GameObject + +### "Mouse clicks don't register after authentication" + +**Issue**: The authentication webview can interfere with Unity's input system. +**Solution**: Handle cursor state in your game code: + +```csharp +void Start() { + PassportManager.Instance.OnLoginSucceeded.AddListener(RestoreInput); + PassportManager.Instance.OnLoginFailed.AddListener(RestoreInput); +} + +void RestoreInput() { + // For FPS games: + Cursor.lockState = CursorLockMode.Locked; + + // For menu-driven games: + Cursor.lockState = CursorLockMode.None; + + // Ensure cursor is visible for UI + Cursor.visible = true; +} +``` + +### "Cursor behavior conflicts with my game" + +**Issue**: PassportUIController's aggressive cursor management interferes with game controls. + +**Solution**: + +1. **Disable** `Force Cursor Always Available` in PassportUIController Inspector +2. **Implement** your own cursor management in the authentication event handlers +3. **Consider** using PassportManager events directly instead of PassportUIController for full control + +## 📚 Learn More + +- [Immutable Passport Documentation](https://docs.immutable.com/build/unity/) +- [Unity Quickstart Guide](https://docs.immutable.com/build/unity/quickstart) From d8d02bea99c9fb9d552e8e59d4b16f9dc540f06f Mon Sep 17 00:00:00 2001 From: Nik Ho Date: Tue, 5 Aug 2025 21:16:19 +1200 Subject: [PATCH 05/31] docs(passport): lint fix prefab samples readme --- .../Passport/Samples~/PassportManagerPrefab/README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Packages/Passport/Samples~/PassportManagerPrefab/README.md b/src/Packages/Passport/Samples~/PassportManagerPrefab/README.md index 9e76585f..46d05f4f 100644 --- a/src/Packages/Passport/Samples~/PassportManagerPrefab/README.md +++ b/src/Packages/Passport/Samples~/PassportManagerPrefab/README.md @@ -59,7 +59,7 @@ Want authentication with zero scripting? Here's how: 1. **Create UI**: Add Canvas → Button (Login), Button (Logout), Text (Status) 2. **Drag Components**: In PassportUIController Inspector, drag the **COMPONENTS** (not GameObjects) to the UI fields: - Expand the GameObject in hierarchy - - Drag the **Button** component (not the GameObject) + - Drag the **Button** component (not the GameObject) - For text: Drag **Text** component OR **TextMeshPro - Text (UI)** component (not the GameObject) - PassportUIController supports both text types - use whichever your project uses 3. **Configure**: Set your Client ID and redirect URIs @@ -119,7 +119,7 @@ Check out the included example script: The `PassportUIController` uses **aggressive cursor management** designed for demos and prototypes. It continuously unlocks the cursor to ensure UI remains clickable. -### For Production Games: +### For Production Games 1. **Disable `forceCursorAlwaysAvailable`** in PassportUIController Inspector 2. **Implement custom cursor logic** based on your game's needs: From a817e635eba6a739ea876c8029ba2eea6f39b76e Mon Sep 17 00:00:00 2001 From: Nik Ho Date: Tue, 5 Aug 2025 21:26:04 +1200 Subject: [PATCH 06/31] fix(passport): fix deeplinking log path --- .../Private/Helpers/WindowsDeepLink.cs | 41 ++++++++++++------- 1 file changed, 26 insertions(+), 15 deletions(-) diff --git a/src/Packages/Passport/Runtime/Scripts/Private/Helpers/WindowsDeepLink.cs b/src/Packages/Passport/Runtime/Scripts/Private/Helpers/WindowsDeepLink.cs index 15ce82af..a6e1861d 100644 --- a/src/Packages/Passport/Runtime/Scripts/Private/Helpers/WindowsDeepLink.cs +++ b/src/Packages/Passport/Runtime/Scripts/Private/Helpers/WindowsDeepLink.cs @@ -137,13 +137,15 @@ private static void CreateCommandScript(string protocolName) " powershell -NoProfile -ExecutionPolicy Bypass -Command ^", " \"$ErrorActionPreference = 'Continue';\" ^", " \"$wshell = New-Object -ComObject wscript.shell;\" ^", - " \"Add-Content -Path \\\"{logPath}\\\" -Value ('[' + (Get-Date) + '] Attempting to activate process ID: ' + %%A);\" ^", + $" \"Add-Content -Path \\\"{logPath}\\\" -Value ('[' + (Get-Date) + '] Attempting to activate process ID: ' + %%A);\" ^", " \"Start-Sleep -Milliseconds 100;\" ^", " \"$result = $wshell.AppActivate(%%A);\" ^", - " \"Add-Content -Path \\\"{logPath}\\\" -Value ('[' + (Get-Date) + '] AppActivate result: ' + $result);\" ^", - " \"if (-not $result) { Add-Content -Path \\\"{logPath}\\\" -Value ('[' + (Get-Date) + '] Failed to activate window') }\" ^", + $" \"Add-Content -Path \\\"{logPath}\\\" -Value ('[' + (Get-Date) + '] AppActivate result: ' + $result);\" ^", + $" \"if (-not $result) {{ Add-Content -Path \\\"{logPath}\\\" -Value ('[' + (Get-Date) + '] Failed to activate window') }}\" ^", " >nul 2>&1", " if errorlevel 1 echo [%date% %time%] PowerShell error: %errorlevel% >> \"%LOG_PATH%\"", + " echo [%date% %time%] Unity activated, self-deleting >> \"%LOG_PATH%\"", + $" del \"%~f0\" >nul 2>&1", " endlocal", " exit /b 0", " )", @@ -155,7 +157,11 @@ private static void CreateCommandScript(string protocolName) "", // Start new Unity instance if none found $"echo [%date% %time%] Starting new Unity instance >> \"%LOG_PATH%\"", - $"start \"\" \"{unityExe}\" -projectPath \"%PROJECT_PATH%\" >nul 2>&1" + $"start \"\" \"{unityExe}\" -projectPath \"%PROJECT_PATH%\" >nul 2>&1", + "", + // Self-delete the batch file when done + "echo [%date% %time%] Script completed, self-deleting >> \"%LOG_PATH%\"", + $"del \"%~f0\" >nul 2>&1" }; File.WriteAllLines(cmdPath, scriptLines); @@ -249,6 +255,7 @@ private static void RegisterProtocol(string protocolName) // Set command to launch the script with the URI parameter var scriptLocation = GetGameExecutablePath(".cmd"); + //string command = $"cmd.exe /c \"\"{scriptLocation}\" \"%1\"\""; string command = $"\"{scriptLocation}\" \"%1\""; uint commandSize = (uint)((command.Length + 1) * 2); @@ -357,18 +364,22 @@ private void HandleDeeplink() } // Clean up command script + // Note: Batch file will self-delete, no need to delete here + // This prevents race condition where Unity deletes the file + // while Windows is still trying to execute it var cmdPath = GetGameExecutablePath(".cmd"); - if (File.Exists(cmdPath)) - { - try - { - File.Delete(cmdPath); - } - catch (Exception ex) - { - PassportLogger.Warn($"Failed to delete script: {ex.Message}"); - } - } + // Commented out to prevent race condition + // if (File.Exists(cmdPath)) + // { + // try + // { + // File.Delete(cmdPath); + // } + // catch (Exception ex) + // { + // PassportLogger.Warn($"Failed to delete script: {ex.Message}"); + // } + // } // Clean up instance Destroy(gameObject); From a49ace55aab1dfd84d8550eb1c920c51c2301f0d Mon Sep 17 00:00:00 2001 From: Nik Ho Date: Sat, 9 Aug 2025 03:16:21 +1200 Subject: [PATCH 07/31] feat: add prefab with ui elements baked in --- .../Runtime/Scripts/Public/PassportManager.cs | 51 ++- .../Scripts/Public/PassportUIBuilder.cs | 414 ++++++++++++++++++ .../Scripts/Public/PassportUIBuilder.cs.meta | 11 + .../PassportManagerComplete.prefab | 110 +++++ .../PassportManagerComplete.prefab.meta | 7 + 5 files changed, 592 insertions(+), 1 deletion(-) create mode 100644 src/Packages/Passport/Runtime/Scripts/Public/PassportUIBuilder.cs create mode 100644 src/Packages/Passport/Runtime/Scripts/Public/PassportUIBuilder.cs.meta create mode 100644 src/Packages/Passport/Samples~/PassportManagerPrefab/PassportManagerComplete.prefab create mode 100644 src/Packages/Passport/Samples~/PassportManagerPrefab/PassportManagerComplete.prefab.meta diff --git a/src/Packages/Passport/Runtime/Scripts/Public/PassportManager.cs b/src/Packages/Passport/Runtime/Scripts/Public/PassportManager.cs index aa420cb4..fde3880a 100644 --- a/src/Packages/Passport/Runtime/Scripts/Public/PassportManager.cs +++ b/src/Packages/Passport/Runtime/Scripts/Public/PassportManager.cs @@ -77,6 +77,9 @@ public class PassportManager : MonoBehaviour public bool IsInitialized { get; private set; } public bool IsLoggedIn { get; private set; } + // UI Builder integration + private PassportUIBuilder uiBuilder; + private void Awake() { // Singleton pattern @@ -94,6 +97,9 @@ private void Awake() private void Start() { + // Find UI Builder if present + uiBuilder = GetComponent(); + // Configure UI elements if provided ConfigureUIElements(); @@ -237,6 +243,12 @@ private async UniTask LoginAsync(DirectLoginMethod? loginMethod = null) // Update UI state after successful login UpdateUIState(); + + // Switch to logged-in panel if UI builder is present + if (uiBuilder != null) + { + uiBuilder.ShowLoggedInPanel(); + } } else { @@ -278,6 +290,12 @@ public async void Logout() // Update UI state after logout UpdateUIState(); + + // Switch back to login panel if UI builder is present + if (uiBuilder != null) + { + uiBuilder.ShowLoginPanel(); + } } catch (Exception ex) { @@ -294,35 +312,45 @@ public async void Logout() /// private void ConfigureUIElements() { - // Set up button listeners + // Set up button listeners (clear existing first to prevent duplicates) if (loginButton != null) { + loginButton.onClick.RemoveAllListeners(); loginButton.onClick.AddListener(() => Login()); loginButton.interactable = IsInitialized && !IsLoggedIn; + Debug.Log("[PassportManager] Configured login button"); } if (googleLoginButton != null) { + googleLoginButton.onClick.RemoveAllListeners(); googleLoginButton.onClick.AddListener(() => Login(DirectLoginMethod.Google)); googleLoginButton.interactable = IsInitialized && !IsLoggedIn; + Debug.Log("[PassportManager] Configured Google login button"); } if (appleLoginButton != null) { + appleLoginButton.onClick.RemoveAllListeners(); appleLoginButton.onClick.AddListener(() => Login(DirectLoginMethod.Apple)); appleLoginButton.interactable = IsInitialized && !IsLoggedIn; + Debug.Log("[PassportManager] Configured Apple login button"); } if (facebookLoginButton != null) { + facebookLoginButton.onClick.RemoveAllListeners(); facebookLoginButton.onClick.AddListener(() => Login(DirectLoginMethod.Facebook)); facebookLoginButton.interactable = IsInitialized && !IsLoggedIn; + Debug.Log("[PassportManager] Configured Facebook login button"); } if (logoutButton != null) { + logoutButton.onClick.RemoveAllListeners(); logoutButton.onClick.AddListener(() => Logout()); logoutButton.interactable = IsInitialized && IsLoggedIn; + Debug.Log("[PassportManager] Configured logout button"); } // Update initial UI state @@ -440,5 +468,26 @@ private void SetUserInfoText(string message) } #endregion + + #region UI Builder Integration + + /// + /// Set UI references from the UI Builder (used internally) + /// + public void SetUIReferences(Button login, Button google, Button apple, Button facebook, Button logout, Text status, Text userInfo) + { + loginButton = login; + googleLoginButton = google; + appleLoginButton = apple; + facebookLoginButton = facebook; + logoutButton = logout; + statusText = status; + userInfoText = userInfo; + + // Re-configure UI elements with new references + ConfigureUIElements(); + } + + #endregion } } \ No newline at end of file diff --git a/src/Packages/Passport/Runtime/Scripts/Public/PassportUIBuilder.cs b/src/Packages/Passport/Runtime/Scripts/Public/PassportUIBuilder.cs new file mode 100644 index 00000000..be9261ba --- /dev/null +++ b/src/Packages/Passport/Runtime/Scripts/Public/PassportUIBuilder.cs @@ -0,0 +1,414 @@ +using UnityEngine; +using UnityEngine.UI; +using UnityEngine.EventSystems; +using TMPro; + +namespace Immutable.Passport +{ + /// + /// Builds a complete UI for PassportManager at runtime. + /// Creates a mobile-first, simple UI that developers can easily customize. + /// + public class PassportUIBuilder : MonoBehaviour + { + [Header("UI Configuration")] + [SerializeField] [Tooltip("The PassportManager to connect this UI to")] + private PassportManager passportManager; + + [Header("Layout Settings")] + [SerializeField] private int canvasOrder = 100; + [SerializeField] private Vector2 panelSize = new Vector2(300, 400); + [SerializeField] private Vector2 buttonSize = new Vector2(280, 45); + [SerializeField] private float elementSpacing = 10f; + + [Header("Cursor Management (Demo Feature)")] + [SerializeField] [Tooltip("WARNING: Aggressive cursor management for demo purposes. May conflict with game cursor logic.")] + private bool forceCursorAlwaysAvailable = true; + + [Header("Generated UI References (Auto-populated)")] + [SerializeField] private Canvas uiCanvas; + [SerializeField] private GameObject loginPanel; + [SerializeField] private GameObject loggedInPanel; + [SerializeField] private Button loginButton; + [SerializeField] private Button googleLoginButton; + [SerializeField] private Button appleLoginButton; + [SerializeField] private Button facebookLoginButton; + [SerializeField] private Button logoutButton; + [SerializeField] private Text statusText; + [SerializeField] private Text userInfoText; + + private bool uiBuilt = false; + + private void Awake() + { + // Find PassportManager if not assigned + if (passportManager == null) + { + passportManager = FindObjectOfType(); + if (passportManager == null) + { + Debug.LogError("[PassportUIBuilder] No PassportManager found in scene. Please assign one in the Inspector."); + return; + } + } + + BuildUI(); + } + + /// + /// Build the complete UI hierarchy at runtime + /// + public void BuildUI() + { + if (uiBuilt) + { + Debug.LogWarning("[PassportUIBuilder] UI already built."); + return; + } + + Debug.Log("[PassportUIBuilder] Building Passport UI..."); + + // Clean up any existing UI first + if (uiCanvas != null) + { + Debug.Log("[PassportUIBuilder] Cleaning up existing UI..."); + DestroyImmediate(uiCanvas.gameObject); + uiCanvas = null; + } + + CreateCanvas(); + CreateLoginPanel(); + CreateLoggedInPanel(); + WireUpPassportManager(); + + // Start with login panel visible + ShowLoginPanel(); + + uiBuilt = true; + Debug.Log("[PassportUIBuilder] Passport UI built successfully!"); + + // Start cursor management if enabled + if (forceCursorAlwaysAvailable) + { + InvokeRepeating(nameof(EnsureCursorAvailable), 0.1f, 0.1f); + } + } + + /// + /// Ensure cursor is always available for UI interaction (demo feature) + /// WARNING: This is aggressive cursor management for demo purposes + /// + private void EnsureCursorAvailable() + { + if (forceCursorAlwaysAvailable) + { + Cursor.lockState = CursorLockMode.None; + Cursor.visible = true; + } + } + + /// + /// Create the main canvas + /// + private void CreateCanvas() + { + // Create Canvas GameObject + GameObject canvasObj = new GameObject("PassportUI"); + canvasObj.transform.SetParent(transform); + + // Add Canvas component + uiCanvas = canvasObj.AddComponent(); + uiCanvas.renderMode = RenderMode.ScreenSpaceOverlay; + uiCanvas.sortingOrder = canvasOrder; + + // Add CanvasScaler for responsive design + CanvasScaler scaler = canvasObj.AddComponent(); + scaler.uiScaleMode = CanvasScaler.ScaleMode.ScaleWithScreenSize; + scaler.referenceResolution = new Vector2(360, 640); // Mobile reference + scaler.screenMatchMode = CanvasScaler.ScreenMatchMode.MatchWidthOrHeight; + scaler.matchWidthOrHeight = 0.5f; + + // Add GraphicRaycaster for input + canvasObj.AddComponent(); + + // Ensure EventSystem exists + EnsureEventSystem(); + } + + /// + /// Create the login panel with all login buttons + /// + private void CreateLoginPanel() + { + // Create login panel + loginPanel = CreatePanel("LoginPanel", uiCanvas.transform); + + // Create title + CreateText("Login", loginPanel.transform, new Vector2(0, 150), 24, TextAnchor.UpperCenter); + + // Create status text + statusText = CreateText("Initializing Passport...", loginPanel.transform, new Vector2(0, 100), 16, TextAnchor.UpperCenter); + statusText.color = Color.yellow; + + // Create login buttons with proper spacing + float startY = 50f; + float spacing = buttonSize.y + elementSpacing; + + googleLoginButton = CreateButton("Google Login", "Continue with Google", loginPanel.transform, + new Vector2(0, startY - (spacing * 0)), Color.white, new Color(0.86f, 0.27f, 0.22f, 1f)); // Google Red + + appleLoginButton = CreateButton("Apple Login", "Continue with Apple", loginPanel.transform, + new Vector2(0, startY - (spacing * 1)), Color.white, Color.black); + + facebookLoginButton = CreateButton("Facebook Login", "Continue with Facebook", loginPanel.transform, + new Vector2(0, startY - (spacing * 2)), Color.white, new Color(0.26f, 0.40f, 0.70f, 1f)); // Facebook Blue + + loginButton = CreateButton("Default Login", "Login", loginPanel.transform, + new Vector2(0, startY - (spacing * 3)), Color.black, new Color(0.9f, 0.9f, 0.9f, 1f)); // Light gray for visibility + } + + /// + /// Create the logged-in panel with user info and logout + /// + private void CreateLoggedInPanel() + { + // Create logged-in panel + loggedInPanel = CreatePanel("LoggedInPanel", uiCanvas.transform); + + // Create welcome text + CreateText("Welcome!", loggedInPanel.transform, new Vector2(0, 100), 24, TextAnchor.UpperCenter); + + // Create user info text + userInfoText = CreateText("Logged in successfully", loggedInPanel.transform, new Vector2(0, 50), 14, TextAnchor.UpperCenter); + userInfoText.color = Color.green; + + // Create logout button + logoutButton = CreateButton("Logout", "Logout", loggedInPanel.transform, + new Vector2(0, -50), Color.white, new Color(0.8f, 0.3f, 0.3f, 1f)); // Red for logout + } + + /// + /// Create a panel GameObject with background + /// + private GameObject CreatePanel(string name, Transform parent) + { + GameObject panel = new GameObject(name); + panel.transform.SetParent(parent, false); + + // Add RectTransform + RectTransform rect = panel.AddComponent(); + rect.anchorMin = new Vector2(0.5f, 0.5f); + rect.anchorMax = new Vector2(0.5f, 0.5f); + rect.pivot = new Vector2(0.5f, 0.5f); + rect.anchoredPosition = Vector2.zero; + rect.sizeDelta = panelSize; + + // Add background image + Image bg = panel.AddComponent(); + bg.color = new Color(0.1f, 0.1f, 0.1f, 0.8f); // Semi-transparent dark background + + Debug.Log($"[PassportUIBuilder] Created panel: '{name}' with size: {panelSize}"); + + return panel; + } + + /// + /// Create a button with text and styling + /// + private Button CreateButton(string name, string text, Transform parent, Vector2 position, Color textColor, Color buttonColor) + { + GameObject buttonObj = new GameObject(name); + buttonObj.transform.SetParent(parent, false); + + // Add RectTransform + RectTransform rect = buttonObj.AddComponent(); + rect.anchorMin = new Vector2(0.5f, 0.5f); + rect.anchorMax = new Vector2(0.5f, 0.5f); + rect.pivot = new Vector2(0.5f, 0.5f); + rect.anchoredPosition = position; + rect.sizeDelta = buttonSize; + + // Add Image component for button background + Image image = buttonObj.AddComponent(); + image.color = buttonColor; + + // Add Button component + Button button = buttonObj.AddComponent