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