Skip to content
Open
Show file tree
Hide file tree
Changes from 12 commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
ac6847c
fix: bugsweep aug16 consolidated — 24 validated fixes (21 bug + 3 perf)
eordano Aug 16, 2026
4605d63
chore: remove ~260 inspection warnings across all touched files
eordano Aug 16, 2026
ad35543
fix review: address Jarvis-protocol findings across all 24 lanes
eordano Aug 17, 2026
1f5ac82
revert: withdraw the credits top-up auto-cancel fix
eordano Aug 17, 2026
1d89406
chore: drop the superseded EventBus test at its old location
eordano Aug 17, 2026
09e24d8
fix: apply saved outfits promptly and fully when slots are unresolved…
eordano Aug 17, 2026
5a374d8
fix: own outfit-fetch pointers so the detached background fetch canno…
eordano Aug 17, 2026
d35afac
Merge branch 'dev' into bugsweep/aug16-consolidated
eordano Aug 17, 2026
9c84b52
Update Explorer/Assets/DCL/Web3/Accounts/RustEthereumAccount.cs
eordano Aug 17, 2026
141faae
revert: keep LeftPad exact-length guard (== size), not >= size
eordano Aug 17, 2026
1a375d4
fix: scope cleartext-scheme enforcement so local-scene-development fe…
eordano Aug 17, 2026
22a1a3d
fix: gate cleartext exemption on explicit local-scene-development opt…
eordano Aug 18, 2026
34a937e
fix(websocket): keep the RPC receive loop on the main thread after co…
eordano Aug 18, 2026
60739e9
chore(merge-prep): adopt dev's AuthenticationScreenController null-sa…
eordano Aug 18, 2026
04be4d0
Merge branch 'dev' into bugsweep/aug16-consolidated
eordano Aug 18, 2026
213268a
fix(build): pass dev's new IMVCManager arg in the AuthenticationScree…
eordano Aug 18, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -82,16 +82,17 @@ public enum AuthStatus
// misclassifying returning users whose cached identity expired.
public bool IsCurrentlyNewAccount { get; internal set; }

public event Action DiscordButtonClicked;
public event Action<string, bool> OTPVerified;
public event Action OTPResend;
public event Action ProfileFinalized;
public event Action? DiscordButtonClicked;
public event Action<string, bool>? OTPVerified;
public event Action? OTPResend;
public event Action? ProfileFinalized;

internal void RaiseProfileFinalized() =>
ProfileFinalized?.Invoke();

private MVCStateMachine<AuthStateBase> fsm;
private AuthenticationScreenAudio audio;
// Null until OnViewInstantiated: the view is created lazily on first Show and may never be instantiated.
private MVCStateMachine<AuthStateBase>? fsm;
private AuthenticationScreenAudio? audio;

public AuthenticationScreenController(
ViewFactoryMethod viewFactory,
Expand Down Expand Up @@ -141,8 +142,8 @@ public override void Dispose()
characterPreviewController?.Dispose();

CancelLoginProcess();
audio.Dispose();
fsm.Dispose();
audio?.Dispose();
fsm?.Dispose();
}

protected override void OnViewInstantiated()
Expand Down Expand Up @@ -194,6 +195,7 @@ protected override void OnViewInstantiated()
protected override void OnBeforeViewShow()
{
base.OnBeforeViewShow();

// Force to re-login if the identity will expire in 24hs or less, so we mitigate the chances on
// getting the identity expired while in-world, provoking signed-fetch requests to fail
IWeb3Identity? storedIdentity = storedIdentityProvider.Identity;
Expand All @@ -206,7 +208,7 @@ protected override void OnBeforeViewShow()
}
else
{
fsm.Enter<LoginSelectionAuthState, int>(UIAnimationHashes.IN, true);
fsm!.Enter<LoginSelectionAuthState, int>(UIAnimationHashes.IN, true);
}
}

Expand All @@ -217,10 +219,10 @@ private async UniTaskVoid TryAutoLoginAndProceedAsync(IWeb3Identity storedIdenti
bool autoLoginSuccess = await web3Authenticator.TryAutoLoginAsync(ct);

if (autoLoginSuccess)
fsm.Enter<ProfileFetchingAuthState, ProfileFetchingPayload>(new (storedIdentity, storedIdentity.Source != IWeb3Identity.Web3IdentitySource.TokenFile, ct));
fsm!.Enter<ProfileFetchingAuthState, ProfileFetchingPayload>(new (storedIdentity, storedIdentity.Source != IWeb3Identity.Web3IdentitySource.TokenFile, ct));
else
{
fsm.Enter<LoginSelectionAuthState, int>(UIAnimationHashes.IN, true);
fsm!.Enter<LoginSelectionAuthState, int>(UIAnimationHashes.IN, true);
}
}
catch (OperationCanceledException)
Expand All @@ -229,7 +231,7 @@ private async UniTaskVoid TryAutoLoginAndProceedAsync(IWeb3Identity storedIdenti
catch (Exception e)
{
ReportHub.LogException(e, new ReportData(ReportCategory.AUTHENTICATION));
fsm.Enter<LoginSelectionAuthState, int>(UIAnimationHashes.IN, true);
fsm!.Enter<LoginSelectionAuthState, int>(UIAnimationHashes.IN, true);
}
}

Expand All @@ -238,18 +240,18 @@ protected override void OnViewShow()
base.OnViewShow();

BlockUnwantedInputs();
audio.OnShow();
audio!.OnShow();
}

protected override void OnViewClose()
{
base.OnViewClose();

fsm.CurrentState?.Exit();
fsm!.CurrentState?.Exit();
CancelLoginProcess();

UnblockUnwantedInputs();
audio.OnHide();
audio!.OnHide();
}

protected override async UniTask WaitForCloseIntentAsync(CancellationToken ct)
Expand Down Expand Up @@ -288,7 +290,7 @@ async UniTaskVoid ChangeAccountAsync(CancellationToken ct)

await web3Authenticator.LogoutAsync(ct);

fsm.Enter<LoginSelectionAuthState, int>(UIAnimationHashes.SLIDE, true);
fsm!.Enter<LoginSelectionAuthState, int>(UIAnimationHashes.SLIDE, true);
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ namespace DCL.AuthenticationScreenFlow
{
public class ProfileFetchingAuthState : AuthStateBase, IPayloadedState<ProfileFetchingPayload>
{
private const int PROFILE_FETCH_ATTEMPTS = 3;
private static readonly TimeSpan PROFILE_FETCH_TIMEOUT = TimeSpan.FromSeconds(15);

private readonly MVCStateMachine<AuthStateBase> machine;
Expand Down Expand Up @@ -70,7 +71,7 @@ public override void Exit()
ProfileNotFoundException ex => new SpanErrorInfo($"Profile not found during {nameof(ProfileFetchingAuthState)}", ex),
NotAllowedUserException ex => new SpanErrorInfo(ex.Message, ex),
TimeoutException ex => new SpanErrorInfo($"Profile fetch timed out during {nameof(ProfileFetchingAuthState)}", ex),
Exception ex => new SpanErrorInfo($"Unexpected error during {nameof(ProfileFetchingAuthState)}", ex),
{ } ex => new SpanErrorInfo($"Unexpected error during {nameof(ProfileFetchingAuthState)}", ex),
};

if (profileFetchException is not OperationCanceledException and not ProfileNotFoundException and not NotAllowedUserException)
Expand Down Expand Up @@ -110,9 +111,7 @@ private async UniTaskVoid FetchProfileFlowAsync(string email, IWeb3Identity iden
});

// Timeout surfaces catalyst stalls as CONNECTION_ERROR instead of a frozen spinner.
Profile? profile = await selfProfile.ProfileAsync(ct).Timeout(PROFILE_FETCH_TIMEOUT);

if (profile != null)
if (await FetchProfileWithTimeoutRetriesAsync(selfProfile, PROFILE_FETCH_TIMEOUT, PROFILE_FETCH_ATTEMPTS, ct) is { } profile)
{
// When the profile was already in cache, for example your previous account after logout, we need to ensure that all systems related to the profile will update
profile.IsDirty = true;
Expand Down Expand Up @@ -156,6 +155,34 @@ private async UniTaskVoid FetchProfileFlowAsync(string email, IWeb3Identity iden
}
}

/// <summary>
/// Each attempt owns a linked token, so a timed-out attempt cancels its underlying request instead of
/// abandoning it. Only exhausting all attempts surfaces as <see cref="TimeoutException" /> (CONNECTION_ERROR);
/// cancellation of <paramref name="ct" /> surfaces as <see cref="OperationCanceledException" />.
/// </summary>
internal static async UniTask<Profile?> FetchProfileWithTimeoutRetriesAsync(ISelfProfile selfProfile, TimeSpan attemptTimeout, int maxAttempts, CancellationToken ct)
{
for (var attempt = 1;; attempt++)
{
using CancellationTokenSource timeoutCts = CancellationTokenSource.CreateLinkedTokenSource(ct);
using IDisposable timeoutTimer = timeoutCts.CancelAfterSlim(attemptTimeout);

if (await selfProfile.ProfileAsync(timeoutCts.Token) is { } profile)
return profile;

// The repository suppresses cancellation into a null profile, including cancellation of the flow token.
// Surface external cancellation as OCE so it is classified as a user cancel, not as "no deployed profile"
// (which on the cached flow would clear a still-valid stored identity)
ct.ThrowIfCancellationRequested();

if (!timeoutCts.IsCancellationRequested)
return null; // genuine "no deployed profile"

if (attempt >= maxAttempts)
throw new TimeoutException($"Profile fetch timed out after {maxAttempts} attempts of {attemptTimeout.TotalSeconds:F0}s each");
}
}

private Profile CreateRandomProfile(string identityAddress)
{
var profile = Profile.NewRandomProfile(identityAddress);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
using NUnit.Framework;

namespace DCL.AuthenticationScreenFlow.Tests
{
[TestFixture]
public class AuthenticationScreenControllerShould
{
[Test]
public void NotThrowOnDisposeWhenViewWasNeverShown()
{
// Registered-but-never-shown lifecycle: the view factory is never invoked, so
// OnViewInstantiated never runs and the lazily-created members stay null
// (sessions with --skip-auth-screen + a valid cached identity).
// The constructor only stores its dependencies; none are dereferenced before the view exists.
var controller = new AuthenticationScreenController(
() => null!,
null!,
null!,
null!,
null!,
null!,
null!,
null!,
null!,
string.Empty,
null!,
null!,
null!,
null!,
null!,
null!,
null!,
null!);

Assert.DoesNotThrow(controller.Dispose);
}
}
}

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading
Loading