diff --git a/com.playeveryware.eos/Runtime/Core/EOSManager.cs b/com.playeveryware.eos/Runtime/Core/EOSManager.cs
index fa61b2fa8..dd7736f87 100644
--- a/com.playeveryware.eos/Runtime/Core/EOSManager.cs
+++ b/com.playeveryware.eos/Runtime/Core/EOSManager.cs
@@ -78,6 +78,7 @@ namespace PlayEveryWare.EpicOnlineServices
using Extensions;
using System.Diagnostics;
using System.Globalization;
+ using System.Threading;
using UnityEngine.Assertions;
using AddNotifyLoginStatusChangedOptions = Epic.OnlineServices.Auth.AddNotifyLoginStatusChangedOptions;
using Credentials = Epic.OnlineServices.Auth.Credentials;
@@ -197,6 +198,10 @@ public partial class EOSSingleton
static private NotifyEventHandle s_notifyConnectLoginStatusChangedCallbackHandle;
static private NotifyEventHandle s_notifyConnectAuthExpirationCallbackHandle;
+ // --- Connect reauth hardening (prevents PUID churn / P2P route resets) ---
+ static private int s_connectReauthInProgress = 0;// Last Connect credential type used. Used to decide whether we can auto-refresh on auth expiration.
+ static private Epic.OnlineServices.ExternalCredentialType s_lastConnectCredentialType;
+
// Setting it twice will cause an exception
static bool hasSetLoggingCallback;
@@ -256,9 +261,14 @@ private string PUIDToString(ProductUserId puid)
///
protected void SetLocalProductUserId(ProductUserId localProductUserId)
{
- Log("Changing PUID: " + PUIDToString(s_localProductUserId) + " => " +
- PUIDToString(localProductUserId));
+ var previous = s_localProductUserId;
s_localProductUserId = localProductUserId;
+
+ // Do not log user identifiers (PUID/EAS ids).
+ if (previous != null && localProductUserId != null && !previous.Equals(localProductUserId))
+ {
+ Log("[EOS] Local ProductUserId changed during runtime. This likely indicates account switch or login with different credentials. Game should tear down/rebuild P2P as needed.", LogType.Error);
+ }
}
//-------------------------------------------------------------------------
@@ -1076,7 +1086,7 @@ public async void StartConnectLoginWithEpicAccount(EpicAccountId epicAccountId,
if (result != Result.Success || !idToken.HasValue)
{
- Debug.LogError($"{nameof(EOSManager)} {nameof(StartConnectLoginWithEpicAccount)}: CopyIdToken failed with result: {result}");
+ Log($"{nameof(EOSManager)} {nameof(StartConnectLoginWithEpicAccount)}: CopyIdToken failed with result: {result}", LogType.Error);
var dummy = new Epic.OnlineServices.Connect.LoginCallbackInfo
{
ResultCode = Result.InvalidAuth
@@ -1163,26 +1173,38 @@ public void StartConnectLoginWithOptions(ExternalCredentialType externalCredenti
public void StartConnectLoginWithOptions(Epic.OnlineServices.Connect.LoginOptions connectLoginOptions,
OnConnectLoginCallback onloginCallback)
{
+ CacheLastConnectCredentialType(connectLoginOptions);
+ // Do not log user identifiers (PUID/EAS ids).
+ Log($"[EOS][ConnectLogin] start cred={s_lastConnectCredentialType}");
+
var connectInterface = GetEOSPlatformInterface().GetConnectInterface();
connectInterface.Login(ref connectLoginOptions, null,
(ref Epic.OnlineServices.Connect.LoginCallbackInfo connectLoginData) =>
{
- if (connectLoginData.ResultCode != Result.Success)
+ try
{
- Log($"Connect login was not successful. ResultCode: {connectLoginData.ResultCode}", LogType.Error);
- }
+ if (connectLoginData.ResultCode != Result.Success)
+ {
+ Log($"Connect login was not successful. ResultCode: {connectLoginData.ResultCode}", LogType.Error);
+ return;
+ }
- if (connectLoginData.LocalUserId != null)
- {
- SetLocalProductUserId(connectLoginData.LocalUserId);
+ if (connectLoginData.LocalUserId == null)
+ {
+ Log("Connect login succeeded but LocalUserId is null (unexpected).", LogType.Error);
+ return;
+ }
+
+ var newPuid = connectLoginData.LocalUserId;
+ SetLocalProductUserId(newPuid);
ConfigureConnectStatusCallback();
- ConfigureConnectExpirationCallback(connectLoginOptions);
- OnConnectLogin?.Invoke(connectLoginData);
+ ConfigureConnectExpirationCallback();
}
-
- if (onloginCallback != null)
+ finally
{
- onloginCallback(connectLoginData);
+ // Always notify listeners/caller exactly once, regardless of early returns above.
+ OnConnectLogin?.Invoke(connectLoginData);
+ onloginCallback?.Invoke(connectLoginData);
}
});
}
@@ -1264,7 +1286,7 @@ public void StartPersistentLogin(OnAuthLoginCallback onLoginCallback)
public void StartLoginWithLoginTypeAndToken(LoginCredentialType loginType, string id, string token,
OnAuthLoginCallback onLoginCallback)
{
- if(loginType == LoginCredentialType.ExchangeCode && string.IsNullOrEmpty(token))
+ if (loginType == LoginCredentialType.ExchangeCode && string.IsNullOrEmpty(token))
{
Debug.LogError($"{nameof(EOSManager)} {nameof(StartLoginWithLoginTypeAndToken)}: ExchangeCode login attempted with empty token. Abort login.");
onLoginCallback?.Invoke(new LoginCallbackInfo
@@ -1345,25 +1367,26 @@ private void ConfigureConnectStatusCallback()
}
//-------------------------------------------------------------------------
- private void ConfigureConnectExpirationCallback(Epic.OnlineServices.Connect.LoginOptions connectLoginOptions)
+ private void ConfigureConnectExpirationCallback()
{
- if (s_notifyConnectAuthExpirationCallbackHandle == null)
- {
- var EOSConnectInterface = GetEOSConnectInterface();
- var addNotifyAuthExpirationOptions = new AddNotifyAuthExpirationOptions();
- ulong callbackHandle = EOSConnectInterface.AddNotifyAuthExpiration(
- ref addNotifyAuthExpirationOptions, null, (ref AuthExpirationCallbackInfo callbackInfo) =>
- {
- StartConnectLoginWithOptions(connectLoginOptions, null);
- });
+ if (s_notifyConnectAuthExpirationCallbackHandle != null)
+ return;
- s_notifyConnectAuthExpirationCallbackHandle = new NotifyEventHandle(callbackHandle, handle =>
- {
- GetEOSConnectInterface()?.RemoveNotifyAuthExpiration(handle);
- });
- }
+ var eosConnectInterface = GetEOSConnectInterface();
+ var addNotifyAuthExpirationOptions = new AddNotifyAuthExpirationOptions();
+
+ ulong callbackHandle = eosConnectInterface.AddNotifyAuthExpiration(
+ ref addNotifyAuthExpirationOptions, null,
+ OnConnectAuthExpiration);
+ Log("[EOS][AuthExp] registered");
+ s_notifyConnectAuthExpirationCallbackHandle = new NotifyEventHandle(callbackHandle, handle =>
+ {
+ GetEOSConnectInterface()?.RemoveNotifyAuthExpiration(handle);
+ });
}
+
+
//-------------------------------------------------------------------------
///
/// Start an EOS Auth Login with the passed in LoginOptions. Call this instead of the method on EOSAuthInterface to ensure that
@@ -1792,6 +1815,77 @@ private static void UpdateNetworkStatus()
platformSpecifics?.UpdateNetworkStatus();
}
+
+ private void CacheLastConnectCredentialType(Epic.OnlineServices.Connect.LoginOptions connectLoginOptions)
+ {
+ // Credentials is nullable: must validate HasValue.
+ if (!connectLoginOptions.Credentials.HasValue)
+ return;
+
+ s_lastConnectCredentialType = connectLoginOptions.Credentials.Value.Type;
+ }
+
+ ///
+ /// Handles Connect auth-expiration notifications.
+ ///
+ /// Policy:
+ /// - If the last Connect credential type was EpicIdToken, the plugin can refresh by re-running Connect.Login
+ /// with a fresh Epic ID token (obtained via Auth.CopyIdToken) and restore Connect state.
+ /// - For other external credential types, the plugin cannot refresh tokens automatically; the game must
+ /// re-fetch the external token from the platform and call StartConnectLoginWithOptions again.
+ ///
+ private void OnConnectAuthExpiration(ref AuthExpirationCallbackInfo callbackInfo)
+ {
+ // Do not log user identifiers (PUID/EAS ids).
+ Log($"[EOS][AuthExp] received. CredentialType={s_lastConnectCredentialType}", LogType.Warning);
+
+ // Prevent concurrent re-auth attempts from repeated expiration notifications.
+ if (Interlocked.Exchange(ref s_connectReauthInProgress, 1) == 1)
+ {
+ Log($"{nameof(EOSManager)} Connect auth expiration ignored (reauth already in progress).", LogType.Warning);
+ return;
+ }
+
+ try
+ {
+ if (s_lastConnectCredentialType == Epic.OnlineServices.ExternalCredentialType.EpicIdToken)
+ {
+ RefreshConnectLoginWithFreshEpicIdToken();
+ }
+ else
+ {
+ Debug.LogWarning(
+ $"{nameof(EOSManager)} Connect auth expired but last credential type was {s_lastConnectCredentialType}. " +
+ "Plugin cannot refresh that token automatically. Game should re-fetch external token and call StartConnectLoginWithOptions again.");
+ }
+ }
+ finally
+ {
+ Interlocked.Exchange(ref s_connectReauthInProgress, 0);
+ }
+ }
+
+
+ private void RefreshConnectLoginWithFreshEpicIdToken()
+ {
+ // NOTE: EOS callbacks are expected to run on the same thread that drives PlatformInterface.Tick().
+ // In typical Unity integrations that's the main thread.
+ try
+ {
+ var localEpicUserId = GetLocalUserId();
+ if (localEpicUserId == null)
+ {
+ Log($"{nameof(EOSManager)} RefreshConnectLoginWithFreshEpicIdToken: Local EpicAccountId is null.", LogType.Warning);
+ return;
+ }
+
+ StartConnectLoginWithEpicAccount(localEpicUserId, null);
+ }
+ catch(System.Exception ex)
+ {
+ Log($"{nameof(EOSManager)} {nameof(RefreshConnectLoginWithFreshEpicIdToken)} failed:{ex.GetType().Name}: {ex.Message}",LogType.Error);
+ }
+ }
}
#endif