Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
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 @@ -19,6 +19,7 @@
using DCL.Web3.Accounts.Factory;
using DCL.Web3.Authenticators;
using DCL.Web3.Identities;
using DCL.WebRequests;
using DCL.WebRequests.Analytics;
using DCL.WebRequests.ChromeDevtool;
using ECS.StreamableLoading.Cache.Disk;
Expand Down Expand Up @@ -119,7 +120,7 @@ await bootstrapContainer.InitializeContainerAsync<BootstrapContainer, BootstrapS
var realmUrls = new RealmUrls(realmLaunchSettings, new RealmNamesMap(webRequestsContainer.WebRequestController), decentralandUrlsSource);

(container.Bootstrap, container.Analytics) = CreateBootstrapperAsync(debugSettings, applicationParametersParser, splashScreen, realmUrls, diskCache, partialsDiskCache, container, webRequestsContainer, container.settings, realmLaunchSettings, world, container.settings.BuildData, dclVersion, ct);
container.CompositeWeb3Provider = CreateWeb3Dependencies(sceneLoaderSettings, web3AccountFactory, identityCache, browser, container, decentralandUrlsSource, decentralandEnvironment, applicationParametersParser);
container.CompositeWeb3Provider = CreateWeb3Dependencies(sceneLoaderSettings, web3AccountFactory, identityCache, browser, container, decentralandUrlsSource, decentralandEnvironment, applicationParametersParser, webRequestsContainer.WebRequestController);

if (container.EnableAnalytics)
{
Expand Down Expand Up @@ -216,7 +217,8 @@ private static ICompositeWeb3Provider CreateWeb3Dependencies(
BootstrapContainer container,
IDecentralandUrlsSource decentralandUrlsSource,
DecentralandEnvironment dclEnvironment,
IAppArgs appArgs)
IAppArgs appArgs,
IWebRequestController webRequestController)
{
int? identityExpirationDuration = appArgs.TryGetValue(AppArgsFlags.IDENTITY_EXPIRATION_DURATION, out string? v)
? int.Parse(v!)
Expand All @@ -229,6 +231,7 @@ private static ICompositeWeb3Provider CreateWeb3Dependencies(
new HashSet<string>(sceneLoaderSettings.Web3WhitelistMethods),
new HashSet<string>(sceneLoaderSettings.Web3ReadOnlyMethods),
web3AccountFactory,
webRequestController,
identityExpirationDuration
);

Expand All @@ -247,8 +250,10 @@ private static ICompositeWeb3Provider CreateWeb3Dependencies(
identityExpirationDuration
);

bool emailOtpEnabled = FeatureFlagsConfiguration.Instance.IsEnabled(FeatureFlagsStrings.EMAIL_OTP_AUTH);

ICompositeWeb3Provider result = new CompositeWeb3Provider(thirdWebAuth, dappAuth, identityCache,
container.Analytics ?? IAnalyticsController.Null);
container.Analytics ?? IAnalyticsController.Null, emailOtpEnabled);

return result;
}
Expand Down
172 changes: 158 additions & 14 deletions Explorer/Assets/DCL/Infrastructure/Global/Dynamic/MainSceneLoader.cs

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using Cysharp.Threading.Tasks;
using DCL.Diagnostics;
using DCL.PerformanceAndDiagnostics.Analytics;
using DCL.Prefs;
using DCL.Web3.Identities;
Expand All @@ -19,6 +20,7 @@ public class CompositeWeb3Provider : ICompositeWeb3Provider
private readonly DappWeb3Authenticator dappAuth;
private readonly IWeb3IdentityCache identityCache;
private readonly IAnalyticsController analytics;
private readonly bool emailOtpEnabled;

private AuthProvider currentProvider = AuthProvider.Dapp;

Expand Down Expand Up @@ -55,12 +57,14 @@ public CompositeWeb3Provider(
ThirdWebAuthenticator thirdWebAuth,
DappWeb3Authenticator dappAuth,
IWeb3IdentityCache identityCache,
IAnalyticsController analytics)
IAnalyticsController analytics,
bool emailOtpEnabled)
{
this.thirdWebAuth = thirdWebAuth ?? throw new ArgumentNullException(nameof(thirdWebAuth));
this.dappAuth = dappAuth ?? throw new ArgumentNullException(nameof(dappAuth));
this.identityCache = identityCache ?? throw new ArgumentNullException(nameof(identityCache));
this.analytics = analytics ?? throw new ArgumentNullException(nameof(analytics));
this.emailOtpEnabled = emailOtpEnabled;
}

// IWeb3Authenticator
Expand Down Expand Up @@ -92,8 +96,19 @@ public UniTask ResendOtpAsync(CancellationToken ct = default) =>

public UniTask<bool> TryAutoLoginAsync(CancellationToken ct)
{
// Temporary heuristic: if we have a stored email, assume ThirdWeb OTP flow; otherwise default to Dapp Wallet.
string email = DCLPlayerPrefs.GetString(DCLPrefKeys.LOGGEDIN_EMAIL, string.Empty);

// If OTP feature flag is disabled, skip ThirdWeb auto-login entirely — even if there is a stored email.
// This prevents getting stuck at the splash screen when the FF is turned off while the user has a previous OTP session.
if (!emailOtpEnabled && !string.IsNullOrEmpty(email))
{
ReportHub.Log(ReportCategory.AUTHENTICATION, "OTP email feature flag is disabled — clearing stored email and skipping ThirdWeb auto-login");
DCLPlayerPrefs.DeleteKey(DCLPrefKeys.LOGGEDIN_EMAIL, save: true);
CurrentProvider = AuthProvider.Dapp;
return UniTask.FromResult(true);
}

// Heuristic: if we have a stored email, assume ThirdWeb OTP flow; otherwise default to Dapp Wallet.
CurrentProvider = string.IsNullOrEmpty(email) ? AuthProvider.Dapp : AuthProvider.ThirdWeb;

// Only ThirdWeb supports auto-login
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,206 @@
using CommunicationData.URLHelpers;
using Cysharp.Threading.Tasks;
using DCL.Diagnostics;
using DCL.WebRequests;
using System;
using System.Collections.Generic;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
using Thirdweb;
using UnityEngine.Networking;

namespace DCL.Web3.Authenticators
{
/// <summary>
/// Adapter that implements Thirdweb SDK's <see cref="IThirdwebHttpClient"/> using the project's
/// <see cref="IWebRequestController"/>, ensuring all HTTP traffic goes through our
/// allocation-optimized, budgeted web request infrastructure (analytics, Sentry, Chrome DevTools, etc.).
/// </summary>
internal class DclThirdwebHttpClient : IThirdwebHttpClient
{
/// <summary>
/// Shared controller reference, set once by the primary constructor and reused
/// by reflection-created clones (Thirdweb SDK's <c>Utils.ReconstructHttpClient</c>
/// instantiates clones via a parameterless constructor found through reflection).
/// </summary>
private static IWebRequestController? sharedWebRequestController;

private readonly IWebRequestController webRequestController;

public Dictionary<string, string> Headers { get; private set; } = new ();

/// <summary>
/// Primary constructor — called explicitly when wiring up the Thirdweb client.
/// </summary>
internal DclThirdwebHttpClient(IWebRequestController webRequestController)
{
sharedWebRequestController = webRequestController;
this.webRequestController = webRequestController;
}

/// <summary>
/// Parameterless constructor required by Thirdweb SDK internals.
/// <c>Thirdweb.Utils.ReconstructHttpClient</c> clones the HTTP client via reflection
/// using <c>GetConstructor(Type.EmptyTypes).Invoke(null)</c>.
/// </summary>
public DclThirdwebHttpClient()
{
this.webRequestController = sharedWebRequestController
?? throw new InvalidOperationException(
$"{nameof(DclThirdwebHttpClient)} must first be created with {nameof(IWebRequestController)} before parameterless construction.");
}

public void SetHeaders(Dictionary<string, string> headers)
{
Headers = new Dictionary<string, string>(headers);
}

public void ClearHeaders()
{
Headers.Clear();
}

public void AddHeader(string key, string value)
{
Headers[key] = value;
}

public void RemoveHeader(string key)
{
Headers.Remove(key);
}

public Task<ThirdwebHttpResponseMessage> GetAsync(string requestUri, CancellationToken cancellationToken = default) =>
SendGetInternalAsync(requestUri, cancellationToken).AsTask();

public Task<ThirdwebHttpResponseMessage> PostAsync(string requestUri, HttpContent content, CancellationToken cancellationToken = default) =>
SendPostInternalAsync(requestUri, content, cancellationToken).AsTask();

public Task<ThirdwebHttpResponseMessage> PutAsync(string requestUri, HttpContent content, CancellationToken cancellationToken = default) =>
SendPutInternalAsync(requestUri, content, cancellationToken).AsTask();

public Task<ThirdwebHttpResponseMessage> DeleteAsync(string requestUri, CancellationToken cancellationToken = default) =>
SendDeleteInternalAsync(requestUri, cancellationToken).AsTask();

public void Dispose() { }

private async UniTask<ThirdwebHttpResponseMessage> SendGetInternalAsync(string requestUri, CancellationToken ct)
{
try
{
return await webRequestController
.SendAsync<GenericGetRequest, GenericGetArguments, ThirdwebResponseOp<GenericGetRequest>, ThirdwebHttpResponseMessage>(
CommonArgumentsFor(requestUri),
default,
new ThirdwebResponseOp<GenericGetRequest>(),
ct,
ReportCategory.AUTHENTICATION,
headersInfo: CreateHeadersInfo(),
suppressErrors: true
);
}
catch (UnityWebRequestException ex) { return WrapErrorResponse(ex); }
}

private async UniTask<ThirdwebHttpResponseMessage> SendPostInternalAsync(string requestUri, HttpContent content, CancellationToken ct)
{
string postData = await content.ReadAsStringAsync();
string contentType = content.Headers.ContentType?.ToString() ?? GenericPostArguments.JSON;

try
{
return await webRequestController
.SendAsync<GenericPostRequest, GenericPostArguments, ThirdwebResponseOp<GenericPostRequest>, ThirdwebHttpResponseMessage>(
CommonArgumentsFor(requestUri),
GenericPostArguments.Create(postData, contentType),
new ThirdwebResponseOp<GenericPostRequest>(),
ct,
ReportCategory.AUTHENTICATION,
headersInfo: CreateHeadersInfo(),
suppressErrors: true
);
}
catch (UnityWebRequestException ex) { return WrapErrorResponse(ex); }
}

private async UniTask<ThirdwebHttpResponseMessage> SendPutInternalAsync(string requestUri, HttpContent content, CancellationToken ct)
{
string postData = await content.ReadAsStringAsync();
string contentType = content.Headers.ContentType?.ToString() ?? GenericPostArguments.JSON;

try
{
return await webRequestController
.SendAsync<GenericPutRequest, GenericPostArguments, ThirdwebResponseOp<GenericPutRequest>, ThirdwebHttpResponseMessage>(
CommonArgumentsFor(requestUri),
GenericPostArguments.Create(postData, contentType),
new ThirdwebResponseOp<GenericPutRequest>(),
ct,
ReportCategory.AUTHENTICATION,
headersInfo: CreateHeadersInfo(),
suppressErrors: true
);
}
catch (UnityWebRequestException ex) { return WrapErrorResponse(ex); }
}

private async UniTask<ThirdwebHttpResponseMessage> SendDeleteInternalAsync(string requestUri, CancellationToken ct)
{
try
{
return await webRequestController
.SendAsync<GenericDeleteRequest, GenericPostArguments, ThirdwebResponseOp<GenericDeleteRequest>, ThirdwebHttpResponseMessage>(
CommonArgumentsFor(requestUri),
GenericPostArguments.Empty,
new ThirdwebResponseOp<GenericDeleteRequest>(),
ct,
ReportCategory.AUTHENTICATION,
headersInfo: CreateHeadersInfo(),
suppressErrors: true
);
}
catch (UnityWebRequestException ex) { return WrapErrorResponse(ex); }
}

private static CommonArguments CommonArgumentsFor(string requestUri) =>
new (URLAddress.FromString(requestUri), RetryPolicy.NONE);

private WebRequestHeadersInfo CreateHeadersInfo() =>
new (Headers);

/// <summary>
/// Wraps a non-2xx <see cref="UnityWebRequestException"/> into <see cref="ThirdwebHttpResponseMessage"/>
/// instead of throwing, since the Thirdweb SDK checks <see cref="ThirdwebHttpResponseMessage.IsSuccessStatusCode"/>
/// and handles errors on its own.
/// </summary>
private static ThirdwebHttpResponseMessage WrapErrorResponse(UnityWebRequestException ex) =>
new (
statusCode: ex.ResponseCode,
content: new ThirdwebHttpContent(ex.Text ?? string.Empty),
isSuccessStatusCode: false
);

/// <summary>
/// Operation that captures raw response bytes and status code on successful requests.
/// </summary>
private readonly struct ThirdwebResponseOp<TRequest> : IWebRequestOp<TRequest, ThirdwebHttpResponseMessage>
where TRequest : struct, ITypedWebRequest
{
public UniTask<ThirdwebHttpResponseMessage?> ExecuteAsync(TRequest webRequest, CancellationToken ct)
{
var wr = webRequest.UnityWebRequest;
byte[] data = wr.downloadHandler?.data ?? Array.Empty<byte>();
long statusCode = wr.responseCode;

return UniTask.FromResult<ThirdwebHttpResponseMessage?>(
new ThirdwebHttpResponseMessage(
statusCode: statusCode,
content: new ThirdwebHttpContent(data),
isSuccessStatusCode: statusCode >= 200 && statusCode < 300
)
);
}
}
}
}

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

Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
using DCL.Multiplayer.Connections.DecentralandUrls;
using DCL.Web3.Abstract;
using DCL.Web3.Identities;
using DCL.WebRequests;
using System.Collections.Generic;
using System.Numerics;
using System.Threading;
Expand Down Expand Up @@ -43,12 +44,13 @@ internal ThirdWebAuthenticator(
HashSet<string> whitelistMethods,
HashSet<string> readOnlyMethods,
IWeb3AccountFactory web3AccountFactory,
IWebRequestController webRequestController,
int? identityExpirationDuration = null)
{
var thirdwebClient = ThirdwebClient.Create(
CLIENT_ID,
bundleId: BUNDLE_ID,
httpClient: new ThirdwebHttpClient(),
httpClient: new DclThirdwebHttpClient(webRequestController),
sdkName: "UnitySDK",
sdkOs: Application.platform.ToString(),
sdkPlatform: "unity",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,12 @@ namespace DCL.Web3.Authenticators
{
public class ThirdWebLoginService
{
/// <summary>
/// Maximum time allowed for auto-login to complete.
/// Prevents indefinite waiting on the splash screen if ThirdWeb services are slow or unreachable.
/// </summary>
private static readonly TimeSpan AUTO_LOGIN_TIMEOUT = TimeSpan.FromSeconds(60);

private readonly IWeb3AccountFactory web3AccountFactory;
private readonly int? identityExpirationDuration;
public IThirdwebWallet? ActiveWallet { get; private set; }
Expand All @@ -38,9 +44,13 @@ public async UniTask<bool> TryAutoLoginAsync(CancellationToken ct)
if (string.IsNullOrEmpty(email))
return false;

using var timeoutCts = CancellationTokenSource.CreateLinkedTokenSource(ct);
timeoutCts.CancelAfter(AUTO_LOGIN_TIMEOUT);
CancellationToken linkedCt = timeoutCts.Token;

try
{
await UniTask.SwitchToMainThread(ct);
await UniTask.SwitchToMainThread(linkedCt);

InAppWallet? wallet = await InAppWallet.Create(
client,
Expand All @@ -51,9 +61,20 @@ public async UniTask<bool> TryAutoLoginAsync(CancellationToken ct)
return false;

ActiveWallet = wallet;
ReportHub.Log(ReportCategory.AUTHENTICATION, "ThirdWeb auto-login successful");
ReportHub.Log(ReportCategory.AUTHENTICATION, "ThirdWeb auto-login successful");
return true;
}
catch (OperationCanceledException) when (!ct.IsCancellationRequested)
{
// Timeout expired (not an external cancellation) — treat as auto-login failure
ReportHub.LogWarning(ReportCategory.AUTHENTICATION, $"ThirdWeb auto-login timed out after {AUTO_LOGIN_TIMEOUT.TotalSeconds}s");
return false;
}
catch (OperationCanceledException)
{
// External cancellation — rethrow so caller knows it was cancelled
throw;
}
catch (Exception e)
{
ReportHub.LogWarning(ReportCategory.AUTHENTICATION, $"ThirdWeb auto-login failed with exception: {e.Message}");
Expand Down
Loading