diff --git a/src/Packages/Passport/Runtime/Scripts/Private/Helpers/WindowsDeepLink.cs b/src/Packages/Passport/Runtime/Scripts/Private/Helpers/WindowsDeepLink.cs index 1a6c948f..372d31c4 100644 --- a/src/Packages/Passport/Runtime/Scripts/Private/Helpers/WindowsDeepLink.cs +++ b/src/Packages/Passport/Runtime/Scripts/Private/Helpers/WindowsDeepLink.cs @@ -141,13 +141,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", " )", @@ -159,7 +161,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); @@ -395,18 +401,10 @@ 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}"); - } - } // Clean up instance Destroy(gameObject); @@ -415,4 +413,3 @@ private void HandleDeeplink() } } #endif - diff --git a/src/Packages/Passport/Runtime/Scripts/Private/Model/DirectLoginMethod.cs b/src/Packages/Passport/Runtime/Scripts/Private/Model/DirectLoginMethod.cs index 5996371f..052064d7 100644 --- a/src/Packages/Passport/Runtime/Scripts/Private/Model/DirectLoginMethod.cs +++ b/src/Packages/Passport/Runtime/Scripts/Private/Model/DirectLoginMethod.cs @@ -8,6 +8,7 @@ namespace Immutable.Passport.Model [Serializable] public enum DirectLoginMethod { + None, Email, Google, Apple, diff --git a/src/Packages/Passport/Runtime/Scripts/Private/Model/DirectLoginOptions.cs b/src/Packages/Passport/Runtime/Scripts/Private/Model/DirectLoginOptions.cs index fbf157d3..42401629 100644 --- a/src/Packages/Passport/Runtime/Scripts/Private/Model/DirectLoginOptions.cs +++ b/src/Packages/Passport/Runtime/Scripts/Private/Model/DirectLoginOptions.cs @@ -19,6 +19,11 @@ public class DirectLoginOptions /// public string email; + /// + /// Marketing consent status (optional). + /// + public MarketingConsentStatus? marketingConsentStatus; + /// /// Default constructor. /// @@ -26,6 +31,7 @@ public DirectLoginOptions() { directLoginMethod = DirectLoginMethod.Email; email = null; + marketingConsentStatus = null; } /// @@ -33,10 +39,12 @@ public DirectLoginOptions() /// /// The direct login method /// The email address (optional) - public DirectLoginOptions(DirectLoginMethod loginMethod, string emailAddress = null) + /// The marketing consent status (optional) + public DirectLoginOptions(DirectLoginMethod loginMethod, string emailAddress = null, MarketingConsentStatus? marketingConsentStatus = null) { directLoginMethod = loginMethod; email = emailAddress; + this.marketingConsentStatus = marketingConsentStatus; } /// diff --git a/src/Packages/Passport/Runtime/Scripts/Private/Model/MarketingConsentStatus.cs b/src/Packages/Passport/Runtime/Scripts/Private/Model/MarketingConsentStatus.cs new file mode 100644 index 00000000..07e74fce --- /dev/null +++ b/src/Packages/Passport/Runtime/Scripts/Private/Model/MarketingConsentStatus.cs @@ -0,0 +1,35 @@ +using System; + +namespace Immutable.Passport.Model +{ + /// + /// Enum representing marketing consent status. + /// + [Serializable] + public enum MarketingConsentStatus + { + OptedIn, + Unsubscribed + } + + /// + /// Extension methods for MarketingConsentStatus enum. + /// + public static class MarketingConsentStatusExtensions + { + /// + /// Converts the enum value to the string format expected by the game bridge. + /// + /// The marketing consent status + /// The corresponding string value + public static string ToApiString(this MarketingConsentStatus status) + { + return status switch + { + MarketingConsentStatus.OptedIn => "opted_in", + MarketingConsentStatus.Unsubscribed => "unsubscribed", + _ => throw new ArgumentOutOfRangeException(nameof(status), status, "Unknown MarketingConsentStatus value") + }; + } + } +} diff --git a/src/Packages/Passport/Runtime/Scripts/Private/Model/MarketingConsentStatus.cs.meta b/src/Packages/Passport/Runtime/Scripts/Private/Model/MarketingConsentStatus.cs.meta new file mode 100644 index 00000000..052053d3 --- /dev/null +++ b/src/Packages/Passport/Runtime/Scripts/Private/Model/MarketingConsentStatus.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 6381d06465c8ca447a54db08db89f3a7 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/src/Packages/Passport/Runtime/Scripts/Private/PassportImpl.cs b/src/Packages/Passport/Runtime/Scripts/Private/PassportImpl.cs index b5c91bc2..f362f7ea 100644 --- a/src/Packages/Passport/Runtime/Scripts/Private/PassportImpl.cs +++ b/src/Packages/Passport/Runtime/Scripts/Private/PassportImpl.cs @@ -287,11 +287,22 @@ private async UniTask LaunchAuthUrl() requestJson += $",\"email\":\"{_directLoginOptions.email}\""; } + if (_directLoginOptions.marketingConsentStatus != null) + { + var consentValue = _directLoginOptions.marketingConsentStatus.Value.ToApiString(); + requestJson += $",\"marketingConsentStatus\":\"{consentValue}\""; + } + requestJson += "}"; } + else + { + PassportLogger.Debug($"{TAG} No direct login options provided (standard auth flow)"); + } requestJson += "}"; + PassportLogger.Debug($"{TAG} Sending auth URL request: {requestJson}"); var callResponse = await _communicationsManager.Call(PassportFunction.GET_PKCE_AUTH_URL, requestJson); var response = callResponse.OptDeserializeObject(); @@ -774,7 +785,7 @@ public interface PKCECallback { /// - /// Called when the Android Chrome Custom Tabs is hidden. + /// Called when the Android Chrome Custom Tabs is hidden. /// Note that you won't be able to tell whether it was closed by the user or the SDK. /// True if the user has entered everything required (e.g. email address), /// Chrome Custom Tabs have closed, and the SDK is trying to complete the PKCE flow. diff --git a/src/Packages/Passport/Samples~/PassportManagerPrefab/Immutable.Passport.Samples.asmdef b/src/Packages/Passport/Samples~/PassportManagerPrefab/Immutable.Passport.Samples.asmdef new file mode 100644 index 00000000..f2469c2a --- /dev/null +++ b/src/Packages/Passport/Samples~/PassportManagerPrefab/Immutable.Passport.Samples.asmdef @@ -0,0 +1,29 @@ +{ + "name": "Immutable.Passport.Samples", + "rootNamespace": "Immutable.Passport", + "references": [ + "Immutable.Passport.Runtime.Public", + "Immutable.Passport.Runtime.Private", + "Immutable.Passport.Core.Logging", + "Immutable.Browser.Core", + "UniTask", + "Unity.Modules.UI", + "Unity.TextMeshPro" + ], + "includePlatforms": [ + "Android", + "Editor", + "iOS", + "macOSStandalone", + "WebGL", + "WindowsStandalone64" + ], + "excludePlatforms": [], + "allowUnsafeCode": false, + "overrideReferences": false, + "precompiledReferences": [], + "autoReferenced": false, + "defineConstraints": [], + "versionDefines": [], + "noEngineReferences": false +} diff --git a/src/Packages/Passport/Samples~/PassportManagerPrefab/PassportManager.cs b/src/Packages/Passport/Samples~/PassportManagerPrefab/PassportManager.cs new file mode 100644 index 00000000..06db748b --- /dev/null +++ b/src/Packages/Passport/Samples~/PassportManagerPrefab/PassportManager.cs @@ -0,0 +1,541 @@ +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] + [Tooltip("Your Immutable Passport client ID")] + private string clientId = "your-client-id-here"; + + [SerializeField] + [Tooltip("Environment: SANDBOX (default) or PRODUCTION")] + 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] + [Tooltip("Automatically initialise on Start (default: true)")] + private bool autoInitialize = true; + [SerializeField] + [Tooltip("Automatically attempt login after initialisation (default: false)")] + private bool autoLogin = false; + [SerializeField] + [Tooltip("Default login method for auto-login and generic login button (None, Google, Apple, Facebook)")] + private DirectLoginMethod directLoginMethod = DirectLoginMethod.None; + [SerializeField] + [Tooltip("Default consent status for marketing communications (Unsubscribed, OptedIn)")] + private MarketingConsentStatus defaultMarketingConsent = MarketingConsentStatus.Unsubscribed; + [SerializeField] + [Tooltip("Control debug output verbosity")] + private LogLevel logLevel = LogLevel.Info; + [SerializeField] + [Tooltip("Hide sensitive token information in debug logs for security")] + 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; } + + // UI Builder integration + private PassportUIBuilder uiBuilder; + + private void Awake() + { + // Singleton pattern + if (Instance == null) + { + Instance = this; + DontDestroyOnLoad(gameObject); + } + else + { + Destroy(gameObject); + return; + } + } + + private void Start() + { + // Find UI Builder if present + uiBuilder = GetComponent(); + + // 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 custom direct login options + /// + /// Custom direct login options including method, email, and marketing consent + public async void Login(DirectLoginOptions directLoginOptions) + { + await LoginAsync(directLoginOptions: directLoginOptions); + } + + /// + /// Internal async login method + /// + /// Optional direct login options. If null, uses the configured directLoginMethod + private async UniTask LoginAsync(DirectLoginOptions directLoginOptions = null) + { + if (!IsInitialized || PassportInstance == null) + { + Debug.LogError("[PassportManager] Passport not initialized. Call InitializePassport() first."); + return; + } + + try + { + // Determine final DirectLoginOptions to use + DirectLoginOptions finalDirectLoginOptions; + string loginMethodText; + + if (directLoginOptions != null) + { + // Use provided DirectLoginOptions (marketing consent already set by developer) + finalDirectLoginOptions = directLoginOptions; + loginMethodText = directLoginOptions.directLoginMethod.ToString(); + } + else + { + // Use configured directLoginMethod from Inspector + loginMethodText = directLoginMethod == DirectLoginMethod.None + ? "default method" + : directLoginMethod.ToString(); + + if (directLoginMethod == DirectLoginMethod.None) + { + // Standard auth flow + finalDirectLoginOptions = null; + } + else + { + // Direct login with configured default marketing consent + finalDirectLoginOptions = new DirectLoginOptions(directLoginMethod, marketingConsentStatus: defaultMarketingConsent); + } + } + + Debug.Log($"[PassportManager] Attempting login with {loginMethodText}..."); + + // Debug log marketing consent if present + if (finalDirectLoginOptions?.marketingConsentStatus != null) + { + Debug.Log($"[PassportManager] Marketing consent: {finalDirectLoginOptions.marketingConsentStatus}"); + } + + bool loginSuccess = await PassportInstance.Login(useCachedSession: false, directLoginOptions: finalDirectLoginOptions); + if (loginSuccess) + { + IsLoggedIn = true; + Debug.Log("[PassportManager] Login successful!"); + OnLoginSucceeded?.Invoke(); + + // Update UI state after successful login + UpdateUIState(); + + // Switch to logged-in panel if UI builder is present + if (uiBuilder != null) + { + uiBuilder.ShowLoggedInPanel(); + } + } + 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(); + + // Switch back to login panel if UI builder is present + if (uiBuilder != null) + { + uiBuilder.ShowLoginPanel(); + } + } + 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 (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(new DirectLoginOptions(DirectLoginMethod.Google, marketingConsentStatus: defaultMarketingConsent))); + googleLoginButton.interactable = IsInitialized && !IsLoggedIn; + Debug.Log($"[PassportManager] Configured Google login button with defaultMarketingConsent: {defaultMarketingConsent}"); + } + + if (appleLoginButton != null) + { + appleLoginButton.onClick.RemoveAllListeners(); + appleLoginButton.onClick.AddListener(() => Login(new DirectLoginOptions(DirectLoginMethod.Apple, marketingConsentStatus: defaultMarketingConsent))); + appleLoginButton.interactable = IsInitialized && !IsLoggedIn; + Debug.Log("[PassportManager] Configured Apple login button"); + } + + if (facebookLoginButton != null) + { + facebookLoginButton.onClick.RemoveAllListeners(); + facebookLoginButton.onClick.AddListener(() => Login(new DirectLoginOptions(DirectLoginMethod.Facebook, marketingConsentStatus: defaultMarketingConsent))); + 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 + 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 + + #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/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/PassportManagerComplete.prefab b/src/Packages/Passport/Samples~/PassportManagerPrefab/PassportManagerComplete.prefab new file mode 100644 index 00000000..3bd6e643 --- /dev/null +++ b/src/Packages/Passport/Samples~/PassportManagerPrefab/PassportManagerComplete.prefab @@ -0,0 +1,112 @@ +%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} + - component: {fileID: 8072342326407231330} + m_Layer: 0 + m_Name: PassportManagerComplete + 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: 0 + directLoginMethod: 0 + defaultMarketingConsent: 1 + logLevel: 3 + redactTokensInLogs: 1 + loginButton: {fileID: 0} + googleLoginButton: {fileID: 0} + appleLoginButton: {fileID: 0} + facebookLoginButton: {fileID: 0} + logoutButton: {fileID: 0} + statusText: {fileID: 0} + statusTextTMP: {fileID: 0} + userInfoText: {fileID: 0} + userInfoTextTMP: {fileID: 0} + OnPassportInitialized: + m_PersistentCalls: + m_Calls: [] + OnPassportError: + m_PersistentCalls: + m_Calls: [] + OnLoginSucceeded: + m_PersistentCalls: + m_Calls: [] + OnLoginFailed: + m_PersistentCalls: + m_Calls: [] + OnLogoutSucceeded: + m_PersistentCalls: + m_Calls: [] + OnLogoutFailed: + m_PersistentCalls: + m_Calls: [] +--- !u!114 &8072342326407231330 +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: ef72a80840b41d54aa0117a1694f72b6, type: 3} + m_Name: + m_EditorClassIdentifier: + passportManager: {fileID: 0} + canvasOrder: 100 + panelSize: {x: 300, y: 400} + buttonSize: {x: 280, y: 45} + elementSpacing: 10 + forceCursorAlwaysAvailable: 1 + uiCanvas: {fileID: 0} + loginPanel: {fileID: 0} + loggedInPanel: {fileID: 0} + loginButton: {fileID: 0} + googleLoginButton: {fileID: 0} + appleLoginButton: {fileID: 0} + facebookLoginButton: {fileID: 0} + logoutButton: {fileID: 0} + statusText: {fileID: 0} + userInfoText: {fileID: 0} diff --git a/src/Packages/Passport/Samples~/PassportManagerPrefab/PassportUIBuilder.cs b/src/Packages/Passport/Samples~/PassportManagerPrefab/PassportUIBuilder.cs new file mode 100644 index 00000000..dfded2df --- /dev/null +++ b/src/Packages/Passport/Samples~/PassportManagerPrefab/PassportUIBuilder.cs @@ -0,0 +1,563 @@ +using UnityEngine; +using UnityEngine.UI; +using UnityEngine.EventSystems; +using TMPro; +using Immutable.Passport.Model; + +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